【实战】一、Jest 前端自动化测试框架基础入门(四) —— 前端要学的测试课 从Jest入门到TDD BDD双实战(四)


学习内容来源:Jest入门到TDD/BDD双实战_前端要学的测试课


相对原教程,我在学习开始时(2023.08)采用的是当前最新版本:

版本
@babel/core ^7.16.0
@pmmmwh/react-refresh-webpack-plugin ^0.5.3
@svgr/webpack ^5.5.0
@testing-library/jest-dom ^5.17.0
@testing-library/react ^13.4.0
@testing-library/user-event ^13.5.0
babel-jest ^27.4.2
babel-loader ^8.2.3
babel-plugin-named-asset-import ^0.3.8
babel-preset-react-app ^10.0.1
bfj ^7.0.2
browserslist ^4.18.1
camelcase ^6.2.1
case-sensitive-paths-webpack-plugin ^2.4.0
css-loader ^6.5.1
css-minimizer-webpack-plugin ^3.2.0
dotenv ^10.0.0
dotenv-expand ^5.1.0
eslint ^8.3.0
eslint-config-react-app ^7.0.1
eslint-webpack-plugin ^3.1.1
file-loader ^6.2.0
fs-extra ^10.0.0
html-webpack-plugin ^5.5.0
identity-obj-proxy ^3.0.0
jest ^27.4.3
jest-enzyme ^7.1.2
jest-resolve ^27.4.2
jest-watch-typeahead ^1.0.0
mini-css-extract-plugin ^2.4.5
postcss ^8.4.4
postcss-flexbugs-fixes ^5.0.2
postcss-loader ^6.2.1
postcss-normalize ^10.0.1
postcss-preset-env ^7.0.1
prompts ^2.4.2
react ^18.2.0
react-app-polyfill ^3.0.0
react-dev-utils ^12.0.1
react-dom ^18.2.0
react-refresh ^0.11.0
resolve ^1.20.0
resolve-url-loader ^4.0.0
sass-loader ^12.3.0
semver ^7.3.5
source-map-loader ^3.0.0
style-loader ^3.3.1
tailwindcss ^3.0.2
terser-webpack-plugin ^5.2.5
web-vitals ^2.1.4
webpack ^5.64.4
webpack-dev-server ^4.6.0
webpack-manifest-plugin ^4.0.2
workbox-webpack-plugin ^6.4.1"

具体配置、操作和内容会有差异,“坑”也会有所不同。。。


一、Jest 前端自动化测试框架基础入门



10.Jest 中的 Mock

新建 lesson8.js

export const runCallback = callback => {
   
  callback();
}

要测试 runCallback 一般思路都是创建一个函数,返回一个值,只要 expect 最终拿到这个值就说明没问题,但是 runCallback 的主要功能是执行 callback,若是和本示例这样没有将 callback 返回,岂不是测不了了。。。

因此最佳实践来了,如下。

(1)toBeCalled

新建 lesson8.test.js

import {
    runCallback } from "./lesson8";

test('测试 runCallback', () => {
   
  const func = jest.fn();
  runCallback(func);
  expect(func).toBeCalled();  
})

mock 一个函数,使用 toBeCalled 即可检测函数是否被调用

(2)func.mock

打印看一下 func.mock 里面有啥(console.log(func.mock)):

{
   
  calls: [ [] ],
  instances: [ undefined ],
  invocationCallOrder: [ 1 ],
  results: [ {
    type: 'return', value: undefined } ]
}
  • calls:func 每次执行的入参列表的列表
  • instances:每次执行 func 时创建的实例的列表(即 函数中this的指向,默认 undefined)
  • invocationCallOrder:每次 func 执行的顺序列表(例如:1, 2, 3 表示按顺序执行)
  • results:每次执行 func 的返回值列表

编辑 lesson8.test.js(执行两次 func)

import {
    runCallback } from "./lesson8";

test('测试 runCallback', () => {
   
  const func = jest.fn();
  runCallback(func);
  runCallback(func);
  expect(func).toBeCalled();  
  console.log(func.mock)
})

再看一下 func.mock 里面有啥:

{
   
  calls: [ [], [] ],
  instances: [ undefined, undefined ],
  invocationCallOrder: [ 1, 2 ],
  results: [
    {
    type: 'return', value: undefined },
    {
    type: 'return', value: undefined }
  ]
}

可以看到每一次调用在 func.mock 中都是留有痕迹的

编辑 lesson8.test.js(给 func 传入一个函数,并执行三次)

test('测试 runCallback', () => {
   
  const func = jest.fn(() => 123); // mock 函数,捕获函数的调用
  // func.mockImplementation(() => 123); // 功能同上一句
  // func.mockImplementationOnce(() => 123); // 只模拟一次
  // func.mockImplementationOnce(() => 456); // 只模拟一次
  // func.mockImplementation(() => this);
  // func.mockReturnThis(); // 作用同上一句
  runCallback(func);
  runCallback(func);
  runCallback(func);
  expect(func).toBeCalled();
  // expect(func).toBeCalledWith(); // 断言每次调用的入参内容
  console.log(func.mock)
})

打印结果:

{
   
  calls: [ [], [], [] ],
  instances: [ undefined, undefined, undefined ],
  invocationCallOrder: [ 1, 2, 3 ],
  results: [
    {
    type: 'return', value: 123 },
    {
    type: 'return', value: 123 },
    {
    type: 'return', value: 123 }
  ]
}

注意:带 Once 的要优先于不带的执行

(3)mockReturnValue & mockReturnValueOnce

编辑 lesson8.test.js(让 func 通过 mockReturnValueOnce 来返回值)

test('测试 runCallback', () => {
   
  const func = jest.fn(); // mock 函数,捕获函数的调用
  func.mockReturnValueOnce('once')
  runCallback(func);
  runCallback(func);
  runCallback(func);
  expect(func).toBeCalled();
  console.log(func.mock)
})

打印结果:

{
   
  calls: [ [], [], [] ],
  instances: [ undefined, undefined, undefined ],
  invocationCallOrder: [ 1, 2, 3 ],
  results: [
    {
    type: 'return', value: 'once' },
    {
    type: 'return', value: undefined },
    {
    type: 'return', value: undefined }
  ]
}

可以看到 'once' 只返回了一次

编辑 lesson8.test.js(让 func 通过 mockReturnValueOnce, mockReturnValueOnce 链式调用,mockReturnValue 三种方式来返回值)

test('测试 runCallback', () => {
   
  const func = jest.fn(); // mock 函数,捕获函数的调用
  func.mockReturnValueOnce('1');
  func.mockReturnValueOnce('2');
  func.mockReturnValueOnce('3').mockReturnValueOnce('4').mockReturnValueOnce('5');
  func.mockReturnValue('6'); // 后续每次返回同样的值
  [...new Array(8)].map(() => runCallback(func))
  expect(func).toBeCalled();
  console.log(func.mock)
})

打印结果:

{
   
  calls: [
    [], [], [], [],
    [], [], [], []
  ],
  instances: [
    undefined, undefined,
    undefined, undefined,
    undefined, undefined,
    undefined, undefined
  ],
  invocationCallOrder: [
    1, 2, 3, 4,
    5, 6, 7, 8
  ],
  results: [
    {
    type: 'return', value: '1' },
    {
    type: 'return', value: '2' },
    {
    type: 'return', value: '3' },
    {
    type: 'return', value: '4' },
    {
    type: 'return', value: '5' },
    {
    type: 'return', value: '6' },
    {
    type: 'return', value: '6' },
    {
    type: 'return', value: '6' }
  ]
}

接下来通过示例理解一下 mock 里的 instances

编辑 lesson8.js(callback 运行一次,之后创建一个实例,并赋予一个属性作为标识)

export const runCallback = (callback, index) => {
   
  callback();
  let obj = new callback()
  obj.name = 'callback_' + index
}

编辑 lesson8.test.js

test('测试 runCallback', () => {
   
  const func = jest.fn(); // mock 函数,捕获函数的调用
  func.mockReturnValue('6');  // 后续每次返回同样的值
  [...new Array(3)].map((item, index) => runCallback(func, index))
  expect(func).toBeCalled();
  console.log(func.mock)
})

打印结果:

{
   
  calls: [ [], [], [], [], [], [] ],
  instances: [
    undefined,
    mockConstructor {
    name: 'callback_0' },
    undefined,
    mockConstructor {
    name: 'callback_1' },
    undefined,
    mockConstructor {
    name: 'callback_2' }
  ],
  invocationCallOrder: [ 1, 2, 3, 4, 5, 6 ],
  results: [
    {
    type: 'return', value: '6' },
    {
    type: 'return', value: undefined },
    {
    type: 'return', value: '6' },
    {
    type: 'return', value: undefined },
    {
    type: 'return', value: '6' },
    {
    type: 'return', value: undefined }
  ]
}

可见每次运行默认不会有实例化对象,因此是 undefined,但是在实例化后,就会有一个命名为 mockConstructor 的构造对象

普通函数可以通过 jest.fn() 的方式来 mock,为保证当前部分功能的独立性,那接口请求也是需要 mock 的(避免网络和后端的影响)

编辑 lesson8.js(新增 getData 请求接口)

export const getData = () => {
   
  return axios.get('/api').then(res => res.data)
}

编辑 lesson8.test.js

import {
    runCallback, getData } from "./lesson8";
import axios from 'axios'

jest.mock('axios');

...

test.only('测试 getData', async () => {
   
  axios.get.mockResolvedValue({
   data: 'hello'})
  await getData().then(data => {
   
    expect(data).toBe('hello')
  })
})

注意:jest.mock 必须放在文件的上面紧贴着 import,且外面不能嵌套任何内容否则对其mock的内容的作用域有影响(与被mock内容保持一致,且加载顺序挨着)

总结一下 mock 的几大功能:

  1. 捕获函数的调用和返回结果,以及 this 和调用顺序
  2. 可以自由设置返回结果
  3. 改变函数的内部实现

注意,从25版本开始,官方文档的结构发生了一些变化,最后一个变化前的版本:


本文仅作记录, 实战要点待后续专文总结,敬请期待。。。

最近更新

  1. TCP协议是安全的吗?

    2024-02-16 11:14:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-02-16 11:14:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-02-16 11:14:02       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-02-16 11:14:02       20 阅读

热门阅读

  1. 代码随想录算法训练营29期Day51|LeetCode 139

    2024-02-16 11:14:02       38 阅读
  2. vue3跨组件(多组件)通信:事件总线【Event Bus】

    2024-02-16 11:14:02       35 阅读
  3. GEE:关于在GEE平台上进行回归计算的若干问题

    2024-02-16 11:14:02       37 阅读
  4. Ubuntu+Anaconda 常用指令记录

    2024-02-16 11:14:02       32 阅读
  5. Ajax,

    2024-02-16 11:14:02       32 阅读
  6. 什么时候需要 / 不需要创建索引?

    2024-02-16 11:14:02       38 阅读
  7. 通过`ssh`同步`tmux`剪贴板内容

    2024-02-16 11:14:02       31 阅读
  8. 《Docker极简教程》--Docker容器--Docker容器的概念

    2024-02-16 11:14:02       30 阅读