先放一些值得参考的文档/教程(持续添加中):
- jest官方文档:jestjs.io/zh-Hans/doc…
- jest官方文档 react-jest: jestjs.io/zh-Hans/doc…
- testing-library作者的个人博客(思想性的东西多一些):kentcdodds.com/
- 一份不错的入门教程:github.yanhaixiang.com/jest-tutori…;及其项目github github.com/haixiangyan…
从零开始熟悉jest
如果你想尽快工程落地,后面会有一篇工程落地文章,可以期待一下。
简易版的单测js项目
可以开一个新项目,先参考 github.yanhaixiang.com/jest-tutori… 看看单测到底是什么&能做什么。这份教程写的还是比较详细的,一步一步跟着配置就可以了。
一份简易版的含单测的js项目(node环境)应该拥有的文件结构如下:
├── jest.config.js
├── package-lock.json
├── package.json
├── src
│ └── utils
│ └── sum.js
├── tests
│ └── utils
│ └── sum.test.js
└── coverage
├── clover.xml # Clover XML 格式的覆盖率报告
├── coverage-final.json # JSON 格式的覆盖率报告
├── lcov-report # HTML 格式的覆盖率报告
│ ├── base.css
│ ├── block-navigation.js
│ ├── favicon.png
│ ├── index.html # 覆盖率根文件
│ ├── prettify.css
│ ├── prettify.js
│ ├── sort-arrow-sprite.png
│ ├── sorter.js
│ └── sum.js.html # sum.js 的覆盖率情况
└── lcov.info
按照上面的操作进行完毕,按我们平常的书写习惯进行代码的编写测试,会发现许多报错:
1. jest不支持es6语法的import export
为了方便书写测试代码,加一个babel插件来进行转义吧:
第一步:在根目录下添加.babelrc文件
# 方案1
{
"plugins": ["@babel/plugin-transform-modules-commonjs"]
}
# 方案2
{
"presets": [
"@babel/preset-env"
],
}
第二步:安装相应插件/包
npm install --save-dev @babel/plugin-transform-modules-commonjs
或
npm i --save-dev @babel/preset-env
ok啦
ps:@babel/preset-env这个包就是支持es语法的,可以通过配置按需引入,默认全部引入
commonjs转换插件相对来说小得多。
2. jest无法识别我们配置好的'~'路径
这个'~'映射到'./src'路径的配置是在jsconfig.json中配置过的,相应的,我们也需要在jest.config.js中配置这种映射:
moduleNameMapper: {
'^~/(.*)$': '<rootDir>/src/$1',
},
体验过了一个简易的单测项目,可能你对单测有了一个基础的认识,那么问题来了,怎么写自己的单测呢?
jest是怎么知道测试需要运行哪些代码的?
可以查看jest.config.js,以下几个配置是用来匹配测试文件的:
module.exports = {
// A list of paths to directories that Jest should use to search for files in
roots: [
"<rootDir>"
],
// The glob patterns Jest uses to detect test files
testMatch: [
"**/__tests__/**/*.[jt]s?(x)",
"**/?(*.)+(spec|test).[tj]s?(x)"
],
// The regexp pattern or array of patterns that Jest uses to detect test files
testRegex: [],
}
默认情况下:
- tests目录中 .js 和 .jsx 文件
- 以 .test 或 .spec 后缀文件
自定义指定测试文件,使用 testRegex 属性修改
其他单测运行方案(某些情况需要的话:
- 指定单个文件的测试
仅运行指定文件名称或文件路径的测试
# 指定测试文件的名称
jest my-test
# 指定测试文件的路径
jest path/to/my-test.js
# 如果你已经配置了npm的快捷命令,那么↓
npm run test my-test
- 只进行发生改动的文件的测试
# jest只运行基于git(未提交文件)的文件测试
jest -o
- 其他个性化方案
# 运行匹配特定名称的测试用例
jest -t name-of-test-or-describe
# 测试会随着代码热更新
jest --watch #runs jest -o by default
jest --watchAll #runs all tests
jest --color #运行时彩色显示
测试文件是怎么写出来的?
基础方法
jest官放的api文档 https://jestjs.io/zh-Hans/docs/api
先着重关注一下jest的几个核心API最简洁的使用方式:test、expect和describe;
- test(it):
it(name, fn, timeout)test()函数用于描述一个测试用例,it是它的别名。
在第二个参数fn内,需要描述你对于某个方法/函数等的期望,这里需要expect这个api来进行具体描述
- expect:
expect(fn).xx(...args)expect()函数是用于指示我们期望的结果。
expect可进行断言的一些方法在这里:jestjs.io/zh-Hans/doc…,其中对于测试一个函数来说最简单常用的就是expect(fn).toBe(value),这个value就是你期望函数fn的返回结果。
expect和test结合使用,比如下面的例子:运行传递进expect()的方法'1+1',期望得到'2'的结果。
it('1+1=2', () => {
expect(1+1).toBe(2)
})
这套方法还是比较语义化的,个人感觉用起来很友好。
- describe:
describe(name, fn)describe()是将多个相关的测试组合在一起的块。
简单来说,一个describe可以包裹多个test,多个test都满足的情况下,describe这个块的结果才正确。比如下面这个例子,测试了myBeverage这个对象描述的某个饮料是否好喝且不酸。
const myBeverage = {
delicious: true,
sour: false,
};
describe('my beverage', () => {
test('is delicious', () => {
expect(myBeverage.delicious).toBeTruthy();
});
test('is not sour', () => {
expect(myBeverage.sour).toBeFalsy();
});
});
有了这三个api,就可以进行一个方法函数的测试了。
异步测试
jest官方文档的描述:jestjs.io/zh-Hans/doc…
回调
应该让test的参数调用done,否则test执行不会等待异步数据,测试结果可能有误。
以下是我自己的测试结果:
// 延时执行函数
export const async = (fn) => {
setTimeout(() => {
fn('hello world')
}, 4000)
}
如果测试代码这么写:
import { async } from '../utils/async'
test('value is hello jest', () => {
async((data) => {
expect(data).toBe('hello jest')
})
})
观察结果,1ms就运行出了测试结果,且pass了。但这明显不合理,async方法对回调传入的参数是hello world,测试expect(data).toBe('hello jest')是不可能pass的。
虽然在延时之后jest也会抛出应错误,但是测试用例是过了的。很明显这样无法在工程中应用。
如果加上done
import { async } from '../utils/async'
test('value is hello jest', done => {
async((data) => {
expect(data).toBe('hello jest')
done()
})
})
运行结果是fail,也抛出了相应错误。合理。
promise
promise的测试需要给test的函数返回这个promise对象:
export const myPromise = () => (
new Promise((resolve) => {
setTimeout(() => (
resolve('hello world')
), 1000)
})
)
test('test my promise is hello jest', () => {
return myPromise().then((data) => {
expect(data).toBe('hello jest')
})
})
如果丢了return,那么测试会像回调方式丢了done一样pass
写一个测试文件到底要测什么呢?
首先要明确一点,无论你测试的内容是什么,都不要对测试对象外部的内容进行测试,你应该默认它们都能够正常运行。
一个React项目里往往包含以下几部分:
- 公共组件 components,组件内主要是ui及其交互逻辑;
- 业务组件 container,组件内主要是网络请求&数据填充逻辑,也包含一些ui交互逻辑;
- 公共方法 utils,往往是纯函数,有明确的输入-输出关系,相对来说容易测试;
- 公共静态变量 constants,纯定义,理论上无需测试,除非你的定义包含了复杂参数;
- 状态管理库,不同的项目可能会使用不同的redux库,不同库的数据处理方案可能不一致,不好统一来讲;
- router配置。
-
对于组件来说,测试可以分为几个模块:
-
网络请求或其他副作用是否正常
-
组件内部的业务方法是否正常
-
DOM按逻辑渲染是否正常
-
-
对于公共方法来说,按照一般的单测思路去测试即可;
-
对于状态管理库来说,这些数据处理逻辑往往都是和业务代码强相关的,而且状态管理库的管理方式也不一而同,也许按集成测试的思路去测试你的业务代码才是更合理的;
-
对于router来说,能够测试的包含两方面:
-
当前路由和url是否匹配
-
当前路由下的页面是否正常渲染
-
当你的项目刚开始测试时,可以从最简单的单测开始。单测就是测试某一个方法,具体到前端项目中可能就是你的公共方法函数(纯jest就可以做到);然后可以测一些公共组件(storybook/react test library);然后再尝试测一些业务代码。
参考 juejin.cn/post/707923…,测试时,不要太过于聚焦你的业务细节。
另外,对于某一个特定的函数来说,测试用例一般会找一些边界条件来进行测试。如果你有一个加法函数,你可以试试:正常的数字参数、传一个undefined/[object]等等其他类型、参数缺失等用例下能否通过测试。
在已有项目中的安装与配置
安装jest及相关包
# 安装jest
npm i --save-dev jest
# es module支持
npm i --save-dev babel-jest @babel/preset-env
# react支持
npm i --save-dev @babel/preset-react react-test-renderer
# jest28版本及以上,如果测试环境选择jsdom还需要添加环境包
# 参考 https://jestjs.io/docs/28.x/upgrading-to-jest28#jsdom
# npm
npm i --save-dev jest-environment-jsdom
# less支持,组件测试时才需要
npm i --save-dev jest-less-loader
# 一次性打包版(不含less)
npm i --save-dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer jest-environment-jsdom
其他插件及配置
在.babelrc中添加presets配置,前面说过,这里是用来支持es语法的
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": []
}
在package.json中添加快捷启动
"scripts": {
"test": "jest",
"coverage": "jest --coverage"
},
其实npm run test未必比npx jest方便,但好处是,如果你更换了测试框架还是可以用npm run test启动,且npm run xxx比较符合日常的命令习惯;并且,如果你需要个性化定制jest的参数,仍旧只需要运行npm run test即可。
自动生成jest配置文件
npx jest --init
✔ Would you like to use Jest when running "test" script in "package.json"? … yes
✔ Would you like to use Typescript for the configuration file? … no
✔ Choose the test environment that will be used for testing › jsdom (browser-like)
✔ Do you want Jest to add coverage reports? … yes
✔ Which provider should be used to instrument code for coverage? › babel
✔ Automatically clear mock calls, instances, contexts and results before every test? … yes
- 添加npm run test的执行方式(可通过
npm run test或npx jest执行测试) - 我们的项目没有使用ts,所以选no
- 选择测试环境,前端项目就选jsdom(如果你的测试内容没有进行任何dom操作,也可以选nodejs)
- 增加代码覆盖率报告
- 前端使用babel检测代码覆盖率
- 自动清除每个测试之间的模拟调用和实例(单测而非集测)
选完后就生成了一个jest.config.js文件,查看文件,所有可配置选项都有注释。
这里有一个比较全的配置文档,可以根据这个来配置:m.w3cschool.cn/jest_cn/jes…
module.exports = {
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// The directory where Jest should output its coverage files
coverageDirectory: 'coverage',
// The test environment that will be used for testing
testEnvironment: 'jsdom',
// 这个path映射需要自己配置一下
moduleNameMapper: {
'^~/(.*)$': '<rootDir>/src/$1',
},
};
一些常用选项含义:
- automock: 告诉 Jest 所有的模块都自动从 mock 导入
- clearMocks: 在每个测试前自动清理 mock 的调用和实例 instance
- collectCoverageFrom: 生成测试覆盖报告时检测的覆盖文件
- coverageDirectory: Jest 输出覆盖信息文件的目录
- coverageReporters: 列出包含 reporter 名字的列表,而 Jest 会用他们来生成覆盖报告
- coverageThreshold: 测试可以允许通过的阈值
- moduleDirectories: 模块搜索路径
- moduleFileExtensions: 代表支持加载的文件名
- testPathIgnorePatterns: 用正则来匹配不用测试的文件
- setupFilesAfterEnv: 配置文件,在运行测试案例代码之前,Jest 会先运行这里的配置文件来初始化指定的测试环境
- testMatch: 定义被测试的文件
- transformIgnorePatterns: 设置哪些文件不需要转译
- transform: 设置哪些文件中的代码是需要被相应的转译器转换成 Jest 能识别的代码,Jest 默认是能识别 JS 代码的,其他语言,例如 Typescript、CSS 等都需要被转译。
将coverage文件夹加入gitignore
coverage文件夹内的信息都是给我们进行测试用的,而且每run test一次就会自动生成,完全没有必要push到远程仓库,只留在本地就可以了。
在项目根目录下的.gitignore中添加coverage
# Coverage directory used by tools like istanbul
coverage
其他命令
npx jest --coverage 生成代码覆盖率,不过上面的collectCoverage: true配置已经帮我们设置了每次执行测试都会自动生成打印覆盖率。
npm run jest name只执行name测试文件中的测试