花一个小时,迅速掌握Jest的全部知识点~
大家好,我是小杜杜,我们知道常用的测试类型有:功能测试
、单元测试
、集成测试
、冒烟测试
等,其中单元测试
是我们必须要掌握的一种技术,所以我们今天一起来看看为什么需要单元测试,作为一名React
开发者,需要掌握哪些相关的技能
如果你有耐心地看完本章,你将会学习到以下内容:
- 什么是单元测试?我们为什么需要?
- 如何做一个简单的单元测试?
- Jest 有哪些语法,Jest 的报告包含哪些信息?
- 如何与ts兼容,需要哪些配置?
- ...
同时附上一张今天的知识图,还请各位小伙伴多多支持~
单元测试
有些小伙伴看到要讲单元测试
,可能会下意识的避而远之,因为单元测试
相对而言比较麻烦,而且从一定的角度来说,也比较鸡肋
,原因主要有以下几点:
1.对于大多数人而言,实际的工作中并不需要(需求都写不完,怎么可能还去写单元测试)
2.Jest
的语法相当于一门新的语言,是有一定的学习成本
3.配置麻烦,虽然Jest
的上手文档看似非常简单,但实际情况是:只要有一个配置的不合适,所有的测试都运行不了,这点是比较难受的
以上几点,可能是大家并不想碰单元测试的主要原因,但单元测试
确实也是不可或缺的一种实际技能
,像一些开源的产品,都会有最基本的单元测试,所以单元测试
是我们进阶
中不可缺少的一部分~
什么是单元测试?
单元测试:指的是以原件的单元
为单位,对软件进行测试。
其中单元
可以是一个函数
,也可以是一个模块
或一个组件
,基本特征就是只要输入不变,必定返回同样的输出。
如果一个软件越容易些单元测试,就表明它的模块化结构越好,给模块之间的耦合越弱。
我们都知道React
是非常自由的,其组件化的思想,React Hooks
的普及,都非常适合进行单元测试
单元测试带来了哪些好处?
首先我认为单元测试
是一个非常具有挑战性的活,因为它是一个不是成功就是失败的活,期间可能会出来各种各样的问题,动手实现下,还是比较有益的,接下来看看单元测试可以给我们带来哪些好处:
保质保量
单元测试可以有效的防止我们减少bug率
虽然bug
我们无法避免,但没有人想bug
多,单元测试会让我们不得不去思考一些异常场景
,这样无形之中,增强了代码的质量
描述代码
对现有的代码进行描述,也就是将用例作为记录。
举个例子,我们有时写一个比较大的组件时,并没有比较好的示例来说明每个参数的效果,确保各种情况下,代码都能正常运行,而单元测试可以诠释这段代码的意义
,并且可以供其他开发者查看,增强代码的可读性
提升个人实力
抛开其他的不谈,单元测试可以很好的提升专业领域
。
首先单元测试
可以算作一个单独领域,不同的框架需要不同的配置,如果依赖的组件发生变化,受影响的组件就有可能发生错误。
其次,不同的环境、不同的测试框架都会有一定的冲突(版本问题)
最后,可以模拟各种环境,不同环境的Mock
能力,比如:React
中有react-testing-library
,Next
中有@nestjs/testing
等
可以看出单元测试
是一个非常大的模块,也是非常值得去研究的领域,总体来说利大于弊
,还请想进阶的伙伴耐心的看完,相信一定会让你受益良多~
Jest
Jest
是Facebook
开源的一个前端测试框架,主要用于React
和React Native
的单元测试,并且被集成在create-react-app
中。
Jest的特点
易用性
:基于Jasmine,提供断言库
,支持多种测试风格适应性
:Jest是模块化
、可扩展
和可配置
的沙箱
和快照
:Jest内置了JSDOM,能够模拟浏览器环境
,并且并行执行快照测试
:Jest能够对React组件树进行序列化
,生成对应的字符串快照,通过比较字符串提供高性能的UI检测Mock系统
:Jest实现了一个强大的Mock系统,并支持自动
和手动mock
支持异步代码测试
:支持Promise
和async/await
自动生成静态分析结果
:内置Istanbul
,测试代码覆盖率,并生成对应的报告
第一个测试程序
初始化项目
mkdir jest-test
cd jest-test
// 初始化package.json文件
npm init -y
// 安装jest
npm i -D jest
初始化jest
// 执行
npx jest --init
此时会弹出一堆问题供你选择,当你选择完后会生成一个jest.config.js
的文件,这个文件就是关于Jest
配置的文件。
再来看看jest.config.js
文件:
module.exports = {
clearMocks: true,
collectCoverage: true,
coverageDirectory: "coverage",
coverageProvider: "v8",
};
示例
我们在目录下创建src/sum.js
文件 和src/sum.test.js
文件
// src/sum.js
const sum = (a, b) => {
return a + b;
}
module.exports = sum;
// src/sum.test.js
const sum = require('./sum');
describe('sum', () => {
it('求和:1 + 2 = 3', () => {
expect(sum(1, 2)).toEqual(3);
});
})
执行
此时,我们就可以执行命令
// npx jest
npm run test
就可以看到一下信息
单独执行
当我们的项目比较大的时候,只需要测试当前的文件即可
//npx jest
npm run test 文件路径
这里的文件路径是相对路径
,会自动匹配格式相同的文件,如:
Jest 配置讲解
我们已经完成了一个简单的jest
测试,接下来我们讲解下各个文件的配置模块,方便我们之后的进行
配置文件
我们通过npx jest --init
生成一个jest.config.js
的文件,这个文件就是有关jest
的配置文件
此外,你可以通过npx jest --help
查看所有Jest Cli
选项
由于配置的字段非常多,这里介绍一些常用的配置,具体的字段可以查看:配置 Jest
三种方式
第一种:我们可以将配置文件写入jest.config.js
,返回一个对象
或是一个函数
,如:
// 对象
import type {Config} from 'jest';
const config: Config = {
verbose: true,
...
};
export default config;
// 函数
import type {Config} from 'jest';
export default async (): Promise<Config> => {
return {
verbose: true,
...
};
};
第二种:可以像package.json
配置一个对应的json
文件,命名为:jest.config.json
, 如:
{
"bail": 1,
"verbose": true,
...
}
第三种:直接写在package.json
中,写在jest
中,如:
{
...
"jest": {
"verbose": true
}
...
}
具体字段
基本字段
clearMocks
:类型:boolean,默认:false,在每次测试前自动清除模拟的上下文collectCoverage
:类型:boolean,默认:false,是否开启覆盖率
coverageDirectory
:类型:string,默认:undefined,生成的覆盖率文件的文件位置coverageProvider
:类型:string,babel(默认)/v8,指示应使用哪个提供程序来检测代码以进行覆盖maxConcurrency
:类型:number,默认:5,限制使用时允许同时运行的测试数量的数字。preset
:类型:string,默认:undefined,预设字段,应该指向哪个文件,可以是文件包或路径,比如安装ts-jest
转译transform
:类型object\<string, pathToTransformer | [pathToTransformer, object]>
,默认:{"^.+\\.[jt]sx?$": "babel-jest"}
,转换器,可以使用正则,用以匹配路径transformIgnorePatterns
:类型:array<string>
,默认:["/node_modules/", "\\.pnp\\.[^\\\/]+$"]
,在转换之前与所有源文件路径匹配的正则表达式模式字符串数组。如果文件路径与任何模式匹配,则不会对其进行转换。displayName
:类型:string | object,默认:undefined,通过这个参数可以直接告诉测试属于哪个项目cacheDirectory
:类型:string,默认:/tmp/<path>
,用来储存依赖信息缓存的目录。testTimeout
:类型:number,默认:5000,测试默认的超时时间testEnvironment
:类型:string,默认:node,用于模拟测试的测试环境,如果我们用到浏览器的环境(如:document),可以用jsdom
代替testMatch
:类型:array<string>
,用于匹配对应文件下的文件testPathIgnorePatterns
:类型:array<string>
,默认:["/node_modules/"]
,在检测的时候跳过一些文件
...
collectCoverageFrom
类型:array,默认:undefined,通过这个参数可以设置哪些文件收集配置信息,如:
import type {Config} from 'jest';
const config: Config = {
collectCoverageFrom: [
'**/*.{ts,tsx}',
'!**/node_modules/**',
'!**/vendor/**',
],
};
export default config;
这个配置将会收集rootDir下的ts, tsx
的所有文件
并不会匹配**/node_modules/**
或 **/vendor/**
的文件
projects
类型:array<string | ProjectConfig>,默认:undefined,我们可以通过这个参数来配置我们的项目,它会提供一组路径或 glob 模式时,Jest 将同时在所有指定项目中运行测试。
比如:
import type {Config} from 'jest';
const config: Config = {
projects: ['<rootDir>', '<rootDir>/examples/*'],
};
export default config;
这个配置可以在根目录
以及示例目录中
的每个文件夹中运行 Jest
再举一个例子:
const config: Config = {
projects: [{
testEnvironment: 'jsdom',
displayName: 'react-jest',
testMatch:[`<rootDir>/react-jest/src/**/*.test.ts?(x)`],
testPathIgnorePatterns: ['/node_modules/'],
cacheDirectory: `./node_modules/.cache/jest`,
testTimeout: 30000,
...
}],
};
像上面这个,我们知道jest
的默认环境为node
,但我们在react-jest
的环境希望是浏览器的环境,就可以单独设置react-jest
下的文件为jsdom
,匹配test
的ts
或tsx
文件,去除一些文件(如node_modules
)和一些其他的配置
全局设定
我们通过sum.test.js
文件,发现了describe
和expect
,但我们并没有引入对应的函数,却能正常的使用,这是为什么呢?
实际上Jest
会将这些方法和对象注入到测试文件的全局环境
里,所以我们在使用的时候并不需要通过import
或require
当然,如果你一定要引用,可以这样引用:
import {describe, expect, test} from '@jest/globals
我们主要讲一下6个主要的方法,更多的方法还请参考:Jest-全局设定
describe
describe: 描述块,将一组功能相关的测试用例组合在一块
用法:describe(name, fn)
name
:string,描述的话语fn
:() => void,将所有的代码写到这个函数里即可
it
it: 别名test
, 用来存放测试用例,可以说有几个it
就会有几个测试用例
用法:it(name, fn, timeout)
或 test(name, fn, timeout)
name
:string,测试名称fn
:() => void,包含测试期望的函数fn
:number,默认:5s
,可选,测试的超时时间
afterAll 和 beforeAll
afterAll: 所有的测试用例执行完后
执行的方法,如果传入的回调函数返回值是 promise
或者 generator
,Jest
会等待 promise resolve
再继续执行。
beforeAll: 与afterAll
相反, 所有的测试用例执行之前
执行的方法
用法:afterAll(fn, timeout)
fn
:() => void,执行的函数timeout
:number,默认:5s
,可选,测试的超时时间
afterEach 和 beforeEach
afterEach: 也 afterAll
相比,afterEach
可以在每个测试完成后
都运行一遍
beforeEach:beforeEach
可以在每个测试完成之前
都运行一遍
用法:afterEach(fn, timeout)
fn
:() => void,执行的函数timeout
:number,默认:5s
,可选,测试的超时时间
示例
为了更好的理解,我们简单的写个示例,如:
const sum = require('./sum');
beforeAll(() => {
console.log('全局之前');
});
afterAll(() => {
console.log('全局之后');
});
beforeEach(() =>{
console.log('全局之前,每个都会执行');
});
afterEach(() => {
console.log('全局之后,每个都会执行');
});
describe('求和', () => {
beforeAll(() => {
console.log('求和:全局之前');
});
afterAll(() => {
console.log('求和:全局之后');
});
beforeEach(() => {
console.log('求和:全局之前,每个都会执行');
});
afterEach(() => {
console.log('求和:全局之后,每个都会执行');
});
it('求和:1 + 2 = 3', () => {
expect(sum(1, 2)).toEqual(3);
});
it('求和:2 + 5 = 7', () => {
expect(sum(2, 5)).toEqual(7);
});
})
执行结果:
断言
断言:Expect,我们在测试的时候常常需要判断,是否满足一个结果,比如像上面的expect(sum(1, 2)).toEqual(3)
就是为了验证,执行完sum函数
后,结果是否等于3
接下来我们来看看一些常见的断言,详情:Expect 断言
基本断言
- expect(value):要测试一个值进行断言的时候,首先使用expect对值进行包裹
- not: 用于测试相反的结果,也就是不等于
- toMatch(regexpOrString):用来检查字符串是否匹配,可以是
正则表达式
或者字符串
- toContain(item):用来判断item是否在一个数组中,也可以用于字符串的判断
- toBeNull(value):只匹配null
- toBeUndefined(value):只匹配undefined
- toBeGreaterThan(number): 大于
- toBeGreaterThanOrEqual(number):大于等于
- toBeLessThan(number):小于
- toBeLessThanOrEqual(number):小于等于
- toBeInstanceOf(class):判断是不是class的实例
- anything(value):匹配除了null和undefined以外的所有值
- toHaveBeenCalled():用来判断mock function是否被调用过
- toHaveBeenCalledTimes(number):用来判断mock function被调用的次数
- assertions(number):验证在一个测试用例中有number个断言被调用
toBeUndefined 和 toBeDefined
toBeUndefined():用于检查变量是否未定义,也就是说,只会匹配undefined
,比如说上面的sum
返回的结果明显不是undefined
,如果使用,则会报错
toBeDefined():与toBeUndefined相反,必须要匹配的有值的情况
如:
describe('求和', () => {
it('求和:1 + 2 = 3', () => {
expect(sum(1, 2)).toBeUndefined(); //error
});
it('求和:1 + 2 = 3', () => {
expect(sum(1, 2)).toBeDefined(); // ok
});
})
toBe 和 toEqual
toBe(value): 使用Object.is来进行比较,严格的比较
,需要注意的是,如果进行浮点数
的比较,要使用toBeCloseTo
,可以理解为 ===
toEqual(value): 用于对象的深度
,它一般比较的是对象的值,而并非对象本身
那么 toBe
和 toEqual
有什么区别呢?接下来一起看看:
对于基本类型:字符串、数字、布尔等, toBe
和 toEqual
并没有任何区别,而区别点主要是在对象上,如:
const data1 = {
name: '小杜杜',
age: 7,
};
const data2 = {
name: '小杜杜',
age: 7,
};
可以看到 data1
和 data2
都有相等的name
和age
,那么data1 === data2
吗?
很明显,它俩并不相等,原因是:它俩的指向不同,所以当我们使用toBe
的时候就会报错,这是个正常现象
但我们现在并不是比较地址,而是比较data1
和data2
的每一项是否相等,这时就会用到toEqual
,换言之,toEqual
会忽略两个对象的指向,只是会比较数值,固:
it('toEqual', () => {
expect(data1).toEqual(data2); //ok
});
it('toBe', () => {
expect(data1).toBe(data2); // error
});
toBeTruthy 和 toBeFalsy
当我们不在乎返回的是什么的时候,只在乎返回的值是否是真的时候,或是为假的时候可以使用 toBeTruthy
(为真) 和 toBeFalsy
(为假)
我们先看下面这个例子:
const data = (count) => {
if( typeof count === 'number'){
return count
}
return undefined
}
describe('测试 toBeTruthy 和 toBeFalsy', () => {
it('toBeTruthy', () => {
expect(data(9)).toBeTruthy(); // ok
});
it('toBeFalsy', () => {
expect(data('小杜杜')).toBeFalsy(); // ok
});
it('toBeTruthy === 0', () => {
expect(data(0)).toBeTruthy(); // error
});
});
当data
函数为数字的时候,就会返回数字
,为其他的类型将会统一返回undefined
那么对应的,如果是数字,则为真值,toBeTruthy()
会校验通过
如果不是数字,toBeFalsy()
会检验通过,toBeTruthy()
不会通过
但当数字为0,返回为0时,toBeTruthy()
也不会通过,这点要注意一下,这里的假值
是指:false
、0
、null
、""
、undefined
和NaN
六种类型
测试异步代码
先看看这段异步代码:
const fetchData = (flag = true) => {
return new Promise((resolve, reject) => {
if(flag){
resolve('小杜杜')
}else{
reject('error 你应该选择rejects')
}
})
}
我们该如何测试这段代码呢?这里提供三种方式
then方式
it('then方式', () => {
return fetchData().then(data => {
expect(data).toBe('小杜杜');
})
});
async await 方式
it('resolves 方式', () => {
expect(fetchData()).resolves.toBe('小杜杜');
});
resolves 和 rejects
resolves 和 rejects:主要用作promise
为resolve/reject
包裹的值,并且支持链式调用
如:
it('resolves 方式', () => {
expect(fetchData()).resolves.toBe('小杜杜');
});
it('rejects 方式', () => {
expect(fetchData(false)).rejects.toMatch('error');
});
覆盖率报告
终端上的结果
接下来我们看看 上述例子中,运行 npx jest
的结果
- %stmts:是
语句覆盖率(statement coverage)
,是不是每个语句都执行了 - %Branch:是
分支覆盖率(branch coverage)
,是不是每个if代码块都执行了 - %Funcs:是
函数覆盖率(function coverage
,是不是每个函数都调用了 - %Lines:是
行覆盖率(line coverage
,是不是每一行都执行了
报告文件
还记得最开始的coverageDirectory: "coverage"
吗?
实际上,终端展示的信息只是一部分,而生成的coverage
文件会生成很多覆盖率文件,包括:XML
、JSON
、HTML
等,当然这些文件的内容是相通的,只是为了不同工具的提取,来看看文件目录:
然后我们打开 sum.js.html
来看看结果
搭建环境
由于Ts
的迅速崛起,我们在项目中往往会使用Ts
,那么该如何进行配置呢?
(有对TS
不了解的小伙伴,可以看看这篇文章:一篇让你完全够用TS的指南)
首先,请大家牢记:Jest,本身不支持转译,所以我们需要通过其他的转译器来帮助我们
将第一个测试程序改造为ts
在这里我们用ts-jest
进行一下简单的改造,因为ts-jest
是官方推荐的
安装ts
执行命令:
npm i -D typescript
安装完后要初始化ts的配置
npx tsc --init
此时会生成一个tsconfig.json
的文件
安装ts-jest
npm i -D @types/jest //jest 类型
npm i -D ts-jest //ts-jest
这里需要注意下:有关jest的版本尽量保持一致(大版本下),否则容易出现兼容性问题
配置
在jest.config.js
文件加入:
module.exports = {
preset: 'ts-jest',
...
};
同时在tsconfig.json
中加入
{
"compilerOptions": {
"types": ["node", "jest"],
...
}
}
最后,我们在将sum.js
和sum.test.js
改为ts
文件即可
运行
creat-react-app 融合jest
上面我们说过jest
被集成在create-react-app
,接下来,我们看看怎么被集成
创建程序
我们直接使用create-react-app
创建一个ts
的程序,执行命令
npx create-react-app react-jest-test(文件名) --template typescript
cd react-jest-test
npm i -D react-test-renderer //添加快照
然后把sum.ts
和sum.test.ts
复制到src
下面
模块化
执行下看看:
这个问题,出现的原因是不支持import
这种导入
解决:引入babel-plugin-transform-es2015-modules-commonjs
npm install --save-dev babel-plugin-transform-es2015-modules-commonjs
然后在根目录下创建.babelrc
文件,加入:
{
"plugins": ["transform-es2015-modules-commonjs"]
}
解决ts问题
然后我们再运行一遍,看看:
这个问题还是上述说的转译问题,我们需要加入babel
和ts
的依赖
npm i -D babel-jest @babel/core @babel/preset-env
npm i -D @babel/preset-typescript
然后在跟目录下创建babel.config.js
文件,加入:
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current'
}
}
],
'@babel/preset-typescript'
]
};
特别注意,当你设置了
babel.config.js
后又可能还是运行不起来,这是因为babel
的设置有关,需要将扩展名跟改为cjs,也就是babel.config.cjs
此时就配置成功了
End
参考
总结
通过上面的介绍,我们发现Jest
实际上是可以单独作为一个模块拿出来的,我们通过一个简单的示例,全面介绍了有关Jest
的语法,方便我们后续的学习
当然 Jest
远远不止这些,希望各位小伙伴耐心的看完,并且尝试一下,在这里还是希望小伙伴能完全熟悉这些概念,方便后续的学习,下篇会将一个关于hooks
的单元测试(一文玩转React Hooks的单元测试),还希望各个小伙伴多多支持~
如果你喜欢,请不要吝惜你的赞,有更好的建议,欢迎评论区讨论,一起走向进阶之路~
转载自:https://juejin.cn/post/7145269660635070495