前端单元测试jest使用学习笔记

1,103 阅读1分钟

中文文档

基础知识

前期准备

  1. npx jest --init初始化jest.config.js模块
  2. 脚本jest --watchAll启动监听修改。npx jest 文件路径,只测试单个文件。npx jest 文件名部分将匹配所有包含改部分的文件。npx jest -o只检查git/hg未提交的代码
  3. 匹配/^.+.test.js$/模块,以下方式可以扩展它
// jest.config.js
const {defaults} = require('jest-config');
module.exports = {
  moduleFileExtensions: [...defaults.moduleFileExtensions, 'ts', 'tsx'],
  // moduleFileExtensions: ["js", "jsx", "ts", "tsx", "json", "node"],
};
  1. 可以在package.json中写入脚步"precommit": "jest"
"husky": {
  "hooks": {
    "pre-commit": "npm run test"
  }
},

pre表示后面执行的命令之前执行post表示后面执行的命令执行后执行,即precommit -> commit -> postcommit

常用语法

  1. 作用域describe: (name: number | string | Function | FunctionLike, fn: EmptyFunction): void
  2. 测试内容ittest: (name: string, fn?: ProvidesCallback, timeout?: number): void;ProvidesCallback的第一个参数done方法表示当前微任务(个人测试)结束后当前测试任务结束,可以返回Promise 第三个参数是异步函数测试的超时时间
// 对ProvidesCallback参数的测试
// 由于异步代码it无法获知
const lazy = (fn)=> {
    setTimeout(() => {
        fn();
    }, 3000);
}
it('ProvidesCallback', function (
// done // 注意,如果写了done就必须执行
) {
    const callback = jest.fn();
    lazy(callback);
    // done()
    // Promise.resolve().then(() => {
    //    expect(callback).toBeCalled();
    // })
    setTimeout(() => {
        // done()
        expect(callback).toBeCalled(); // 测试未执行至此当前测试就已退出
    }, 1000);
    
    // 以下代码也可正常通过测试,建议和assertions一起使用
    // return new Promise(resolve => {
    //    setTimeout(() => {
    //        expect(callback).toBeCalled();
    //        resolve()
    //    }, 3001);
    // })
})

describe.onleit.onledescribe.skipit.skip表示只运行或跳过。

  1. expect: 用于校验测试是否通过的方法,常见有严格相等expect(1).toBe(1)、递归子项相等expect({a: 1}).toEqual({a: 1})、是否已赋值expect(null).toBeDefined()、不匹配expect().not.
  2. beforeAllafterAllbeforeEachbeforeEach: (fn: ProvidesCallback, timeout?: number) => any分别是当前作用域(由describe产生)所有测试开始前、所有测试结束后、每一个测试开始前、每一个测试结束后执行。执行顺序可以为beforeAll->beforeEach->beforeEach->beforeEach->beforeEach->...->afterAll。顺序外层before->内层before->内层after->外层after
  3. expect.assertions(number)表示当前测试需要经过多少个expect,否则测试不通过。主要用途是异步时是否按正常逻辑去执行了expect
  4. jest.fn():用于处理测试中创建的函数的执行次数或参数或返回值
it('jest.fn', function () {
    const fn = jest.fn()
        .mockReturnValue(1) // 声明返回值,也可以声明传参
    let num = fn()
    expect(num).toBe(1)
    // 是否调用过
    expect(fn).toBeCalled()
    // 调用的次数
    expect(fn).toBeCalledTimes(1)
})

语法使用场景:

  • fetch中的回调方法可以穿入fn,测试fn是否执行
  • react hooks中测试在生命周期中执行的正确性,ahooks中useUnmount就是用的这种方式测试
it('test unmount', async () => {
  const fn = jest.fn();
  const hook = renderHook(() => useUnmount(fn));
  expect(fn).toBeCalledTimes(0);
  hook.rerender();
  expect(fn).toBeCalledTimes(0);
  hook.unmount();
  expect(fn).toBeCalledTimes(1);
});
  1. jest.mock:作用类似jest.fn,不同之处在于jest.mock需要引入一个模块jest.mock('./a.js'),类似于将a中导出的函数都用jest.fn封装,于是我们可以记录a.js中函数被调用的次数
const a = requrie(./a.js) // a中有fn方法
const b = requrie(./b.js) // b方法调用了a.fn方法
jest.mock('./a.js')
it('jest.mock', function () {
    b()
    // 是否调用过
    expect(a.fn).toHaveBeenCalled()
    // 调用的次数
    expect(a.fn).toHaveBeenCalledTimes(1)
})
  1. jest.spyOn:对jest.mock的细化,接收两个参数,第一个参数是模块或对象,第二个参数是对象中的函数名
const a = requrie(./a.js) // a中有fn方法
const b = requrie(./b.js) // b方法调用了a.fn方法

it('jest.spyOn', function () {
    b() // 此处调用不会计入次数
    const fn = jest.spyOn(a, 'fn')
    b()
    // 是否调用过
    expect(fn).toHaveBeenCalled()
    // 调用的次数
    expect(fn).toHaveBeenCalledTimes(1)
})
  1. 定时器相关
  • jest.useFakeTimers(): 告诉jest当前模块可以使用假时间,即可以控制时间快进
  • jest.runAllTimers(): 让所有定时器立即执行
  • jest.clearAllTimes(): 清除所有定时器
  • jest.runOnlyPendingTimers(): 立即执行最快的定时器
  • jest.advanceTimersByTime(): 让时间快进jest.advanceTimersByTime(3000)快进3s

在react中使用jest

react中常用的测试库

react-test-renderer,以及基于react-test-renderer的@testing-library/:主要有react、react-hooks(更推荐)

@testing-library/react-hooks

常用两个api: renderHook, act,最新版本还有cleanup, addCleanup, removeCleanup, suppressErrorOutput

  • act 为react-test-renderer导出方法,修改状态的方法必须在该函数中调用
  • renderHook renderHook函数接收一个函数和props,函数返回值为测试的hook调用后的结果,函数传参为之后rerender调用时的传参。

renderHook返回值主要有result、rerender、unmount、waitForNextUpdate。waitFor、waitForValueToChange

result.current即测试的hook调用后的结果

rerender方法可以改变hook的prop,无须在act中使用,内部已实现。unmount卸载后也可以用它重新挂载

unmount方法执行时当前hook卸载

waitForNextUpdate等待下一个状态更新,超时时间超过1s则测试不通过。主要作用是等待Promise模拟的接口调用后数据变更

// useMyHook
const [state, setState] = useState(1)
useEffect(() => {
    api().then((num) => setState(num))
}, [])
return state
// test.js
const api = () => Promise.resolve(3)
let hook
 // 此处后续在使用rerender时 await act 也可行,是因为微任务执行顺序
 // 开发时请使用waitForNextUpdate
act(() => {
    hook = renderHook((r = api) => useMyHook(r))
})
// 只有等待模拟数据来时才往下走
await hook.waitForNextUpdate()
expect(hook.result.current).toBe(3)
  • cleanup方法用于清除缓存,防止内存泄露

@testing-library/react

  • fireEvent 模拟用户操作 例如fireEvent.click(HtmlElement)
  • render 加载组件,返回获取组件节点的方法,常用getByText、getByRole

参考

// jest.config.*
import type { Config } from '@jest/types'
const config: Config.InitialOptions = {
  preset: 'ts-jest/presets/js-with-ts',
  clearMocks: true,
  testEnvironment: 'jsdom',
  modulePathIgnorePatterns: ['<rootDir>/package.json'],
  moduleNameMapper: {
    '@/(.*)$': '<rootDir>/src/$1'
  },
  rootDir: '.',
  resetMocks: false,
  globals: {
    'ts-jest': {
      tsconfig: 'tsconfig.json'
    }
  }
}
export default config

npx create-react-app my-app中使用

react-scripts库内置了jest,可以直接使用

在其它中使用

npm install --save-dev jest babel-jest babel-preset-es2015 babel-preset-react react-test-renderer
// .babelrc
{
  "presets": ["es2015", "react"]
}

特别注意Jest 版本 24 开始不再支持 Babel 6。推荐升级Babel 7。但是如果你无法升级到 Babel 7,可以继续使用 Jest 23,或者升级到 Jest 24 并且使 babel-jest 固定在 23 的版本

"dependencies": {
  "babel-core": "^6.26.3",
  "babel-jest": "^23.6.0",
  "babel-preset-env": "^1.7.0",
  "jest": "^24.0.0"
}

在vue中使用

使用vue-cli搭建,选择unit -> jest,注意在package.json中识别测试的文件规则为

[  "**/__tests__/*.{j,t}s?(x)",  "**/tests/unit/**/*.spec.{j,t}s?(x)"]

@vue/test-utils官网

// jest.config.*
import type { Config } from '@jest/types'
const config: Config.InitialOptions = {
  preset: 'ts-jest',
  clearMocks: true,
  testEnvironment: 'jsdom',
  modulePathIgnorePatterns: ['<rootDir>/package.json'],
  roots: ['<rootDir>'],
  transform: {
      // 注意jest版本和vue版本,低版本使用vue-jest
      // https://github.com/vuejs/vue-jest/issues/355
    '^.+\.vue$': 'vue3-jest' 
  },
  resetMocks: false,
  moduleFileExtensions: ['ts', 'tsx', 'js', 'json', 'vue']
}
export default config