各框架特点
Jest
- facebook 坐庄
- 基于 Jasmine 至今已经做了大量修改添加了很多特性
- 开箱即用配置少,API简单
- 支持断言和仿真
- 支持快照测试
- 在隔离环境下测试
- 互动模式选择要测试的模块
- 优雅的测试覆盖率报告,基于Istanbul
- 智能并行测试(参考)
- 全局环境,比如 describe 不需要引入直接用
- 较多用于 React 项目(但广泛支持各种项目)
Mocha
- 灵活(不包括断言和仿真,自己选对应工具)
流行的选择:chai,sinon - 社区成熟用的人多,测试各种东西社区都有示例
- 需要较多配置
- 可以使用快照测试,但依然需要额外配置
Jasmine
- 开箱即用(支持断言和仿真)
- 全局环境
- 比较'老',坑基本都有人踩过了
AVA
- 异步,性能好
- 简约,清晰
- 快照测试和断言需要三方支持
Tape
- 体积最小,只提供最关键的东西
- 对比其他框架,只提供最底层的 API
JEST
1 什么是 Jest
Jest 是 Facebook 的一套开源的 JavaScript 测试框架,它自动集成了断言、JSDom、覆盖率报告等开发者所需要的所有测试工具,是一款几乎零配置的测试框架。基于Jasmine的JavaScript单元测试框架。Jest源于Facebook的构想,用于快速、可靠地测试Web聊天应用。它吸引了公司内部的兴趣,Facebook的一名软件工程师Jeff Morrison半年前又重拾这个项目,改善它的性能,并将其开源。Jest的目标是减少开始测试一个项目所要花费的时间和认知负荷,因此它提供了大部分你需要的现成工具:快速的命令行接口、Mock工具集以及它的自动模块Mock系统。此外,如果你在寻找隔离工具例如Mock库,分其它工具大部将让你在测试中(甚至经常在你的主代码中)写一些不尽如人意的样板代码,以使其生效。Jest与Jasmine框架的区别是在后者之上增加了一些层。最值得注意的是,运行测试时,Jest会自动模拟依赖。Jest自动为每个依赖的模块生成Mock,并默认提供这些Mock,这样就可以很容易地隔离模块的依赖。
Jest支持Babel,我们将很轻松的使用ES6的高级语法
Jest支持webpack,非常方便的使用它来管理我们的项目
Jest支持TypeScript,书写测试用例更加严谨
-
简化API
Jest既简单又强大,内置支持以下功能:
- 灵活的配置:比如,可以用文件名通配符来检测测试文件。
- 测试的事前步骤(Setup)和事后步骤(Teardown),同时也包括测试范围。
- 匹配表达式(Matchers):能使用期望
expect句法来验证不同的内容。 - 测试异步代码:支持承诺(promise)数据类型和异步等待
async/await功能。 - 模拟函数:可以修改或监查某个函数的行为。
- 手动模拟:测试代码时可以忽略模块的依存关系。
- 虚拟计时:帮助控制时间推移。
-
性能与隔离
Jest文档里写道:
Jest能运用所有的工作部分,并列运行测试,使性能最大化。终端上的信息经过缓冲,最后与测试结果一起打印出来。沙盒中生成的测试文件,以及自动全局状态在每个测试里都会得到重置,这样就不会出现两个测试冲突的情况。
Mocha用一个进程运行所有的测试,和它比较起来,Jest则完全不同。要在测试之间模拟出隔离效果,我们必须要引入几个测试辅助函数来妥善管理清除工作。这种做法虽然不怎么理想,但99%的情况都可以用,因为测试是按顺序进行的。
-
沉浸式监控模式
快速互动式监控模式可以监控到哪些测试文件有过改动,只运行与改动过的文件相关的测试,并且由于优化作用,能迅速放出监控信号。设置起来非常简单,而且还有一些别的选项,可以用文件名或测试名来过滤测试。我们用Mocha时也有监控模式,不过没有那么强大,要运行某个特定的测试文件夹或文件,就不得不自己创造解决方法,而这些功能Jest本身就已经提供了,不用花力气。
-
代码覆盖率&测试报告
Jest内置有代码覆盖率报告功能,设置起来易如反掌。可以在整个项目范围里收集代码覆盖率信息,包括未经受测试的文件。
要使完善Circle CI整合,只需要一个自定义报告功能。有了Jest,用jest-junit-reporter就可以做到,其用法和Mocha几乎相同。](github.com/michaelleea…)
-
快照功能
快照测试的目的不是要替换现有的单元测试,而是要使之更有价值,让测试更轻松。在某些情况下,某些功能比如React组件功能,有了快照测试意味着无需再做单元测试,但同样这两者不是非此即彼。
2 怎么用 Jest
安装
新建文件夹然后通过npm 命令安装:
npm install --save-dev jest
或者通过yarn来安装:
yarn add --dev jest
然后就可以开始测试了
也可用npm install -g jest进行全局安装;并在 package.json 中指定 test 脚本:
{
"scripts": {
"test": "jest"
}
}
Jest 的测试脚本名形如.test.js,不论 Jest 是全局运行还是通过npm test运行,它都会执行当前目录下所有的*.test.js 或 *.spec.js 文件、完成测试。
ES6语法支持:
- 安装依赖
yarn add --dev babel-jest @babel/core @babel/preset-env
- 配置
.babelrc
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
接下来就可以使用ES6的语法了~~~
更多高阶的ES6/7/8…语法,可以参见:babel官网
关于Typescript的支持,可以参见 :Using Typescript
3 初识 Jest
-
Introduction 简介
- Getting Started 入门
- Using Matchers 使用匹配器
- Testing Asynchronous Code 测试异步代码
- Setup and Teardown 设置和拆卸
- Mock Functions mock函数
- Jest Platform jest平台
- Jest Community jest社区
- More Resources 更多资源
-
Guides 指南
- Snapshot Testing 快照测试
- An Async Example 异步示例
- Timer Mocks 定时器模拟
- Manual Mocks 手动模拟
- ES6 Class Mocks es6类模拟
- Bypassing module mocks 绕过模块模拟
- ECMAScript Modules ECMAScript模块
- Using with webpack 和webpack一起使用
- Using with puppeteer 和puppeteer一起使用
- Using with MongoDB 和MongoDB一起使用
- Using with DynamoDB 和DynamoDB一起使用
- DOM Manipulation DOM操作
- Watch Plugins 监听插件
- Migrating to Jest 迁移到jest
- Troubleshooting 故障排除
- Architecture 建筑
-
Framework Guides 框架指南
4 有能力的Jest
4.1 匹配器
什么是匹配器,有什么用?
Jest uses "matchers" to let you test values in different ways(Jest 使用“匹配器”让您以不同方式测试值)
-
expect(value) expect
expect每次要测试值时都会使用该函数。test('two plus two is four', () => {
expect(2 + 2).toBe(4);
});
-
expect.extend(matchers) 将自己的匹配器添加到 Jest -
expect.anything() -
expect.any(constructor) -
.not 不会
expect(1+1).not.toBe(3); -
.resolves 使用resolves解开一个兑现承诺的价值,所以任何其他匹配可以链接。如果承诺被拒绝,则断言失败
await expect(Promise.resolve('lemon')).resolves.toBe('lemon'); -
.rejects 使用.rejects拆开包装,因此任何其他匹配可链接被拒绝承诺的理由。如果承诺被实现,则断言失败 -
.toHaveBeenCalled() 确保函数得到调用 -
.toHaveBeenCalledTimes(number) 确保函数得到调用次数确切数字 -
.toHaveBeenCalledWith(arg1, arg2, ...) 确保模拟函数被调用的具体参数 -
.toHaveBeenLastCalledWith(arg1, arg2, ...) 测试最后一次调用的参数 -
.toHaveBeenNthCalledWith(nthCall, arg1, arg2, ....) 用来测试它被调用的参数\test('drinkEach drinks each drink', () => { const drink = jest.fn(); drinkEach(drink, ['lemon', 'octopus']); expect(drink).toHaveBeenNthCalledWith(1, 'lemon'); expect(drink).toHaveBeenNthCalledWith(2, 'octopus'); }); -
.toHaveReturned() 测试模拟函数是否成功返回(即没有抛出错误)至少一次 -
.toHaveLength(number) 使用.toHaveLength检查的对象有一个.length属性,并将其设置为某一数值 -
.toBeCloseTo(number, numDigits?) 使用toBeCloseTo浮点数的近似相等比较\test('adding works sanely with decimals', () => { expect(0.2 + 0.1).toBeCloseTo(0.3, 5); }); -
.toBeNull()`` 只匹配null -
.toBeTruthy()``匹配任何if语句为真 -
.toBeUndefined() 只匹配undefined -
.toContain(item) 使用.toContain时要检查的项目是在数组中 -
.toEqual(value) 用于.toEqual递归比较对象实例的所有属性(也称为“深度”相等) -
.toMatch(regexp | string) 使用.toMatch检查字符串中的正则表达式匹配。 -
.toMatchSnapshot(propertyMatchers?, hint?) 这可确保值与最近的快照相匹配 -
.toThrow(error?) 函数抛出 -
.toThrowErrorMatchingSnapshot(hint?) 测试一个函数抛出匹配最新的快照时
4.2 异步代码测试
1:callback方式
test('the data is peanut butter', done => {
function callback(data) {
try {
expect(data).toBe('peanut butter');
done();
} catch (error) {
done(error);
}
}
fetchData(callback);
});
2:Promises
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
3:.resolves / .rejects
test('the data is peanut butter', () => {
return expect(fetchData()).resolves.toBe('peanut butter');
});
4:Async/Await
test('the data is peanut butter', async () => {
await expect(fetchData()).resolves.toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
await expect(fetchData()).rejects.toMatch('error');
});
4.3 mock函数和spy
Mock与Spy
mock测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。
Mock 是单元测试中经常使用的一种技术。单元测试,顾名思义测试的重点是某个具体单元。但是在实际代码中,代码与代码之间,模块与模块之间总是会存在着相互引用。这个时候,剥离出这种单元的依赖,让测试更加独立,使用到的技术就是 Mock。
jest.fn(implementation)返回一个新的、未使用的模拟函数jest.isMockFunction(fn) 确定给定的函数是否是模拟函数jest.spyOn(object, methodName) 创建一个类似于jest.fn但也跟踪对 的调用的模拟函数jest.spyOn(object, methodName, accessType?) 从 Jest 22.1.0+ 开始,该jest.spyOn方法采用可选的第三个参数accessType,可以是'get'或'set',这在你想分别监视 getter 或 setter 时被证明是有用的。jest.clearAllMocks() 清除所有模拟的mock.calls,mock.instances和mock.results属性jest.resetAllMocks() 重置所有模拟的状态jest.restoreAllMocks() 将所有模拟恢复到其原始值
为什么要使用Mock函数?
在项目中,一个模块的方法内常常会去调用另外一个模块的方法。在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,使用Mock函数是十分有必要。
Mock函数提供的以下三种特性,在我们写测试代码时十分有用:
- 捕获函数调用情况
- 设置函数返回值
- 改变函数的内部实现
举个例子:
// math.js
export const getFooResult = () => {
// foo logic here
};
export const getBarResult = () => {
// bar logic here
};
// caculate.js
import { getFooResult, getBarResult } from "./math";
export const getFooBarResult = () => getFooResult() + getBarResult();
此时,getFooResult() 和 getBarResult() 就是 getFooBarResult 这个函数的依赖。如果我们关注的点是 getFooBarResult 这个函数,我们就应该把 getFooResult 和 getBarResult Mock 掉,剥离这种依赖。下面是一个使用 Jest 进行 Mock 的例子。
jest.fn()是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。
test('测试jest.fn()调用', () => {
let mockFn = jest.fn();
let result = mockFn(1, 2, 3);
// 断言mockFn的执行后返回undefined
expect(result).toBeUndefined();
// 断言mockFn被调用
expect(mockFn).toBeCalled();
// 断言mockFn被调用了一次
expect(mockFn).toBeCalledTimes(1);
// 断言mockFn传入的参数为1, 2, 3
expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
})
情景一:设置函数 的返回值
// calculate.test.js
import { getFooBarResult } from "./calculate";
import * as fooBar from './math';
test('getResult should return result getFooResult() + getBarResult()', () => {
// mock add方法和multiple方法
fooBar.getFooBarResult = jest.fn(() => 10);
fooBar.getBarResult = jest.fn(() => 5);
const result = getFooBarResult();
expect(result).toEqual(15);
});
Mock其实就是一种Spies,在Jest中使用spies来“spy”(窥探)一个函数的行为。
Jest文档对于spies的解释](jestjs.io/docs/en/moc…):
Mock函数也称为“spies”,因为它们让你窥探一些由其他代码间接调用的函数的行为,而不仅仅是测试输出。你可以通过使用
jest.fn()创建一个mock函数。
简单来说,一个spy是另一个内置的能够记录对其调用细节的函数:调用它的次数,使用什么参数。
// calculate.test.js
import { getFooBarResult } from "./calculate";
import * as fooBar from './math';
test('getResult should return result getFooResult() + getBarResult()', () => {
// mock add方法和multiple方法
fooBar.getFooResult = jest.fn(() => 10);
fooBar.getBarResult = jest.fn(() => 5);
const result = getFooBarResult();
// 监控getFooResult和getBarResult的调用情况.
expect(fooBar.getFooResult).toHaveBeenCalled();
expect(fooBar.getBarResult).toHaveBeenCalled();
});
情景二:捕获函数调用情况
// bot method
const bot = {
sayHello: name => {
console.log(`Hello ${name}!`);
}
};
// test.js
describe("bot", () => {
it("should say hello", () => {
const spy = jest.spyOn(bot, "sayHello");
bot.sayHello("Michael");
expect(spy).toHaveBeenCalledWith("Michael");
spy.mockRestore();
});
});
我们通过 jest.spyOn 创建了一个监听 bot 对象的 sayHello 方法的 spy。它就像间谍一样监听了所有对 bot#sayHello 方法的调用。由于创建 spy 时,Jest 实际上修改了 bot 对象的 sayHello 属性,所以在断言完成后,我们还要通过 mockRestore 来恢复 bot 对象原本的 sayHello方法。
Jest的spyOn介绍
情景三:修改函数的内容实现
const bot = {
sayHello: name => {
console.log(`Hello ${name}!`);
}
};
describe("bot", () => {
it("should say hello", () => {
const spy = jest.spyOn(bot, "sayHello").mockImplementation(name => {
console.log(`Hello mix ${name}`)
});
bot.sayHello("Michael");
expect(spy).toHaveBeenCalledWith("Michael");
spy.mockRestore();
});
});
使用spyOn方法,还可以去修改Math.random这样的函数
jest.spyOn(Math, "random").mockImplementation(() => 0.9);
举个例子:
// getNum.js
const arr = [1,2,3,4,5,6];
const getNum = index => {
if (index) {
return arr[index % 6];
} else {
return arr[Math.floor(Math.random() * 6)];
}
};
// num.test.js
import { getNum } from '../src/getNum'
describe("getNum", () => {
it("should select numbber based on index if provided", () => {
expect(getNum(1)).toBe(2);
});
it("should select a random number based on Math.random if skuId not available", () => {
const spy = jest.spyOn(Math, "random").mockImplementation(() => 0.9);
expect(getNum()).toBe(6);
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
});
4.4 定时器模拟
jest.useFakeTimers() or jest.useRealTimers()
jest.useFakeTimers(implementation?: 'modern' | 'legacy') 指示jest使用的虚假的定时器jest.useRealTimers() 指示 Jest 使用标准计时器函数的真实版本。jest.runAllTicks()jest.runAllTimers()jest.runAllImmediates()jest.advanceTimersByTime(msToRun)jest.runOnlyPendingTimers()jest.advanceTimersToNextTimer(steps)jest.clearAllTimers()jest.getTimerCount()jest.setSystemTime(now?: number | Date)jest.getRealSystemTime()
4.5 DOM操作
'use strict';
jest.mock('../fetchCurrentUser');
test('displays a user after a click', () => {
// Set up our document body
document.body.innerHTML =
'<div>' +
' <span id="username" />' +
' <button id="button" />' +
'</div>';
// This module has a side-effect
require('../displayUser');
const $ = require('jquery');
const fetchCurrentUser = require('../fetchCurrentUser');
// Tell the fetchCurrentUser mock function to automatically invoke
// its callback with some data
fetchCurrentUser.mockImplementation(cb => {
cb({
fullName: 'Johnny Cash',
loggedIn: true,
});
});
// Use jquery to emulate a click on our button
$('#button').click();
// Assert that the fetchCurrentUser function was called, and that the
// #username span's inner text was updated as we'd expect it to.
expect(fetchCurrentUser).toBeCalled();
expect($('#username').text()).toEqual('Johnny Cash - Logged In');
});
@testing-library/react
Simple and complete React DOM testing utilities that encourage good testing practices.
简单而完整的 React DOM 测试实用程序,支持良好的测试实践。
4.6 快照测试
5 标识美男Jest
afterAll(fn, timeout)afterEach(fn, timeout)beforeAll(fn, timeout)beforeEach(fn, timeout)describe(name, fn)describe.each(table)(name, fn, timeout)describe.only(name, fn)describe.only.each(table)(name, fn)describe.skip(name, fn)describe.skip.each(table)(name, fn)test(name, fn, timeout)test.concurrent(name, fn, timeout)test.concurrent.each(table)(name, fn, timeout)test.concurrent.only.each(table)(name, fn)test.concurrent.skip.each(table)(name, fn)test.each(table)(name, fn, timeout)test.only(name, fn, timeout)test.only.each(table)(name, fn)test.skip(name, fn)test.skip.each(table)(name, fn)test.todo(name)
来个例子:
创建一个react项目 npx create-react-app my-app
资料来源:
jest中文官网:jestjs.io/docs/code-t…
@testing-library/react地址 :www.npmjs.com/package/@te…
jest教程:juejin.cn/post/684490…
使用 React Testing Library 和 Jest 完成单元测试:segmentfault.com/a/119000002…