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 testnpm 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来运行测试。

现在,您还没有任何测试,因此不会执行任何操作:

使用Yarn进行测试

让我们创建第一个测试。打开一个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命令来手动执行测试。它将运行测试并保持在监视模式下,以在更改了具有测试(或测试文件)的文件之一时重新运行它们:

在VS Code中运行一个简单的Jest测试

匹配器

在前一篇文章中,我只使用了toBe()作为唯一的匹配器

test('Adding 1 + 1 equals 2', () => {
 expect(sum(1, 1)).toBe(2)
})

匹配器是一种方法,用于测试值。

最常用的匹配器是将expect()的结果与传递的值进行比较:

  • toBe使用===进行严格的相等比较
  • toEqual比较两个变量的值。如果它是对象或数组,则检查所有属性或元素的等式
  • toBeNull在传递null值时为true
  • toBeDefined在传递一个已定义的值时为true(与上述相反)
  • toBeUndefined在传递一个未定义的值时为true
  • toBeCloseTo用于比较浮点值,避免四舍五入误差
  • toBeTruthy如果值被视为真(例如if语句)则为true
  • toBeFalsy如果值被视为假(例如if语句)则为true
  • toBeGreaterThan如果expect()的结果大于参数,则为true
  • toBeGreaterThanOrEqual如果expect()的结果等于参数或大于参数,则为true
  • toBeLessThan如果expect()的结果小于参数,则为true
  • toBeLessThanOrEqual如果expect()的结果等于参数或小于参数,则为true
  • toMatch用于使用正则表达式模式匹配字符串
  • toContain用于数组,在其元素集合中包含参数时为true
  • toHaveLength(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()
 }
})

Jest异步测试回调函数

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')
 })
})

Jest异步测试Promises

被拒绝的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')
 })
})

Jest异步测试catch

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')
})

Jest异步测试等待异步

模拟

在测试中,模拟允许您测试依赖于:

  • 数据库
  • 网络请求
  • 访问文件
  • 任何外部系统

这样做的好处是:

  1. 您的测试运行更快,在开发过程中快速获得反馈
  2. 您的测试与网络条件或数据库状态无关
  3. 您的测试不会对任何数据存储造成污染,因为它们不会触及数据库
  4. 对测试所做的任何更改都不会影响后续测试的状态,并且重新运行测试套件应该从一个已知且可重现的起始点开始
  5. 您不必担心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)来更新快照。