Jest是一个用于测试JavaScript代码的库。它是一个由Facebook维护的开源项目,特别适用于React代码的测试,尽管不仅限于此:它可以测试任何JavaScript代码。Jest非常快速和易于使用。
Jest简介
Jest是一个用于测试JavaScript代码的库。
它是一个由Facebook维护的开源项目,特别适用于React代码的测试,尽管不仅限于此:它可以测试任何JavaScript代码。它的优势是:
- 快速
- 可以执行快照测试
- 它具有某些约定,并且提供了一切必要的东西,无需进行选择
Jest是一个和Mocha非常相似的工具,尽管它们之间存在一些差异:
- Mocha更灵活,而Jest有一套规则
- Mocha需要更多的配置,而Jest通常可以直接运行,因为它有一套规则
- Mocha更早并且更稳定,拥有更多的工具集成
在我看来,Jest最重要的特点是它是一个开箱即用的解决方案,无需与其他测试库进行交互即可完成工作。
安装
在create-react-app
中,默认安装了Jest,所以如果你使用它,你不需要安装Jest。
使用Yarn可以在其他项目中安装Jest:
yarn add --dev jest
或者使用npm:
npm install --save-dev jest
请注意,我们都将Jest放在了package.json
文件的devDependencies
部分中,这样它将仅在开发环境中安装,而不会安装在生产环境中。
将以下代码添加到package.json
文件的scripts部分中:
{
"scripts": {
"test": "jest"
}
}
这样我们就可以使用yarn test
或npm run test
来运行测试。
或者,您还可以全局安装Jest:
yarn global add jest
并使用jest
命令行工具运行所有测试。
创建第一个Jest测试
使用create-react-app
创建的项目已经默认安装并预配置了Jest,但是将Jest添加到任何项目中都非常简单,只需键入以下命令即可:
yarn add --dev jest
将以下代码添加到您的package.json
文件中:
{
"scripts": {
"test": "jest"
}
}
然后通过在shell中执行yarn test
来运行测试。
现在,您还没有任何测试,因此不会执行任何操作:
让我们创建第一个测试。打开一个math.js
文件并键入我们将稍后测试的一些函数:
const sum = (a, b) => a + b
const mul = (a, b) => a \* b
const sub = (a, b) => a - b
const div = (a, b) => a / b
module.exports = { sum, mul, sub, div }
现在,在相同的文件夹中创建一个math.test.js
文件,并在其中使用Jest来测试在math.js
中定义的函数:
const { sum, mul, sub, div } = require('./math')
test('Adding 1 + 1 equals 2', () => {
expect(sum(1, 1)).toBe(2)
})
test('Multiplying 1 \* 1 equals 1', () => {
expect(mul(1, 1)).toBe(1)
})
test('Subtracting 1 - 1 equals 0', () => {
expect(sub(1, 1)).toBe(0)
})
test('Dividing 1 / 1 equals 1', () => {
expect(div(1, 1)).toBe(1)
})
运行yarn test
将在所有找到的测试文件上运行Jest,并返回结果:
使用VS Code运行Jest
VS Code是JavaScript开发的一个很好的编辑器。Jest扩展为我们的测试提供了一流的集成。
安装后,它会自动检测您是否已经在devDependencies中安装了Jest,并运行测试。您也可以通过选择Jest: Start Runner命令来手动执行测试。它将运行测试并保持在监视模式下,以在更改了具有测试(或测试文件)的文件之一时重新运行它们:
匹配器
在前一篇文章中,我只使用了toBe()
作为唯一的匹配器:
test('Adding 1 + 1 equals 2', () => {
expect(sum(1, 1)).toBe(2)
})
匹配器是一种方法,用于测试值。
最常用的匹配器是将expect()
的结果与传递的值进行比较:
toBe
使用===
进行严格的相等比较toEqual
比较两个变量的值。如果它是对象或数组,则检查所有属性或元素的等式toBeNull
在传递null值时为truetoBeDefined
在传递一个已定义的值时为true(与上述相反)toBeUndefined
在传递一个未定义的值时为truetoBeCloseTo
用于比较浮点值,避免四舍五入误差toBeTruthy
如果值被视为真(例如if
语句)则为truetoBeFalsy
如果值被视为假(例如if
语句)则为truetoBeGreaterThan
如果expect()的结果大于参数,则为truetoBeGreaterThanOrEqual
如果expect()的结果等于参数或大于参数,则为truetoBeLessThan
如果expect()的结果小于参数,则为truetoBeLessThanOrEqual
如果expect()的结果等于参数或小于参数,则为truetoMatch
用于使用正则表达式模式匹配字符串toContain
用于数组,在其元素集合中包含参数时为truetoHaveLength(number)
:检查数组的长度toHaveProperty(key, value)
:检查对象是否具有属性,并可选地检查其值toThrow
检查您传递的函数是否抛出了异常(一般或特定的异常)toBeInstanceOf()
:检查对象是否为类的实例
所有这些匹配器都可以使用语句中的.not.
进行否定,例如:
test('Adding 1 + 1 does not equal 3', () => {
expect(sum(1, 1)).not.toBe(3)
})
要与Promise一起使用,可以使用.resolves
和.rejects
:
expect(Promise.resolve('lemon')).resolves.toBe('lemon')
expect(Promise.reject(new Error('octopus'))).rejects.toThrow('octopus')
设置
在运行测试之前,您需要进行一些初始化工作。
使用beforeAll()
函数可以在运行所有测试之前执行一次操作:
beforeAll(() => {
//一些操作
})
使用beforeEach()
可以在每个测试运行之前执行一些操作:
beforeEach(() => {
//一些操作
})
清除
与设置类似,您还可以在每个测试运行之后执行一些操作:
afterEach(() => {
//一些操作
})
在所有测试结束后执行操作:
afterAll(() => {
//一些操作
})
使用describe()分组测试
您可以在一个单独的文件中创建测试组,以隔离设置和清除操作:
describe('第一组', () => {
beforeEach(() => {
//一些操作
})
afterAll(() => {
//一些操作
})
test(/\*...\*/)
test(/\*...\*/)
})
describe('第二组', () => {
beforeEach(() => {
//一些操作
})
beforeAll(() => {
//一些操作
})
test(/\*...\*/)
test(/\*...\*/)
})
测试异步代码
在现代JavaScript中,异步代码有两种形式:回调函数和Promises。在Promises的基础上,我们可以使用async/await。
回调函数
您不能在回调函数中进行测试,因为Jest不会执行它 - 在调用回调函数之前,测试文件的执行就结束了。要解决这个问题,请将一个参数传递给测试函数,并方便地命名为done
。在结束该测试之前,Jest将等待您调用done()
:
//uppercase.js
function uppercase(str, callback) {
callback(str.toUpperCase())
}
module.exports = uppercase
//uppercase.test.js
const uppercase = require('./src/uppercase')
test(`将'test'转换为'TEST'`, (done) => {
uppercase('test', (str) => {
expect(str).toBe('TEST')
done()
}
})
Promises
对于返回Promises的函数,我们需要从测试中返回一个Promise:
//uppercase.js
const uppercase = str => {
return new Promise((resolve, reject) => {
if (!str) {
reject('空字符串')
return
}
resolve(str.toUpperCase())
})
}
module.exports = uppercase
//uppercase.test.js
const uppercase = require('./uppercase')
test(`将'test'转换为'TEST'`, () => {
return uppercase('test').then(str => {
expect(str).toBe('TEST')
})
})
被拒绝的Promises则可以使用.catch()
进行测试:
//uppercase.js
const uppercase = str => {
return new Promise((resolve, reject) => {
if (!str) {
reject('Empty string')
return
}
resolve(str.toUpperCase())
})
}
module.exports = uppercase
//uppercase.test.js
const uppercase = require('./uppercase')
test(`将'test'转换为'TEST'`, () => {
return uppercase('').catch(e => {
expect(e).toMatch('Empty string')
})
})
Async/await
对于返回Promises的函数,我们还可以使用async/await,从而使语法非常简洁明了:
//uppercase.test.js
const uppercase = require('./uppercase')
test(`将'test'转换为'TEST'`, async () => {
const str = await uppercase('test')
expect(str).toBe('TEST')
})
模拟
在测试中,模拟允许您测试依赖于:
- 数据库
- 网络请求
- 访问文件
- 任何外部系统
这样做的好处是:
- 您的测试运行更快,在开发过程中快速获得反馈
- 您的测试与网络条件或数据库状态无关
- 您的测试不会对任何数据存储造成污染,因为它们不会触及数据库
- 对测试所做的任何更改都不会影响后续测试的状态,并且重新运行测试套件应该从一个已知且可重现的起始点开始
- 您不必担心API调用和网络请求的速率限制
当您想要避免副作用(例如写入数据库)或跳过代码中的耗时部分(例如网络访问)时,模拟是有用的,而且还可以避免多次运行测试的影响(例如,想象一个发送电子邮件或调用限速API的函数)。
更重要的是,如果您正在编写一个单元测试,则应该单独测试函数的功能,而不是测试它接触到的所有东西。
使用模拟,您可以检查模块函数是否已被调用以及使用了哪些参数,方法如下:
expect().toHaveBeenCalled()
:检查被追踪函数是否已被调用expect().toHaveBeenCalledTimes()
:计算被追踪函数被调用的次数expect().toHaveBeenCalledWith()
:检查函数是否已使用特定的参数调用expect().toHaveBeenLastCalledWith()
:检查函数最后一次调用的参数
在不影响函数代码的情况下追踪包的调用
当您导入一个包时,可以使用spyOn()
告诉Jest“追踪”特定函数的执行,而不会影响该方法的工作方式。
示例:
const mathjs = require('mathjs')
test(`mathjs的log函数`, () => {
const spy = jest.spyOn(mathjs, 'log')
const result = mathjs.log(10000, 10)
expect(mathjs.log).toHaveBeenCalled()
expect(mathjs.log).toHaveBeenCalledWith(10000, 10)
})
模拟整个包
Jest提供了一种方便的方式来模拟整个包。在项目根目录中创建一个__mocks__
文件夹,并在此文件夹中为每个包创建一个JavaScript文件。
假设您导入了mathjs
。在项目根目录中创建一个__mocks__/mathjs.js
文件,并添加以下内容:
module.exports = {
log: jest.fn(() => 'test')
}
这将模拟包的log()函数。您可以添加任意数量的要模拟的函数:
const mathjs = require('mathjs')
test(`mathjs的log函数`, () => {
const result = mathjs.log(10000, 10)
expect(result).toBe('test')
expect(mathjs.log).toHaveBeenCalled()
expect(mathjs.log).toHaveBeenCalledWith(10000, 10)
})
模拟单个函数
您可以使用jest.fn()
模拟单个函数:
const mathjs = require('mathjs')
mathjs.log = jest.fn(() => 'test')
test(`mathjs的log函数`, () => {
const result = mathjs.log(10000, 10)
expect(result).toBe('test')
expect(mathjs.log).toHaveBeenCalled()
expect(mathjs.log).toHaveBeenCalledWith(10000, 10)
})
您还可以使用jest.fn().mockReturnValue('test')
创建一个简单的模拟,除了返回一个值之外,它不执行任何操作。
预先构建的模拟
您可以在常用库中找到预制的模拟。例如,这个包 https://github.com/jefflau/jest-fetch-mock 允许您模拟fetch()
调用,并在测试中提供样本返回值,而无需与实际服务器进行交互。
快照测试
快照测试是Jest提供的一个非常棒的功能。它可以记住您的UI组件是如何呈现的,并将其与当前测试进行比较,如果不匹配,则引发错误。
这是对简单的create-react-app
应用程序的App组件进行的一个简单的测试(确保您安装了react-test-renderer
):
import React from 'react'
import App from './App'
import renderer from 'react-test-renderer'
it('正确呈现', () => {
const tree = renderer.create(<App />).toJSON()
expect(tree).toMatchSnapshot()
})
首次运行此测试时,Jest将快照保存在__snapshots__
文件夹中。以下是App.test.js.snap文件的内容:
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
className="App"
>
<header
className="App-header"
>
<img
alt="logo"
className="App-logo"
src="logo.svg"
/>
<h1
className="App-title"
>
Welcome to React
</h1>
</header>
<p
className="App-intro"
>
To get started, edit
<code>
src/App.js
</code>
and save to reload.
</p>
</div>
`
如您所见,它是App组件呈现的代码,没有其他内容。
下次运行测试时,测试将输出<App />
的输出并将其与快照进行比较。如果App发生了更改,则会引发错误:
在create-react-app
中使用yarn test
时,您处于监视模式,在那里您可以按w
键显示更多选项:
监视模式用法
› 按u键以更新失败的快照。
› 按p键以通过文件名正则表达式进行筛选。
› 按t键以通过测试名称正则表达式进行筛选。
› 按q键退出监视模式。
› 按Enter键触发一次测试运行。
如果您的更改是有意的,则按u
键将更新失败的快照,并使测试通过。
您还可以在watch模式之外运行jest -u
(或jest --updateSnapshot
)来更新快照。