image.png
本章内容:

  • 单元测试
  • Jest 和 Vue Test Utils 的基础和进阶全覆盖
  • TDD,测试驱动开发,一种全新的开发方式

测试框架

  • 断言
  • Mock
  • 异步支持
  • 代码覆盖率

    测试框架 Jest 简介

    特点

  • 开箱即用,零配置

  • 内置代码覆盖率
  • 容易 mock

安装:yarn add jest
查看版本:npx jext version
断言示例
image.png
测试结果
1前端单元测试框架 Jest 基础知识入门 - 图3
编辑器
如果使用的是 vscode 并且安装了 jest 插件,那么可以实时并且直观的看到测试是否通过
1前端单元测试框架 Jest 基础知识入门 - 图4

Jest实现异步测试

callback

  1. // callback
  2. const fetchUser = (cb) => {
  3. setTimeout(() => {
  4. cb("hello")
  5. }, 100)
  6. }
  7. it('test callback', (done) => {
  8. fetchUser((data) => {
  9. expect(data).toBe("hello")
  10. done() //不加done,会忽略回调等待的时间
  11. })
  12. })

Promise

  1. const userPromise = () => Promise.resolve("hello")
  2. it('test Promise', () => {
  3. //必须要return
  4. return userPromise().then(data => {
  5. expect(data).toBe("hello")
  6. })
  7. })

aysnc await

  1. // async await
  2. const userPromise = () => Promise.resolve("hello")
  3. it('test async await', async () => {
  4. const data = await userPromise()
  5. expect(data).toBe("hello")
  6. })

expect

  1. const userPromise = () => Promise.resolve("hello")
  2. const userPromiseReject = () => Promise.reject("error")
  3. // expect 必须return
  4. it('test with expect', () => {
  5. //可以直接调用expect的resolves方法
  6. return expect(userPromise()).resolves.toBe("hello")
  7. })
  8. // expect reject
  9. it('test with expect reject', () => {
  10. //可以直接调用expect的rejects方法
  11. return expect(userPromiseReject()).rejects.toBe("error")
  12. })

Jest 实现 Mock

为什么需要 Mock

  • 前端需要网络请求
  • 后端依赖数据库等模块
  • 局限性:依赖其它的模块

Mock 解决方案

  • 测试替代,将真实代码替换为替代代码。

Mock 的几大功能

  • 创建 mock function,在测试中使用,用来测试回调
  • 手动 mock,覆盖第三方实现
  • 三大 API 实现不同粒度的时间控制

    函数测试

    ```typescript function mockTest(shouldCall, cb) { if (shouldCall) { return cb(42) } }

it(‘test with mock function’, () => { // 创建一个假的函数实现 const mockCB = jest.fn() mockTest(true, mockCB) // 函数是否被调用过了 expect(mockCB).toHaveBeenCalled() // 是否被参数调用 expect(mockCB).toHaveBeenCalledWith(42) // 被调用的次数 expect(mockCB).toHaveBeenCalledTimes(1) // 函数调用 console.log(mockCB.mock.calls); // 函数调用结果 console.log(mockCB.mock.results); })

  1. 函数没有返回参数,返回undefined<br />![](https://cdn.nlark.com/yuque/0/2022/webp/25963140/1656327006319-e95cdc58-5354-4b10-be6d-0bc7c34ad6f3.webp#clientId=ufcf1783f-3654-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=ufd331b88&margin=%5Bobject%20Object%5D&originHeight=940&originWidth=1721&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u33c12e5f-b266-4ef3-b5ea-df2fe58961a&title=)
  2. ```typescript
  3. function mockTest(shouldCall, cb) {
  4. if (shouldCall) {
  5. return cb(42)
  6. }
  7. }
  8. it('test with mock implementation', () => {
  9. const mockCB = jest.fn(x => x * 2)
  10. mockTest(true, mockCB)
  11. console.log(mockCB.mock.calls);
  12. console.log(mockCB.mock.results);
  13. })

value为84
1前端单元测试框架 Jest 基础知识入门 - 图5

  1. function mockTest(shouldCall, cb) {
  2. if (shouldCall) {
  3. return cb(42)
  4. }
  5. }
  6. it('test with mock mockReturnValue', () => {
  7. const mockCB = jest.fn().mockReturnValue(20)
  8. mockTest(true, mockCB)
  9. console.log(mockCB.mock.calls);
  10. console.log(mockCB.mock.results);
  11. })

value为20
1前端单元测试框架 Jest 基础知识入门 - 图6

第三方模块实现

  1. const axios = require('axios')
  2. module.exports = function getUserName(id) {
  3. return axios
  4. .get(`http://jsonplaceholder.typicode.com/users/${id}`)
  5. .then((resp) => {
  6. return resp.data.username
  7. })
  8. }
  1. const getUserName = require('./user')
  2. it('test with mock modules', () => {
  3. return getUserName(1).then((name) => {
  4. console.log(name)
  5. })
  6. })

image.png

  1. const getUserName = require('./user')
  2. // 先引入 axios 这个模块
  3. const axios = require('axios')
  4. // 调用 jest.mock 接管 axios 模块
  5. jest.mock('axios')
  6. // mock axios.get方法的实现
  7. axios.get.mockImplementation(() => {
  8. return Promise.resolve({ data: { username: 'warbler' } })
  9. })
  10. it('test with mock modules', () => {
  11. return getUserName(1).then((name) => {
  12. console.log(name)
  13. expect(axios.get).toHaveBeenCalled()
  14. expect(axios.get).toHaveBeenCalledTimes(1)
  15. })
  16. })

image.png
或者使用mockReturnValue结果一致
axios.get.mockReturnValue(Promise.resolve({ data: { username: 'warbler' } }))
或者直接使用mockResolveValue结果一致
axios.get.mockResolvedValue({ data: { username: 'warbler' } })

处理重复mock

问题:多次对同一个模块进行mock,产生重复的工作?
解决:在根目录新建mocks文件夹,新建需要mock的模块同名文件axios.js,jest会自动对这个文件夹下的文件进行处理。

  1. const getUserName = require('./user')
  2. // 先引入 axios 这个模块
  3. const axios = require('axios')
  4. // 调用 jest.mock 接管 axios 模块
  5. // jest.mock('axios')
  6. // // mock axios.get方法的实现
  7. // axios.get.mockImplementation(() => {
  8. // return Promise.resolve({ data: { username: 'warbler' } })
  9. // })
  10. it('test with mock modules', () => {
  11. return getUserName(1).then((name) => {
  12. console.log(name)
  13. expect(axios.get).toHaveBeenCalled()
  14. expect(axios.get).toHaveBeenCalledTimes(1)
  15. })
  16. })
  1. const axios = {
  2. get: jest.fn(() => Promise.resolve({ data: { username: 'warbler' } })),
  3. }
  4. module.exports = axios

image.png

mock Timer

  • runAllTimers:执行完所有的 timer
  • runOnlyPendingTimers:执行完正在等待的 timer
  • advanceTimersByTime:精确控制时间流逝多少 ms ```typescript const fetchUser = (cb) => { setTimeout(() => { cb(‘hello’) }, 1000) }

// 所有的 timer 都被 jest 接管 jest.useFakeTimers() jest.spyOn(global, ‘setTimeout’) it(‘test the callback after 1 sec’, () => { const callback = jest.fn() fetchUser(callback) expect(callback).not.toHaveBeenCalled() // setTimeout 此时是一个 mock function expect(setTimeout).toHaveBeenCalledTimes(1) // 一下子执行完所有的 timer jest.runAllTimers() // 是否被调用 expect(callback).toHaveBeenCalled() // 调用的参数 expect(callback).toHaveBeenCalledWith(‘hello’) })

const loopFetchUser = (cb) => { setTimeout(() => { cb(‘one’) setTimeout(() => { cb(‘two’) }, 2000) }, 1000) }

it(‘test the callback in loopFetchUser’, () => { const callback = jest.fn() loopFetchUser(callback) // 没有被调用 expect(callback).not.toHaveBeenCalled() // 执行完正在等待的 timer jest.runOnlyPendingTimers() // 调用次数 expect(callback).toHaveBeenCalledTimes(1) // 上一次调用的参数 expect(callback).toHaveBeenLastCalledWith(‘one’) // 执行完正在等待的 timer jest.runOnlyPendingTimers() // 调用次数 expect(callback).toHaveBeenCalledTimes(2) // 上一次调用的参数 expect(callback).toHaveBeenLastCalledWith(‘two’) })

it(‘test the callback in advance timer’, () => { const callback = jest.fn() loopFetchUser(callback) // 没有被调用 expect(callback).not.toHaveBeenCalled() // 控制时间流逝多少ms jest.advanceTimersByTime(500) // 控制时间流逝多少ms jest.advanceTimersByTime(500) // 调用次数 expect(callback).toHaveBeenCalledTimes(1) // 控制时间流逝多少ms expect(callback).toHaveBeenLastCalledWith(‘one’) jest.advanceTimersByTime(2000) // 调用次数 expect(callback).toHaveBeenCalledTimes(2) // 上一次调用的参数 expect(callback).toHaveBeenLastCalledWith(‘two’) }) ``` image.png