zl程序教程

您现在的位置是:首页 >  其他

当前栏目

【架构师(第二十七篇)】前端单元测试框架 Jest 基础知识入门

基础知识框架前端入门 架构师 单元测试 jest
2023-06-13 09:15:45 时间

单元测试

单元测试其实在我的实际开发中并没有用到过,但却经常听说,接下来进行单元测试的学习

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

测试框架

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

测试框架 jest 简介

特点

  • 开箱即用,零配置
  • 内置代码覆盖率
  • 容易 mock

安装

npm i --save-dev jest

查看版本

npx jest --version
27.5.1

断言示例

test('test common matcher', () => {
  expect(2 + 2).toBe(4);
});

test('test not equal', () => {
  expect(2 + 2).not.toBe(5);
});

test('test tp be true or false', () => {
  expect(1).toBeTruthy();
  expect(0).toBeFalsy();
});

test('test number', () => {
  expect(4).toBeGreaterThan(3);
  expect(2).toBeLessThan(3);
});

test('test object', () => {
  expect({ name: 'warbler' }).toEqual({ name: 'warbler' });
});

测试结果

编辑器

如果使用的是 vscode 并且安装了 jest 插件,那么可以实时并且直观的看到测试是否通过

Jest 实现异步测试

回调方式

// callback
const fetchUser = (cb) => {
  setTimeout(() => {
    cb("hello")
  }, 100)
}

it('test callback', (done) => {
  fetchUser((data) => {
    expect(data).toBe("hello")
    done()
  })
})

Promise

需要 return

// promise
const userPromise = () => Promise.resolve("hello")

it('test Promise', () => {
  return userPromise().then(data => {
    expect(data).toBe("hello")
  })
})

async await

// async await
const userPromise = () => Promise.resolve("hello")

it('test async await', async () => {
  const data = await userPromise()
  expect(data).toBe("hello")
})

expect

expect 会添加一些属性,也可以获取到 promiserejectresolve,需要 return

const userPromise = () => Promise.resolve("hello")
const userPromiseReject = () => Promise.reject("error")

// expect
it('test with expect', () => {
  return expect(userPromise()).resolves.toBe("hello")
})

// expect reject
it('test with expect reject', () => {
  return expect(userPromiseReject()).rejects.toBe("error")
})

Jest mock

为什么需要 Mock

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

Mock 解决方案

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

Mock 的几大功能

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

函数测试

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);
})

这里结果是 undefined ,因为并没有 mock 函数的实现,所以默认为 undefined

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

it('test with mock implementation', () => {
  const mockCB = jest.fn(x => x * 2)
  mockTest(true, mockCB)
  console.log(mockCB.mock.calls);
  console.log(mockCB.mock.results);
})

现在 mock 函数的实现, 返回参数的二倍,可以看见 value 变成了 84

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

it('test with mock mockReturnValue', () => {
  const mockCB = jest.fn().mockReturnValue(20)
  mockTest(true, mockCB)
  console.log(mockCB.mock.calls);
  console.log(mockCB.mock.results);
})

还可以 mock 函数的返回值,可以看见 value 变成了 20

第三方模块实现

// 一个真实的网络请求模块
const axios = require('axios')

module.exports = function getUserName(id) {
  return axios.get(`http://jsonplaceholder.typicode.com/users/${id}`).then((resp) => {
    return resp.data.username
  })
}

进行测试

const getUserName = require('./user')

it('test with mock modules', () => {
  return getUserName(1).then((name) => {
    console.log(name);
  })
})

结果输出了 Bret

接下来使用 jest 进行第三方模块 axiosmock

const getUserName = require('./user')

// 先引入 axios 这个模块
const axios = require('axios')
// 调用 jest.mock 接管 axios 模块
jest.mock("axios")
// mock axios.get方法的实现
axios.get.mockImplementation(() => {
  return Promise.resolve({ data: { username: 'warbler' } })
})

it('test with mock modules', () => {
  return getUserName(1).then((name) => {
    console.log(name);
    expect(axios.get).toHaveBeenCalled()
    expect(axios.get).toHaveBeenCalledTimes(1)
  })
})

结果已经变成了 warbler

或者使用 mockReturnValue 直接返回结果,结果是一样的。

axios.get.mockReturnValue(Promise.resolve({ data: { username: 'warbler' } }))

还用更简单的方式,直接返回一个 Promiseresolve

axios.get.mockResolvedValue({ data: { username: 'warbler' } })

如果多处对同一个模块进行 mock,会造成大量重复的工作,可以在根目录下新建 __mocks__ 文件夹, 然后新建需要 mock 的模块同名文件 axios.jsjest 会自动对这个文件夹下的文件进行处理。

const axios = {
  get: jest.fn(() => Promise.resolve({ data: { username: "warbler" } }))
}
module.exports = axios

timer mocks

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

// 所有的 timer 都被 jest 接管
jest.useFakeTimers();

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')
})