错误发现得越早,改正错误的成本越低,正确改正错误的可能性也越大。 —— 《软件测试的艺术》
基础配置
yarn add --dev jest typescript ts-jest @types/jest
基本用法
代码:
export function sum(a: number, b: number) {
return a + b;
}
单个用例
// single test
test('1+2=3', () => {
let num = sum(1, 2);
expect(num).toEqual(3);
});
yarn jest
运行结果:
PASS src/__tests__/math.test.ts
✓ 1+2=3 (3 ms)
多个用例组合:describe + test
// multiple tests
describe('sum', () => {
test('1+2=3', () => {
let num = sum(1, 2);
expect(num).toEqual(3);
});
test('11+12=23', () => {
let num = sum(11, 12);
expect(num).toEqual(23);
});
});
yarn jest
运行结果:
PASS src/__tests__/math.test.ts
sum
✓ 1+2=3 (2 ms)
✓ 11+12=23
用例运行前后 hook 设置:Setup and Teardown
hooks:
- beforeEach
- afterEach
- beforeAll
- afterAll
作用域:文件或 describe 内
执行顺序:用例+hooks 组合场景
- 按文件顺序执行 describe 内同步代码
-
按文件顺序执行执行 test 单测:
- beforeAll(作用域内执行一次)
- beforeEach
- test 单测
- afterEach
- afterAll(作用域内执行一次)
示例:order-of-exec.test.ts
beforeAll(() => console.log('1 - beforeAll'));
afterAll(() => console.log('1 - afterAll'));
beforeEach(() => console.log('1 - beforeEach'));
afterEach(() => console.log('1 - afterEach'));
test('', () => console.log('1 - test'));
describe('Scoped / Nested block', () => {
console.log('describe----------');
beforeAll(() => console.log('2 -- beforeAll'));
afterAll(() => console.log('2 -- afterAll'));
beforeEach(() => console.log('2 -- beforeEach'));
afterEach(() => console.log('2 -- afterEach'));
test('', () => console.log('2 -- test'));
});
// 执行结果:
// describe----------
// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll
建议:do setup and teardown inside
before*
andafter*
handlers rather than inside thedescribe
blocks.
skip and only
一般用于验证和调试
skip:
describe('skip', () => {
test('should be ok', () => {
expect(3).toEqual(3);
});
// skip 的会被跳过
test.skip('skip', () => {
expect(3).toEqual(3);
});
});
执行结果:
PASS src/__tests__/skip.test.ts
skip
✓ should be ok (2 ms)
○ skipped skip
only:
describe('only', () => {
// 只有 only 的会执行
test.only('should be ok', () => {
expect(3).toEqual(3);
});
test('skip', () => {
expect(3).toEqual(3);
});
});
执行结果:
PASS src/__tests__/only.test.ts
only
✓ should be ok (2 ms)
○ skipped skip
进阶用法
异步
async function getDataByAsync() {
await new Promise((res) => setTimeout(res, 1 * 100));
return 3;
}
describe('getDataByAsync', () => {
// test 的 fn 参数传入一个 异步函数, hooks 也能这么用
test('data should be 3', async () => {
let data = await getDataByAsync(); // 记得 await
expect(data).toEqual(3);
});
});
Error
同步方法
function funcThantThrowError() {
throw new Error('something wrong');
}
test('funcThantThrowError', () => {
// 参数为函数名
expect(funcThantThrowError).toThrow('something wrong');
// 带参数的写法: 若 funcThantThrowError 带参数
expect(() => funcThantThrowError(1,2,3)).toThrow('something wrong');
});
异步方法
async function funcThantThrowErrorByAsync() {
await new Promise((res) => setTimeout(res, 1 * 100));
throw new Error('something wrong by async');
}
// method 1
test('test funcThantThrowErrorByAsync with try catch', async () => {
try {
await funcThantThrowErrorByAsync();
} catch (error) {
expect(error).toEqual(new Error('something wrong by async'));
}
});
// method 2
test('test funcThantThrowErrorByAsync with rejects', async () => {
await expect(funcThantThrowErrorByAsync).rejects.toThrow(
'something wrong by async'
);
});
Mock
函数
function forEach(n: number, callback: Function) {
for (let index = 0; index < n; index++) {
callback(index);
}
}
test('forEach', () => {
const mockCallback = jest.fn((x) => 42 + x);
forEach(2, mockCallback);
// The mock function is called twice
expect(mockCallback.mock.calls.length).toBe(2);
// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
// The return value of the first call to the function was 42
expect(mockCallback.mock.results[0].value).toBe(42);
// The return value of the second call to the function was 43
expect(mockCallback.mock.results[1].value).toBe(43);
});
类中的方法
使用spyOn
class DataProvider {
getDataById(id: string) {
return `mock data ${id}`;
}
}
function getData(id: string) {
let provider = new DataProvider();
return provider.getDataById(id);
}
describe('getData', () => {
test('should pass', () => {
// mock DataProvider 的 getDataById 方法
const spy = jest
.spyOn(DataProvider.prototype, 'getDataById')
.mockImplementation((id: string): string => {
return `spy mock data ${id}`;
});
let data = getData('11');
expect(data).toBe('spy mock data 11');
// 清除 mock,否则 下个用例会失败,一般清理操作会放到 hook 中
spy.mockRestore();
});
test('should fail if spy is enabled', () => {
let data = getData('11');
expect(data).toBe('mock data 11');
});
});
普通模块
src/async-data-provider.ts:
export async function getDataByAsync() {
await new Promise((res) => setTimeout(res, 1 * 100));
return 3;
}
export async function getDataById(id: string) {
await new Promise((res) => setTimeout(res, 1 * 100));
return `mock data ${id}`;
}
方法1:参考「类中的方法」的 spyOn
import * as provider from '../async-data-provider';
test('getDataById', async () => {
const spy = jest
.spyOn(provider, 'getDataById')
.mockImplementation(async (id: string) => {
return `spy mock data ${id}`;
});
let data = await provider.getDataById('11');
expect(data).toBe('spy mock data 11');
spy.mockRestore();
});
方义2:
mock 文件:src/__mocks__/async-data-provider.ts
export async function getDataById(id: string) {
await new Promise((res) => setTimeout(res, 1 * 100));
return `mock mock data ${id}`;
}
测试文件:src/__tests__/mock-module-with-mock.test.ts
import * as provider from '../async-data-provider';
jest.mock('../async-data-provider');
test('getDataById', async () => {
let data = await provider.getDataById('11');
expect(data).toBe('mock mock data 11');
});
node_modules 中的模块
- 需要在 jest 配置中的
roots
目录下,创建__mocks__
文件夹
- 模块命名规则:
__mocks__/@scope/project-name.ts
Mock 文件:src/__mocks__/lodash.ts
function nth<T>(items: T[], n: number) {
console.log('mock lodash-----');
return items[n];
}
let _ = { nth };
export default _;
测试文件:src/__tests__/mock-npm-module.test.ts
import _ from 'lodash';
test('mock nth', async () => {
let items = [0, 1, 2];
let n = _.nth(items, 2);
expect(n).toBe(2);
});
执行结果:
mock lodash-----
PASS src/__tests__/mock-npm-module.test.ts
✓ mock nth (19 ms)
其它:mock 万物
Todo: mysql,mongodb,redis,mq...
jest.requireActual
其他
覆盖率
细化统计
命令:yarn jest --coverage --silent
------------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------------|---------|----------|---------|---------|-------------------
All files | 25 | 100 | 0 | 33.33 |
async-data-provider.ts | 25 | 100 | 0 | 33.33 | 2-3
------------------------|---------|----------|---------|---------|-------------------
汇总统计
命令:yarn jest --coverage --silent --coverageReporters="text-summary"
=============================== Coverage summary ===============================
Statements : 25% ( 1/4 )
Branches : 100% ( 0/0 )
Functions : 0% ( 0/2 )
Lines : 33.33% ( 1/3 )
================================================================================
expect 源码
jest/packages/expect/src/index.ts:
export const expect: Expect = (actual: any, ...rest: Array<any>) => {
if (rest.length !== 0) {
throw new Error('Expect takes at most one argument.');
}
const allMatchers = getMatchers();
const expectation: any = {
not: {},
rejects: {not: {}},
resolves: {not: {}},
};
const err = new JestAssertionError();
Object.keys(allMatchers).forEach(name => {
const matcher = allMatchers[name];
const promiseMatcher = getPromiseMatcher(name, matcher) || matcher;
expectation[name] = makeThrowingMatcher(matcher, false, '', actual);
expectation.not[name] = makeThrowingMatcher(matcher, true, '', actual);
expectation.resolves[name] = makeResolveMatcher(
name,
promiseMatcher,
false,
actual,
err,
);
expectation.resolves.not[name] = makeResolveMatcher(
name,
promiseMatcher,
true,
actual,
err,
);
expectation.rejects[name] = makeRejectMatcher(
name,
promiseMatcher,
false,
actual,
err,
);
expectation.rejects.not[name] = makeRejectMatcher(
name,
promiseMatcher,
true,
actual,
err,
);
});
return expectation;
};