使用Jest进行单元测试

Jest是Facebook开发的一款JavaScript测试框架。我之前做单元测试一直使用的是Mocha测试框架与Chai断言库。之前在好几个地方看到了Jest测试框架,心生好奇学习了一下。

Jest框架

Jest框架标榜"Delightful JavaScript Testing",提供零配置、快速反馈与快照测试功能。

零配置方面,Jest本身就是测试框架、断言库、Mock框架与测试覆盖率检查工具的结合体。而Mocha仅仅是测试框架。要在Mocha里配置出与Jest相同的功能的话需要自行安装像Chai断言库、Sinon.js框架与istanbul测试覆盖率检查。所以说Jest可以说是开箱即用的。Jest在零配置的情况下就可以自动检测以*.spec.js*.test.js模式命名的文件以及__tests__文件夹下的文件,把它们作为测试用例。

快速反馈方面,Jest的监视模式可以只运行与被改变的文件相关的测试用例,不需要把所有的测试用例重跑一遍。开发者可以快速知道改变代码的结果。

快照测试方面,Jest可以将某次函数的输出保存为一个快照,之后测试就把实际输出与快照中的输出进行对比,保证函数行为的一致性。

Jest可以分为测试框架、断言以及Mock三个部分。

测试框架

关注Jest的测试框架部分,主要是关注在Jest中测试用例如何写、如何组织的问题。Jest中,测试用例的写法与Mocha非常相似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const binaryStringToNumber = binString => {
if (!/^[01]+$/.test(binString)) {
throw new CustomError('Not a binary number.');
}

return parseInt(binString, 2);
};

describe('binaryStringToNumber', () => {
describe('given an invalid binary string', () => {
test('composed of non-numbers throws CustomError', () => {
expect(() => binaryStringToNumber('abc')).toThrowError(CustomError);
});

test('with extra whitespace throws CustomError', () => {
expect(() => binaryStringToNumber(' 100')).toThrowError(CustomError);
});
});

describe('given a valid binary string', () => {
test('returns the correct number', () => {
expect(binaryStringToNumber('100')).toBe(4);
});
});
});

在Jest中,不必将test嵌入describe中。如果能使测试更简单的话,test也可以直接写在顶层。

skip,only,each修饰符

describetest可以连接skiponlyeach修饰符。如describe.skip('something', testFunction),会在测试时跳过这一个describeonly会使测试只运行指定的测试用例,这在某个测试用例出错Debug时非常好用。each修饰符可以执行多次参数不同的测试,它接受一个数组table和一个测试函数,table里的元素会作为参数传入测试函数。具体语法可以参见文档

beforeAll,afterAll,beforeEach,afterAll钩子函数

Jest也支持在执行测试用例之前以及之后执行一些代码来做一些工作,像在测试前设置好测试数据、在测试后清理测试数据。这些工作可以作为beforeAllafterAllbeforeEachafterAll的回调函数。

断言

Jest支持expect式的断言,像expect(1).toBe(1),其中toBe就是断言部分。Jest支持很丰富的断言。

相等断言

断言两个基本类型的值相等使用expect(val1).toBe(val2)。注意toBe断言使用Object.is()判断相等。它与==以及===都有不同。相对===Object.is()-0, +0NaN的判断上有所不同。

如果要断言数组或者Object相等,使用toEqual断言。它会递归地判断每个属性/元素是否是相等的。

数字大小断言

大小关系断言有toBeGreaterThantoBeGreaterThanOrEqualtoBeLessThantoBeLessThanOrEqual。名字很直白,不解释。

对于浮点数,不能使用toBe或者toEqual进行相等断言。Jest提供了toBeCloseTo断言,可以在忽略一定误差的情况下,断言浮点数相等。

1
2
3
4
test('float equality', function () {
expect(0.2 + 0.1).toBeCloseTo(0.3) //pass
})

真值断言(Truthiness)

Jest提供toBeTruthytoBeFalsy断言被测试函数的返回结果在if中是真还是假。像:

1
2
3
test('nonempty string should be true', function () {
expect('it is true').toBeTruthy() //pass
})

Jest也提供toBeNulltoBeUndefinedtoBeDefined针对性的断言nullundefined的情况。

字符串相关

Jest提供toMatch断言被测试的字符串是否匹配给定正则表达式。

1
2
3
test('but there is a "stop" in Christoph', () => {
expect('Christoph').toMatch(/stop/) // pass
});

数组

要断言数组中包含某个子项可以使用toContain断言。

1
2
3
4
5
6
7
8
9
10
11
const shoppingList = [
'diapers',
'kleenex',
'trash bags',
'paper towels',
'beer',
];

test('购物清单(shopping list)里面有啤酒(beer)', () => {
expect(shoppingList).toContain('beer');
});

抛出异常

要断言对函数的某些操作会抛出异常可以使用toThrow断言。

1
2
3
4
5
test('throws on octopus', () => {
expect(() => {
drinkFlavor('octopus');
}).toThrow();
});

not修饰符

not修饰符可以把所有的断言反向,像expect(1).not.toBe(2)

Jest提供的断言不止上面提到那么多。常用到的还有像断言长度的toHaveLength,断言对象有某个属性以及属性的值的toHaveProperty。更多断言的可以参见Expect文档

Mock

如果目前正在开发的模块存在依赖,比如某个函数需要一个随机产生的结果。假如依赖模块没有开发完成或者结果不可预知,我们是无法测试我们的模块的。为了解开开发模块与依赖模块在测试上的耦合,我们可以使用Mock功能去模拟被依赖模块的行为,比如规定某个函数调用时返回某些值。此时测试我们开发的模块就与被依赖模块的实际逻辑无关,实现了测试上的解耦。

这一部分比较复杂,我会在另外一篇文章来介绍这一部分。

踩坑部分

这里是我将一个用Vue编写的项目从Mocha转到Jest时踩的一些坑,算是笔记吧。

Automatic mock不支持引用路径带有alias

@是Vue项目中src目录的alias(通过jest.config.jsmoduleNameMapper指定)。如果尝试使用alias指定Automatic mock的模块:

1
2
import Module from '@/src/libs/module'
jest.mock('@src/libs/module')

上述会报错。目前的解决方案是jest issue #1290提到的用Manual mock。这个方式有用,但是过了那么多年可能有更好的做法。

vscode-jest与Vue CLI 3脚手架生成的项目整合

vscode-jest是一款VS Code的插件,它可以在文件保存时使用Jest的监视模式进行单元测试。但是对于Vue CLI 3生成的项目,vscode-js原生的配置无法使用。为了使vscode-jest有用我们需要做一些额外的工作。下面假设已经通过vue add @vue/unit-jest安装了@vue/cli-plugin-unit-jest

  1. 在Workspace设置中将jest.pathToJest设置为npm run test:unit(使用npm)或者yarn test:unit(使用yarn)。
  2. jest.config.js里添加两行
1
2
process.env.VUE_CLI_BABEL_TARGET_NODE = true
process.env.VUE_CLI_BABEL_TRANSPILE_MODULES = true
  1. 执行npx jest --clearCache

vue-cli issue #1879上有关于这个问题的讨论。如果没用可以尝试其他人提出的方法。