Getting Started
使用 yarn 安装 Jest︰
yarn add --dev jest
或 npm:
npm install --save-dev jest
注:Jest的文档统一使用yarn命令,不过使用npm也是可行的。 你可以在yarn的说明文档里看到yarn 与npm之间的对比。
让我们开始为一个假设函数编写测试,该函数将两个数字相加。 首先,创建一个 sum.js 文件:
function sum(a, b) {
return a + b;
}
module.exports = sum;
然后,创建一个名为 sum.test.js 的文件。 这将包含我们的实际测试:
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
将下面的配置部分添加到你的 package.json 里面:
{
"scripts": {
"test": "jest"
}
}
最后,运行 yarn test 或 npm run test ,Jest将打印下面这个消息:
PASS ./sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)
你刚刚成功地写了第一个Jest测试 !
此测试使用 expect 和 toBe 来测试两个值完全相同。
在命令行运行
你可以通过命令行直接运行Jest(前提是jest已经处于你的环境变量 PATH中,例如通过 yarn global add jest 或 npm install jest --global安装的Jest) ,并为其指定各种有用的配置项。
这里演示了如何对能匹配到 my-test 的文件运行 Jest、使用config.json 作为一个配置文件、并在运行完成后显示一个原生的操作系统通知。
jest my-test --notify --config=config.json
如果你愿意了解更多关于通过命令行运行 jest 的内容,请继续阅读 Jest CLI 选项 页面。
更多配置
生成一个基础配置文件
基于您的项目,Jest将向您提出几个问题,并将创建一个基本的配置文件,每个选项都有一个简短的说明:
jest --init
使用 Babel
如果需要使用 Babel,可以通过 yarn来安装所需的依赖。
yarn add --dev babel-jest @babel/core @babel/preset-env
可以在工程的根目录下创建一个babel.config.js文件用于配置与你当前Node版本兼容的Babel:
// babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
],
};
Babel的配置取决于具体的项目使用场景 ,可以查阅 Babel官方文档来获取更多详细的信息。
使用 webpack
Jest 可以用于使用 webpack 来管理资源、样式和编译的项目中。 webpack 与其他工具相比多了一些独特的挑战。 参考 webpack 指南 来开始起步。
使用 TypeScript
Jest supports TypeScript, via Babel. First make sure you followed the instructions on using Babel above. Next install the @babel/preset-typescript via yarn:
yarn add --dev @babel/preset-typescript
Then add @babel/preset-typescript to the list of presets in your babel.config.js.
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
+ '@babel/preset-typescript',
],
};
不过,在配合使用TypeScript与Babel时,仍然有一些 注意事项 。 因为需要Babel才能支持 TypeScript 转义, Jest 不支持 tests 代码进行类型检查。如果你想要, 你可以用 ts-jest。
Using Matchers
Jest使用“匹配器”让你可以用各种方式测试你的代码。 这篇文档将向你介绍一些常用的匹配器, 在 expect API 的文档里可以看到完整的列表。
普通匹配器
最简单的测试值的方法是看是否精确匹配。
test('two plus two is four', () => {
expect(2 + 2).toBe(4);
});
在此代码中,expect (2 + 2) 返回一个"期望"的对象。 你通常不会对这些期望对象调用过多的匹配器。 在此代码中,.toBe(4) 是匹配器。 当 Jest 运行时,它会跟踪所有失败的匹配器,以便它可以为你打印出很好的错误消息。
toBe 使用 Object.is 来测试精确相等。 如果您想要检查对象的值,请使用 toEqual 代替:
test('object assignment', () => {
const data = {one: 1};
data['two'] = 2;
expect(data).toEqual({one: 1, two: 2});
});
toEqual 递归检查对象或数组的每个字段。
您还可以测试相反的匹配︰
test('adding positive numbers is not zero', () => {
for (let a = 1; a < 10; a++) {
for (let b = 1; b < 10; b++) {
expect(a + b).not.toBe(0);
}
}
});
Truthiness
在测试中,你有时需要区分 undefined、 null,和 false,但有时你又不需要区分。 Jest 让你明确你想要什么。
toBeNull 只匹配 null
toBeUndefined 只匹配 undefined
toBeDefined 与 toBeUndefined 相反
toBeTruthy 匹配任何 if 语句为真
toBeFalsy 匹配任何 if 语句为假
例如:
test('null', () => {
const n = null;
expect(n).toBeNull();
expect(n).toBeDefined();
expect(n).not.toBeUndefined();
expect(n).not.toBeTruthy();
expect(n).toBeFalsy();
});
test('zero', () => {
const z = 0;
expect(z).not.toBeNull();
expect(z).toBeDefined();
expect(z).not.toBeUndefined();
expect(z).not.toBeTruthy();
expect(z).toBeFalsy();
});
您应该使用匹配器最精确地对应您的代码你想要什么。
数字
大多数的比较数字有等价的匹配器。
test('two plus two', () => {
const value = 2 + 2;
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
// toBe and toEqual are equivalent for numbers
expect(value).toBe(4);
expect(value).toEqual(4);
});
对于比较浮点数相等,使用 toBeCloseTo 而不是 toEqual,因为你不希望测试取决于一个小小的舍入误差。
test('两个浮点数字相加', () => {
const value = 0.1 + 0.2;
//expect(value).toBe(0.3); 这句会报错,因为浮点数有舍入误差
expect(value).toBeCloseTo(0.3); // 这句可以运行
});
字符串
您可以检查对具有 toMatch 正则表达式的字符串︰
test('there is no I in team', () => {
expect('team').not.toMatch(/I/);
});
test('but there is a "stop" in Christoph', () => {
expect('Christoph').toMatch(/stop/);
});
Arrays and iterables
你可以通过 toContain来检查一个数组或可迭代对象是否包含某个特定项:
const shoppingList = [
'diapers',
'kleenex',
'trash bags',
'paper towels',
'beer',
];
test('the shopping list has beer on it', () => {
expect(shoppingList).toContain('beer');
expect(new Set(shoppingList)).toContain('beer');
});
例外
如果你想要测试的特定函数抛出一个错误,在它调用时,使用 toThrow。
function compileAndroidCode() {
throw new Error('you are using the wrong JDK');
}
test('compiling android goes as expected', () => {
expect(compileAndroidCode).toThrow();
expect(compileAndroidCode).toThrow(Error);
// You can also use the exact error message or a regexp
expect(compileAndroidCode).toThrow('you are using the wrong JDK');
expect(compileAndroidCode).toThrow(/JDK/);
});
以及更多 这些只是浅尝辄止。 这些只是一部分,有关匹配器的完整列表,请查阅 参考文档。
一旦你学会了如何使用匹配器后,接下来可以学习 Jest 是如何让你可以 测试异步代码的。
Testing Asynchronous Code
在JavaScript中执行异步代码是很常见的。 当你有以异步方式运行的代码时,Jest 需要知道当前它测试的代码是否已完成,然后它可以转移到另一个测试。 Jest有若干方法处理这种情况。
回调
最常见的异步模式是回调函数。
例如,假设您有一个 fetchData(callback)函数,获取一些数据并在完成时调用 callback(data)。 你想要测试它返回的数据是不是 'peanut butter'.
默认情况下,Jest 测试一旦执行到末尾就会完成。 那意味着该测试将不会按预期工作:
// 不要这样做!
test('the data is peanut butter', () => {
function callback(data) {
expect(data).toBe('peanut butter');
}
fetchData(callback);
});
问题在于一旦fetchData执行结束,此测试就在没有调用回调函数前结束。
还有另一种形式的 test,解决此问题。 使用单个参数调用 done,而不是将测试放在一个空参数的函数。 Jest会等done回调函数执行结束后,结束测试。
// done 很重要
test('the data is peanut butter', done => {
function callback(data) {
expect(data).toBe('peanut butter');
done();
}
fetchData(callback);
});
如果 done()永远不会调用,这个测试将失败,这也是你所希望发生的。
Promises
如果你用 promises, 它有更直接的方法去处理异步测试.
在你的测试中返回一个promise,jest将会等待这个promise去执行resolve。
如果承诺被拒绝,则测试将自动失败。
举个例子,如果 fetchData 不使用回调函数,而是返回一个 Promise,其解析值为字符串 'peanut butter' 我们可以这样测试:
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
一定不要忘记把promise 作为返回值⸺如果你忘了 return 语句的话,在 fetchData返回的这个promise 被 resolve、then() 有机会执行之前,测试就已经被视为已经完成了。
如果你想要 Promise 被拒绝,使用 .catch 方法。 请确保添加 expect.assertions 来验证一定数量的断言被调用。 否则一个fulfilled态的 Promise 不会让测试失败︰
test('the fetch fails with an error', () => {
expect.assertions(1);
return fetchData().catch(e => expect(e).toMatch('error'));
});
.resolves / .rejects
您也可以在 expect 语句中使用 .resolves 匹配器,Jest 将等待此 Promise 解决。 如果承诺被拒绝,则测试将自动失败。
test('the data is peanut butter', () => {
return expect(fetchData()).resolves.toBe('peanut butter');
});
一定不要忘记把整个断言作为返回值返回⸺如果你忘了return语句的话,在 fetchData 返回的这个 promise 变更为 resolved 状态、then() 有机会执行之前,测试就已经被视为已经完成了。
如果你想要 Promise 被拒绝,使用 .catch 方法。 它参照工程 .resolves 匹配器。 如果 Promise 被拒绝,则测试将自动失败。
test('the fetch fails with an error', () => {
return expect(fetchData()).rejects.toMatch('error');
});
补充:
expect.assertions(number) 验证在测试期间是否调用了一定数量的断言。这在测试异步代码时通常很有用,以确保实际调用了回调中的断言。
例如,假设我们有一个函数doAsync,该函数接收两个回调callback1和callback2,它将以未知顺序异步调用这两个回调。 我们可以使用以下方法进行测试:
test('doAsync calls both callbacks', () => {
expect.assertions(2);
function callback1(data) {
expect(data).toBeTruthy();
}
function callback2(data) {
expect(data).toBeTruthy();
}
doAsync(callback1, callback2);
});
expect.assertions(2) 确保两个回调都实际被调用。
Async/Await
或者,您可以在测试中使用 async 和 await。可以写一个异步的测试,function 传给测试之前用async关键字。例如,可以用来测试相同的 fetchData 方案︰
test('the data is peanut butter', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
expect.assertions(1);
try {
await fetchData();
} catch (e) {
expect(e).toMatch('error');
}
});
你可以用 .resolves or .rejects绑定async and 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.toThrow('error');
});
在这些案例中,async and await 是语法糖,实现了promises例子中的相同逻辑。
上述的诸多形式中没有哪个形式特别优于其他形式,你可以在整个代码库中,甚至也可以在单个文件中混合使用它们。 这只取决于哪种形式更能使您的测试变得简单。
实战配置
安装各种包
// identity-obj-proxy An identity object using ES6 proxies. Useful for mocking webpack imports. For instance, you can tell Jest to mock this object as imported CSS modules; then all your className lookups on the imported styles object will be returned as-is. https://github.com/keyz/identity-obj-proxy
// jsdom JSDOM is a JavaScript based headless browser that can be used to create a realistic testing environment. https://github.com/jsdom/jsdom
// JavaScript Testing utilities for React https://airbnb.io/enzyme/
// https://github.com/facebook/jest
// react-test-renderer https://jestjs.io/docs/en/snapshot-testing
npm i jest jsdom babel-jest enzyme enzyme-adapter-react-16 react-test-renderer identity-obj-proxy -D
// 如果要支持ts
npm i ts-jest -D
// 如果要支持canvas
npm i canvas -D
修改jest.config.js配置
module.exports = {
// collectCoverage: false,
// collectCoverageFrom: ['src/**/*.{ts,tsx}', '!**/*.d.ts'],
// coveragePathIgnorePatterns: [
// '<rootDir>/src/Task/',
// '<rootDir>/src/Uploader/',
// '<rootDir>/src/utils/ReactVersionWrapper.tsx',
// ],
// coverageDirectory: './tests/coverage',
transform: {
// '^.+\\.(tsx|ts)?$': 'ts-jest',
'^.+\\.(jsx?|scss|js|css$)': 'babel-jest',
// '^.+\\.svg$': '<rootDir>/svgTransform.js',
},
transformIgnorePatterns: ['/node_modules/'],
// testRegex: '(/test/.*|\\.(test|spec))\\.(ts|tsx|js)$',
// moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
setupFiles: ['<rootDir>/tests/setup.js'],
moduleNameMapper: {
'\\.(scss|less|css)$': 'identity-obj-proxy',
},
// snapshotSerializers: ['enzyme-to-json/serializer'],
// testEnvironment: 'jsdom',
// testResultsProcessor: 'jest-sonar-reporter',
};
设置 setup.js
// JSDOM is a JavaScript based headless browser that can be used to create a realistic testing environment.
// JavaScript Testing utilities for React https://airbnb.io/enzyme/
// https://github.com/airbnb/enzyme/blob/master/docs/guides/jsdom.md
/*
Canvas support
jsdom includes support for using the canvas package to extend any <canvas> elements with the canvas API. To make this work, you need to include canvas as a dependency in your project, as a peer of jsdom. If jsdom can find the canvas package, it will use it, but if it's not present, then <canvas> elements will behave like <div>s. Since jsdom v13, version 2.x of canvas is required; version 1.x is no longer supported.
*/
const Adapter = require('enzyme-adapter-react-16');
const Enzyme = require('enzyme');
Enzyme.configure({ adapter: new Adapter() });
const { JSDOM } = require('jsdom');
const jsdom = new JSDOM('<!doctype html><html><body></body></html>');
const { window } = jsdom;
function copyProps(src, target) {
Object.defineProperties(target, {
...Object.getOwnPropertyDescriptors(src),
...Object.getOwnPropertyDescriptors(target),
});
}
global.window = window;
global.document = window.document;
global.navigator = {
userAgent: 'node.js',
};
global.requestAnimationFrame = function(callback) {
return setTimeout(callback, 0);
};
global.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
copyProps(window, global);
设置 babel.config.js
// babel.config.js
module.exports = {
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-react'],
};
拿一个jsx文件测试看看
import React from 'react';
import { mount, shallow } from 'enzyme';
const jsdom = require('jsdom');
import renderer from 'react-test-renderer';
const { JSDOM } = jsdom;
import List from '../list';
test('List render correctly', async () => {
const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`, {
resources: 'usable',
runScripts: 'dangerously',
});
await sleep();
global.window = dom.window;
const tree = renderer.create(<List />).toJSON();
expect(tree).toMatchSnapshot();
});
function sleep() {
return new Promise(resolve => {
setTimeout(() => {
resolve('');
}, 1500);
});
}
生成快照
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`List render correctly 1`] = `
<div
className=".blue"
>
aaa1111
<p>
aaaa
</p>
</div>
`;
参考文章: