Umi Jest
Mock History
在umi
中,history
是命令行导航或说是路由的方式。我们可以通过像history.push("/")
的方式进行路由跳转。当要给history
添加单元测试的使用,我们可以使用下面的方式:
import { history } from 'umi';
jest.mock("umi",()=>{
return {
history: {
push: jest.fn()
}
}
})
describe('history', function () {
it('should be /', function () {
expect(history.push).toBeCalledWith("/");
});
});
这里的关键点在于:
- 在当前测试中添加
history
的显示引入 - 使用
toBeCalledWith
来判定history
的push方法接收到的是我们预期的参数。
使用命令行
在使用jest
的时候,我们常常会使用到命令行的方式运行测试,这可以给予我们一定的自由度来控制测试的运行。
方式
在命令行中使用jest
有几种方式,其一就是直接使用已安装的本地jest
,使用方式如下:
./node_modules/bin/jest --可选参数
其二是使用全局安装的jest
脚本,使用方法如下:
jest --可选参数
其三是使用pnpm
,npm
和yarn
这样的包管理器运行。这里以pnpm
为例。在使用这种方式的时候,我们通常需要在package.json
中添加一个脚本命令,如:
{
"script": {
"test": "jest"
}
}
当我们使用这样的方式来运行jest
的时候,命令行要用以下的方式:
pnpm test -- --参数
具体参数
--json
以JSON
的形式输出测试结果
--lastCommit
运行上一次commit
中的所有测试
--onlyChanged
只运行当前仓库中修改文件的测试。别名:-o
注意:这个命令只有在项目启用了
git
或hg
仓库时才有用
--silent
静默运行测试,不在控制台打印测试信息
--bail[=]
当失败的测试套件达到指定个数的时候,立即退出。默认时为1
。别名:-b
。
--changedSince
只运行提供的commit hash
或分支
后的修改。
--coverage[=]
指定是否要测试代码覆盖率。别名:--collectCoverage
--coverageDirector=
指定代码覆盖率输出的目录
--coverageProvider=
指定使用哪个选项来检测代码的覆盖率。可选项有babel
和v8
。默认为babel
。
--debug
使用调试样式运行Jest
--env=
指定Jest
运行的测试环境。可以指定任意的文件或node模块。如jsdom
,node
或path/to/my-envoriment.js
.
--expand
用于显示测试失败时详细的区别,而不是使用部分区别。别名:-e
--init
以会话的形式初始化一个jest.config.js
的配置文件。
--listTests
列表检测到的所有测试
--noStackTrace
在输出中,忽略堆栈追踪
--passWithNoTests
当Jest
检测到一个测试文件内没有任何测试时,将其标记为通过。
默认情况下,当一个测试文件内不包含测试时,会默认测试失败。
--showConfig
显示Jest的配置内容
--findRelatedTests
查找并运行以空格分隔的一系列和源文件有关的测试。
--testNamePattern=
运行测试名符合给定的正则表达式的测试。别名:-t
。
配置文件
代码覆盖率
当在测试中需要输出代码覆盖率的时候,我们需要进行一定的配置,下面是一个配置示例:
module.exports = {
collectCoverage: true,
coverageThreshold: {
global: {
branches: 95,
functions: 95,
lines: 95,
statements: 95
},
'./src/reducers/**/*.ts': {
statements: 90
},
'./src/utils/**/*.ts': {
functions: -5
}
},
coverageDirectory: 'reports',
collectCoverageFrom:["src/**/*.ts","!**/node_modules/**"],
coveragePathIgnorePatterns: ["<rootDir>/dist/","<rootDir>/node_modules/"],
coverageProvider: "babel",
coverageReporters: ["clover","json",'lcov',['text',{skipFull: true}]]
}
在这个配置示例中,我们对其中的各个字段分别进行一下讲解:
collectCoverage
字段为一个boolean值,指定其为true
时,才会触发代码覆盖率的收集操作coverageThreshold
字段是对代码覆盖率的统计指标的定义。其可以定义一个全局的代码覆盖率的指标,用global
表示,其中要求branches(条件分支)
的覆盖率最少达到95%
,functions(函数)
的代码覆盖率最少也要达到95%
,lines代码行
的代码覆盖率也要达到95%
,statements(语句)
的代码覆盖率也要达到95%
。此外,我们还可以单独指定某个匹配模式下对应的代码覆盖率,比如示例中./src/reducers/**/*.ts
模式下statements
的代码覆盖率达到90%
即可。也可以指定一个负值,用于表示没有覆盖的测试的最大数量,比如示例中./src/utils/**/*.ts
模式下,functions
没有测试的最大数量为5。一旦测试的代码覆盖率达不到我们设置的指标,那么测试就会失败。coverageDirectory
用于指定代码覆盖率输出报告的文件目录,如果没有设置这个属性,那么报告会输出到项目目录的coverage
目录中。collectCoverageFrom
用于指定从什么地方收集要测试的代码。coveragePathIgnorePatterns
用于指定测试时,要忽略哪些目录下的代码。coverageProvider
用于指定输出报告的提供者,可选的有babel
和v8
,默认使用babel
。coverageReporters
用于指定输出报告的格式。默认值为["clover", "json", "lcov", "text"]
。
除了在示例中提及到的几个有关代码覆盖率的选项外,还有一个forceCoverageMatch
选项,其主要用于从上面的配置中已忽略的部分再添加进测试中。
报告格式
全局对象
为了方便在测试中使用,Jest
自动在测试文件中注入了全局对象describe
,test
和expect
。通过自动注入的方式使得我们在测试文件中不再需要显示的重复import {describe, expect, text} from '@jest/globals'
引用了。
Jest
的全局对于可以分为以下几类:
- 钩子类
- afterAll
- beforeAll
- afterEach
- beforeEach
- describe类
- test类
- expect类
因为expect
类比较重要,所以单独记录。
全局对象 test
在Jest
中,全局对象test
有一个别名it
,在也就是说,在Jest
中能使用test
的地方也可以使用it
来替代。
test
test
对象最简单的使用如下:
test('testName', () => {
expect(1 + 1).toBe(2)
})
test
函数接收三个参数。
- 第一个参数为测试方法的名称,通过这个名称可以在测试失败的时候,更加精确的定位是哪个测试失败了。
- 第二个参数是一个匿名函数。在这个函数中应该包含本测试的准备工作和一个或多个断言函数。(推荐只在一个
test
方法中包含一个断言函数,虽然Jest
允许在test
中包含多个断言函数)。 - 第三个参数(可选)是测试运行的超时时间,如果函数运行超时,则自动失败。默认是5s。
如果test
第二个参数的函数返回一个Promise
,那么,在Promise
状态改变后,断言才会生产。比如asyncBe
是返回一个Promise
。那么可以像这样测试:
test("test the asyncBe", () => {
return asyncBe(1).then(v => {
expect(v).toBe(1)
})
})
关于异步函数的测试,请参考异步函数的测试
test.only
test.only
可以使Jest
只运行一个测试块中当前的测试。这在调试测试的时候会非常有用。它可以让你只运行当前的测试,而忽略其它测试,以此来提升测试速度和减少反馈时间。test.only
有一个别名:fit
。
示例如下:
test.only('it is raining', () => {
expect(inchedOfRain()).toBeGreaterThan(0);
});
test('it is not snow', () => {
expect(inchedOfSnow()).toBe(0);
});
在上面的示例中,当运行测试的时候,只会运行it is raining测试,因为其标记了only
。
注意:请不要把
test.only
提交到仓库中,它应该只是你调试测试的一个常用工具。
test.todo
test.todo
用于指定计划添加测试的时候。其只接收一个参数,即测试的名称。使用test.todo
标记的测试会在最后的汇总的时候高亮,所以可以方便的知道还有哪些计划的测试需要添加。
注意:
test.todo
如果包含了函数会抛出错误。如果你已经有了一个实现测试的测试,但不想运行它。请使用todo.skip
。
test.skip
当正在维护一个很大的代码的时候,如果一个测试因为某些原因临时遭到了破坏。但又不想删除它。只想临时跳过这个测试。这个时候就可以使用test.skip
。别名:xit
,xtest
。
示例如下:
test('it is raining', () => {
expect(inchesOfRain()).toBeGreaterThan(0)
});
test.skip('it is not snowing', () => {
expect(inchesOfSnow()).toBe(0)
})
上面的代码只会运行it is raining。因为其使用了test.skip
。
test.each
当你需要使用不同的数据来运行测试的时候可以使用test.each
。先来看示例:
test.each([
[1, 1, 2],
[1, 2, 3],
[2, 1, 3]
])('.add(%i, %i)', (a, b, expected) => {
expect(a + b).toBe(expected);
})
test.each([
{a: 1, b: 1, expected: 2},
{a: 1, b: 2, expected: 3},
{a: 2, b: 1, expected: 3}
])('.add($a, $b)', ({a, b, expected}) => {
expect(a + b).toBe(expected);
})
通过示例我们可以发现test.each
的第一个参数是一个二维数组。其返回值接收3个参数。
- 第一个参数:测试名。
- 其名称是可以通过
printf
的参数%p
美化格式%s
字符串%d
数字%i
整数%f
浮点数%j
JSON%o
对象%#
测试的索引%%
%
- 或是对象解构的参数,如
$a
,$b
- 其名称是可以通过
- 第二个参数,测试函数。
- 第三个参数(可选),超时时间,默认为5s。
test.concurrent
test.concurrent
目前是一个实验性的功能。其作用是显式指定其测试开发运行。其包含的函数和test
基本一致。只是第二个参数接收的是一个包含有断言的
异步函数.
示例:
test.concurrent('addition of 2 numbers', async () => {
expect(5 + 3).toBe(8);
});
test.concurrent('subtracting 2 numbers', async () => {
expect(5 - 3).toBe(2);
})
注意:当在测试中使用了
test.concurrent
时,最配置maxConcurrency
选项,用来防止Jest
同时运行太多的测试。
钩子类全局对象
describe全局对象
Expect断言
在Jest
中有40多种断言匹配器,还有三种修饰符。这40多种断言匹配器可以分为多个种类。
基本类型断言
基本类型断言匹配器有以下几个:
toBe
toBe匹配器在Jest
中的主要作用是用于断言基本类型的。其使用Object.is
来判断断言结果,而不是使用的全等运算符(===)。
使用toBe 判断对象是否相等的时候,其判断是的两个对象是不是使用的同一个引用,而不是比较其字面值是否相等,如果要比较再个对象的字面量是否相等,请使用toEqual。
请不要使用toBe来比较两个浮点数是否相等,因为实现的原因,在JavaScript
和其它大多数的语言中,0.1+0.2并不完全等于0.3。(
因为精度丢失)。如果要比较两个浮点数,请使用toBeCloseTo。
注意:
Object.is
和全等运行符在+0
和-0
,Number.NaN
和NaN
的运算上是不一样的。其他时候的运算结果是一致的。区别如图:
toEqual
toEqual
方法在处理基础类型上和toBe
的行为是一致的。但在处理复杂类型(对象,数组,Set,Map等)时,toBe
是直接比较左右两边是否指向同一个引用。而toEqual
则会对这些复杂类型进行递归调用toEqual
方法,最终判断是字面量是否相等。如:
test("toEqual example", () => {
const a = {
id: 1,
lang: 'JS'
};
const b = {
id: 1,
lang: 'JS'
};
expect(a).toEqual(b);
expect(a).not.toBe(b);
})
toBeDefined
断言一个变量或一个函数的返回値是已经定义的。比如:
test('assert toBeDefined', () => {
expect(1).toBeDefined()
})
注意,如果要断言一个变量或返回值是未定义的,相比下
not.toBeDefined
,toBeUndefined是更好的选择。
toBeTruthy
断言一个值是真值,而不仅仅是true
。比如:
test('assert toBeTruthy', () => {
expect(1).toBeTruthy();
})
注意:在
JavaScript
中,只有false
,''
,0
,null
,undefined
和NaN
是假值,其余的都是真值。
如果要判断一个值为假值,相比下
not.toBeTruthy
,toBeFalsy是更好的选择。
toBeFalsy
断言一个值是假值。而不仅仅是false
。比如:
test(`assert toBeFalsy`, () => {
expect(0).toBeFalsy();
})
注意:在
JavaScript
中,只有false
,''
,0
,null
,undefined
和NaN
是假值,其余的都是真值。
如果要判断一个值为假值,相比下
not.toBeFalsy
,toBeTruthy是更好的选择。
toBeNaN
判断一个值是NaN
。
test('assert toBeNaN', () => {
expect(NaN).toBeNaN();
expect(1).not.toBeNaN();
})
注意,在
JavaScript
中目前有两种方法来判断一个值是不是NaN
- Number.isNaN
NaN===NaN
返回值为false
,即NaN
是JavaScript中唯一一个不等于自身的。
toBeNull
判断一个值是null
。toBeNull
在逻辑上等价于toBe(null)
。但当断言失败的时候,toBeNull
的提示信息更加友好。
test('assert toBeNull', ()=>{
expect(null).toBeNull();
})
toBeCloseTo
toBeCloseTo 用于判定两个给定的浮点数在指定的精度下是否相等。之所以需要指定精度,是因为在计算机的底层使用的是二进制表示的十进制的数字。并不是每一个十进制小数都可以使用二进制精确的表示,这就会丢失精度。所以才有了在指定精度下判断相等的方法。
toBeCloseTo的签名为toBeCloseTo(number, numDigits?)
:
numDigits
为精度,可选,其默认值为2.其表示Math.abs(expected - received) < 0.005
。其中,0.005
和2
的关系是:(10**-2)/2
。
示例代码如下:
test('assert 0.1+0.2 and 0.3', () => {
expect(0.1 + 0.2).toBeCloseTo(0.3, 5)
})
toBeUndefined
用于断言一个变量或返回值是未定义的。比如:
test('assert toBeUndefined', () => {
let a;
expect(a).toBeUndefined();
})
注意,如果要断言一个变量或返回值是已定义的,相比下
.not.toBeUndefined
,toBeDefined()是更好的选择。
toBeGreaterThan
使用received>expected
来进行断言。
比如:
expect('assert toBeGreaterThan', ()=>{
expect(12).toBeGreaterThan(10)
})
toBeGreaterThanOrEqual
使用received>=expected
进行断言。
比如:
expect('assert toBeGreaterThanOrEqual', () => {
expect(12).toBeGreaterThanOrEqual(12);
})
toBeLessThan
使用received<expected
进行断言。
比如:
expect('assert toBeLessThan', ()=>{
expect(12).toBeLessThan(15)
})
toBeLessThanOrEqual
使用recieved<=expected
进行断言。
比如:
expect('assert toBeLessThanOrEqual', ()=>{
expect(12).toBeLessThanOrEqual(12)
})
数组类断言
Jest
中和数组相关的断言匹配器有下面一些:
toContain
toContain
用于判断数组中是否包含指定的元素。其使用===
来判断数组中的项目和给定内容的比较。比如:
const array = [1, 2, 3];
test('assert toContain', () => {
expect(array).toContain(2);
})
toContain
不仅仅用于数组,还可以用于其他可迭代类型,比如字符串,集合,node集合和HTML集合等。
注意,因为
toContain
中使用的是===
。所以,如果数组中的项目是对象,那么比较的是对象的引用是否是同一个,而不是比较字面量。
toContainEqual
toContainEqual
同样是用于判断数组中是否包含指定的元素。其和toContain
的区别在于,toContain
对于复杂类型的比较是比较其引用。而toContainEqual
是比较其字面量。就和toBe
和toEqual
一样。
比如:
const array = [{id: 1}, {id: 2}];
test(`assert toContainEqual`, () => {
expect(array).toContainEqual({id: 1})
})
toHaveLength
toHaveLength
用于检查一个数组中元素的个数或字符串的长度。比如:
test('assert toHaveLength', () => {
expect([1, 2, 3]).toHaveLength(3);
})
注意,
toHaveLength
不仅仅可以用于数组和字符串,它可以用于任何有length
属性的对象。用于判断其length
是否等于指定的数值。
expect.arrayContaining
expect.arrayContaining(array)
可以用于判断一个数组是不是另一个数组的子数组。其主要需要配合toEqual
或toBeCalledWith
使用。还可以配合objectContaining
或toMatchObject
使用。
it('assert arrayContaining', function () {
const expected = [1, 2, 3];
expect([1, 2, 3, 4]).toEqual(expect.arrayContaining(expected));
});
测试异步函数
在Jest
中测试异步函数主要有4种方法:
- 在
test
中返回的Promise
中断言 - 使用
async
和await
- 在回调函数中使用
done
函数 - 使用
resolves
和rejects
返回Promise
如果要测试的函数返回的是一个Promise
,那么我们就可以让test
返回Promise
,在Promise
中使用断言。比如:
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
})
})
这样,我们就可以测试这个Promise
函数了。
注意:我们一定要返回
Promise
,让Jest
知道这是一个异步函数,在Promise
的resolve
后进行断言,否则,Jest会提前断言。这个时候,Promise
的状态还没有改变,得不到我们想要的结果。如果Promise
没有resolve
,而是jest
了,那么测试会直接失败。
async
/await
在测试异步代码的时候,我们可以直接使用async
和await
。就像下面一样:
test('the data is peanut butter', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
})
test('the fetch fails with an error', async () => {
expect.assertions(1);
try {
await fetchData();
} catch (e) {
expect(e).toMatch('error');
}
})
在使用async
/await
测试异步代码的时候,我们要给test
函数添加async
标记,在异步代码前使用await
。
当使用async
/await
测试异步代码失败的时候,记得一定要使用assertions
来断言测试有过断言,否则测试不能通过。
使用回调函数
如果我们要测试的异步代码是在回调函数中,而不是在Promise
里,那么我们就需要使用test
函数中单参数done
来解决问题了。比如:
test('the data is peanut butter', done => {
function callback(error, data) {
if (error) {
done(error);
return;
}
try {
expect(data).toBe('peanut butter');
done();
} catch (error) {
done(error);
}
}
fetchData(callback);
})
在这个例子中,如果done()
函数从来没有执行过,那么测试会超时失败。
如果expect
执行失败,那么其将抛出一个错误,其后的done
就不会执行。如果我们想要知道具体为什么会失败,那么我们就要在测试中捕获这个错误。所以需要把expect
放入try
中,然后在catch
中捕获错误,使用done
来接收error
的值。
使用resolves
和rejects
我们也可以让test
中的函数返回一个resolves
或rejects
来测试异步代码。比如:
test('the data is peanut butter', () => {
return expect(fetchData()).resolves().toBe('peanut butter');
})
直接通过resolves
来声明我们期望Promise
的状态会变为resolve
状态,并返回peanut butter
,否则测试失败(无论Promise
是变为reject
还是resolve
后返回的不是peanut butter
)。
同理,通过rejects
,我们期望Promise
的状态会变为rejects
,并返回一个指定的错误。如:
test('the fetch fails with an error', () => {
return expect(fetchData()).rejects.toMatch('error');
})
测试RxJs中Promise
的行为
因为RxJs
的实现机制和Promise
微任务的问题,在RxJs
中不能使用弹珠测试,那么我们就要使用Jest
中测试异步代码中的方法来解决问题了。比如:
// Some RxJS code that also consumes a Promise, so TestScheduler won't be able
// to correctly virtualize and the test will always be really asynchronous.
const myAsyncCode = () => from(Promise.resolve('something'));
it('has async code', (done) => {
myAsyncCode().subscribe((d) => {
assertEqual(d, 'something');
done();
});
});
Mock
在Jest
测试中,Mock
的重要性丝毫不亚于expect
断言的重要性。因为在很多的场合,没有Mock
,测试会很难执行甚至于不能执行。
常用的Mock场景:
location的mock
在开发浏览器应用的时候,我们会不可避免的使用到window.location
对象(后面直接称为location
对象)
。当我们的方法使用到location
对象的时候,要对这个方法进行单元测试,那我们就需要使用方法来mock
这个location
对象。
下面是我们要mocklocation
的方法和步骤:
安装依赖
我们需要知道的是,我们使用location
的场景是在浏览器端,所以我们的测试环境也应该是和浏览器相关的。这里我们使用jsdom
环境。首先,我们需要安装依赖jest-environment-jsdom
:
pnpm add -D jest-environment-jsdom
设置测试环境
设置测试环境的方法有两种,一种是在配置文件中指定全部测试的测试环境,一种是测试文件中单独指定测试文件自身的测试环境。我们这里使用第二种方法:
/**
* @jest-environment jsdom
*/
设置mock方法
为了使用方法,我们可以提取一个公共的mock方法,如下:
function stubLocation(location: Record<string, any>) {
jest.spyOn(window, "location", "get").mockReturnValue({
...window.location,
...location,
});
}
之后,我们就可以直接在测试中调用stubLocation
方法为location
进行mock
了。
总结
我们通过安装依赖,设置测试环境和添加公共的mock方法,实现了对location
的包装。具体示例如下:
pnpm add -D jest-environment-jsdom
/**
* @jest-environment jsdom
*/
test("test location", () => {
stubLocation({url: "https://www.exmaple.com"});
expect(localtion.url).toBe("https://www.example.com")
})
function stubLocation(location: Record<string, any>) {
jest.spyOn(window, "location", "get").mockReturnValue({
...window.location,
...location,
});
}