一、起步
Jest 是什么?
Jest 是用来创建、执行和构建测试用例的一个 JavaScript 测试库, 是目前最受欢迎的测试执行器。
首先,使用 yarn 或 npm 安装 Jest︰
yarn add --dev jest or npm install --save-dev jest
接着,创建一个 sum.js 的文件:
export default function sum(a, b) {
return a + b
}
然后,创建一个名为 sum.test.js 的文件。 这将包含我们的实际测试:
import sum from './sum'
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3)
})
将下面的配置部分添加到你的 package.json 里面:
{
"scripts": {
"test": "jest"
}
}
PASS ./sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)
如果想要支持TypeScript ,可以使用 ts-jest.
二、匹配器
测试值最简单的方法是看是否精确匹配期望。
test('2 + 2 = 4', () => {
expect(2 + 2).toBe(4)
})
在上面的代码中,expect (2 + 2) 返回一个"期望"的对象,.toBe(4) 是匹配器。 当 Jest 运行时,它会跟踪所有失败的匹配器,以便它可以为你打印出很好的错误消息。
toBe 使用的是 Object.is 来测试精确相等。 如果想要检查对象的值,使用 toEqual 代替:
test('object assignment', () => {
const data = {name: 'Jest', age:12}
expect(data).toEqual({name: 'Jest', age:12});});
toEqual 会递归检查对象或数组的每个字段。还可以使用 not 测试相反的匹配︰
test('1 is not 0', () => {
expect(1).not.toBe(0)
})
其他的常用匹配器:
-
toBeNull 只匹配 null
-
toBeUndefined 只匹配 undefined
-
toBeDefined 与 toBeUndefined 相反
-
toBeTruthy 匹配任何 if 语句为真
-
toBeFalsy 匹配任何 if 语句为假
-
toBeGreaterThan 大于
-
toBeGreaterThanOrEqual 大于等于
-
toBeLessThan 小于
-
toBeLessThanOrEqual 小于等于
-
toMatch 正则匹配
-
toContain 包含
-
toThrow 抛出错误
三、测试异步代码
1. 回调
假设有一个 fetchData(callback) 函数,获取一些数据并在完成时调用 callback(data)。 期望返回的数据是一个字符串 'callback done'
默认情况下,Jest 测试一旦执行到末尾就会完成。 这意味着该测试不会按预期工作:
// 错误做法
test('the data is callback done', () => {
function callback(data) {
expect(data).toBe('callback done')
}
fetchData(callback)
})
fetchData执行结束,此测试就在没有调用回调函数前结束了。
解决方法:使用单个参数调用 done,而不是将测试放在一个空参数的函数。 Jest会等done回调函数执行结束后,结束测试。
test('the data is callback done', done => {
function callback(data) {
try {
expect(data).toBe('callback done')
done()
} catch (error) {
done(error)
}
}
fetchData(callback)
})
若 done() 函数从未被调用,测试用例会正如你预期的那样执行失败(显示超时错误)。
若 expect 执行失败,它会抛出一个错误,后面的 done() 不再执行。 若我们想知道测试用例为何失败,我们必须将 expect 放入 try 中,将 error 传递给 catch 中的 done函数。 否则,最后控制台将显示一个超时错误失败,不能显示我们在 expect(data) 中接收的值。
2.Promise
如果fetchData返回一个Promise,可以这样测试:
test('the data is success', () => {
return fetchData().then(data => {
expect(data).toBe('success')
})
})
注意:一定不要忘记把 promise 作为返回值。
如果忘了 return 语句的话,在 fetchData 返回的这个 promise 被 resolve、then() 有机会执行之前,测试就已经被视为已经完成了。
如果期望promise被reject的话,使用.catch方法。请确保添加 expect.assertions 来验证一定数量的断言被调用。否则,一个已兑现的承诺就不会失败。
test('the fetch fails with an error', () => {
expect.assertions(1)
return fetchData().catch(e => expect(e).toMatch('error'))
})
除了上面的方法,还可以使用.resolves / .rejects 匹配器:
test('the data is success', () => {
return expect(fetchData()).resolves.toBe('success');
})
3. Async/Await
test('the data is success', async () => {
const data = await fetchData()
expect(data).toBe('success')
});
test('the fetch fails with an error', async () => {
expect.assertions(1)
try {
await fetchData()
} catch (e) {
expect(e).toMatch('error')
}
})
四、预处理和后处理
有时我们想在测试开始之前进行下环境的检查、或者在测试结束之后作一些清理操作,这就需要对用例进行预处理或后处理。
对测试文件中所有的用例进行统一的预处理,使用 beforeAll() 函数;如果想在每个用例开始前进行都预处理,则可使用 beforeEach() 函数。至于后处理,也有对应的 afterAll() 和 afterEach() 函数。
如果只是想对某几个用例进行同样的预处理或后处理,可以使用 describe() 函数表示一组用例,再将上面提到的四个处理函数置于 describe() 的处理回调内,就实现了对一组用例的预处理或后处理。
describe 和 test 块的执行顺序
Jest 会在测试开始之前执行测试文件里所有的 describe 处理程序(handlers)。 当 describe 块运行完后,,默认情况下,Jest 会按照 test 出现的顺序依次运行所有测试,,等待每一个测试完成并整理好,然后才继续往下走。
因此,准备工作和整理工作最好在 before* 和 after* 处理程序里面 (而不是在 describe 块中)进行。
describe('outer', () => {
console.log('describe outer-a')
test('test for describe inner 1', () => {
console.log('test for describe inner 1')
expect(true).toEqual(true)
})
describe('describe inner', () => {
console.log('describe inner')
test('test for describe inner 2', () => {
console.log('test for describe inner 2')
expect(false).toEqual(false)
})
})
console.log('describe outer-b')
})
// describe outer-a
// describe inner
// describe outer-b
// test for describe inner 1
// test for describe inner 2
五、Mock 函数
Mock 函数允许你测试代码之间的连接——实现方式包括:擦除函数的实际实现、捕获对函数的调用 ( 以及在这些调用中传递的参数) 、在使用 new 实例化时捕获构造函数的实例、允许测试时配置返回值。
假设我们要测试函数 forEach 的内部实现,这个函数为传入的数组中的每个元素调用一次回调函数。
function forEach(arr, callback) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i])
}
}
为了测试此函数,我们可以使用一个 mock 函数,然后通过检查 mock 函数的状态来确保callback如期调用。
const mockCallback = jest.fn(x => 8 + x)
forEach([0, 1], mockCallback)
// 此 mock 函数被调用了两次
expect(mockCallback.mock.calls.length).toBe(2)
// 第一次调用函数时的第一个参数是 0
expect(mockCallback.mock.calls[0][0]).toBe(0)
// 第二次调用函数时的第一个参数是 1
expect(mockCallback.mock.calls[1][0]).toBe(1)
// 第一次函数调用的返回值是 42
expect(mockCallback.mock.results[0].value).toBe(8)
所有的 mock 函数都有一个特殊的 .mock属性,它保存了关于此函数如何被调用、调用时的返回值的信息。同时jest也为我们提供了toHaveBeenCalled、toHaveBeenCalledTimes、toHaveBeenCalledWith等自定义的匹配器。
注意:在Jest中如果想捕获函数的调用情况,则该函数必须被mock或者spy!
Mock 的返回值
const mockFn = jest.fn()
console.log(mockFn())// undefined
mockFn.mockReturnValueOnce(10).mockReturnValue(true)
console.log(mockFn(), mockFn(), mockFn())// 10, true, true
模拟模块
有时需要测试该方法而不实际调用 API ,比如在使用请求时会导致测试缓慢而且脆弱,这时可以用 jest.mock() 函数自动模拟 axios 模块。
模拟axios模块,我们可为 .get 提供一个 mockResolvedValue ,它会返回假数据用于测试。
import axios from 'axios'
jest.mock('axios')
test('should fetch users', () => {
const users = [{ name: 'Jest' }]
const response = { data: users }
axios.get.mockResolvedValue(response)
return axios
.get('/users.json')
.then(resp => resp.data)
.then(data => expect(data).toEqual(users))
})
六、Timer Mock
原生的定时器函数(如:setTimeout, setInterval, clearTimeout, clearInterval)并不是很方便测试,因为程序需要等待相应的延时。
我们可以通过jest.useFakeTimers()来模拟定时器函数。如果需要在一个文件或一个describe块中运行多次测试,可以在每次测试前手动添加jest.useFakeTimers(),或者在beforeEach中添加。 如果不这样做的话内部的定时器不会被重置。
-
runAllTimers 快进时间使得所有定时器回调被执行
-
runOnlyPendingTimers 只快进进行中的定时器
-
advanceTimersByTime 按时间提前定时器
-
clearAllTimers 删除所有待处理的计时器
七、Jest 覆盖率
在运行时我们可以直接增加 --coverage参数,生成coverage报表查看当前项目的覆盖率
"scripts": {
"test": "jest --coverage"
}
现在,每次运行 npm run test 时,我们项目下就会产生coverage报表来查看当前项目的覆盖率

八、遇到的一些问题及解决办法
通用建议
如果测试失败,第一件要检查的事就是,当仅运行这条测试时,它是否仍然失败。
test.only('this will be the only test that runs', () => {
expect(true).toBe(false)
});
test('this test will not run', () => {
expect('A').toBe('A')
})
如果你有一个测试作为一个更大的用例中的一部分时,运行失败。但是当你单独运行它时,并不会失败,所以最好考虑其他测试对这个测试的影响。 通常可以通过修改 beforeEach 来清除一些共享的状态来修复这种问题。
Jest 不会执行资源的onload事件
解决办法:
方案1——通过手动触发执行
beforeAll(() => {
Object.defineProperty(global.Image.prototype, 'src', {
set(src) {
if (src === 'LOAD_FAILURE_SRC') {
setTimeout(() => this.onerror(new Error('mocked error')))
} else if (src === 'LOAD_SUCCESS_SRC') {
setTimeout(() => this.onload())
}
}
})
})
方案2.——安装 canvas
yarn add canvas -D
package.json 中增加如下配置:
"testEnvironmentOptions": { "resources": "usable"}
原因:
当浏览器配置为不加载图像时,不会触发加载事件。jsdom默认配置为不加载任何外部资源,因此在这方面它遵循浏览器。这会使图像与其他未加载的资源(如视频、音频、linkrel=preload等)保持一致
所以我们可以切换到加载“usable”外部资源的模式,但是只有在安装了canvas包时,才包括图像。
所以只有在安装了canvas包时才会执行该测试,在这种情况下,加载事件会触发。
Jest 中某些API不存在
原因:
Jest的实现依赖于JsDom,如果存在JsDom中没有实现的API,则无法使用
解决方案: 手动mock
如果我们的测试中使用了window.matchMedia()。Jest将会返回 TypeError: window.matchMedia is not a function 导致测试出错。
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn()
}))
})
Jest测试随机性函数
mock 随机函数,返回固定值
测试时间:
const mockDate = new Date(1466424490000)
jest.spyOn(global, 'Date').mockImplementation(() => mockDate)
测试随机数:
global.Math.random = jest.fn(() => 0.5)
扩展
- 如果要测试 React 组件的话,可以查看 《Testing React Components with react-test-renderer, and the Act API》 这篇教程。这篇教程覆盖了单元测试组件、类组件、Hooks 组件以及新的 Act API。
- 如果想要了解关于 UI 测试 方面的东西,可以查看这篇教程 《Cypress Tutorial for Beginners: Getting started with End to End Testing》。
- 如果想要了解自动化测试和持续集成,可以查看 《Continuous Integration in JavaScript: a Guide (ft. Github Actions)》 这篇教程。