简介

作为目前流行的前端测试框架之一,Test 框架提供比较完善的断言库,强大的 mock 功能,包含测试覆盖率工具,开箱即用。

匹配器 (Matchers)

在验证测试结果和预期值的时,用匹配器验证更加方便,支持的匹配值类型有:Boolean、Number、String、Array、Iterables等常见类型,提供很多匹配方法,还有更多高级其它的匹配用法。

常见类型的匹配

Normal
test('测试 2 + 2  是否为 4', () => {
  expect(2 + 2).toBe(4);
});
Boolean
test('null', () => {
  const n = null;
  expect(n).toBeNull();
  expect(n).toBeDefined();
  expect(n).not.toBeUndefined();
  expect(n).not.toBeTruthy();
  expect(n).toBeFalsy();
});
Number
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);
});
String
test('there is no I in team', () => {
  expect('team').not.toMatch(/I/);
  
});

test('but there is a "stop" in Christoph', () => {
  expect('Christoph').toMatch(/stop/);
});
Array 和 iterables
const shoppingList = [
  'diapers',
  'kleenex',
  'trash bags',
  'paper towels',
  'milk',
];

test('the shopping list has milk on it', () => {
  expect(shoppingList).toContain('milk');
  expect(new Set(shoppingList)).toContain('milk');
});

更多高级的匹配

Throw 的 匹配
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/);
});

抛出异常的函数需要被包裹在一个函数里

自定义匹配器

expect.extend({
  yourMatcher(x, y, z) {
    return {
      pass: true,
      message: () => '',
    };
  },
});

异步匹配

官方提供了 done 函数方式和 promise 方式,显然 promise 更为简单直接。

// 期望异步数据结果为:'peanut butter'
test('the data is peanut butter', () => {
  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});
// 支持 Async/Await
test('the data is peanut butter', async () => {
  const data = await fetchData();
  expect(data).toBe('peanut butter');
});

// 期望异步获取一个异常
test('the fetch fails with an error', () => {
  expect.assertions(1); 
  return fetchData().catch(e => expect(e).toMatch('error'));
});

expect.assertions(1); 确保当前测试至少执行了1次断言,也就是 catch 函数至少执行了一次。

Setup & Teardown

在单个 test 的时候,执行之前和执行之后可以通过勾子函数处理一些任务。

多次测试重复设置

beforeEach(() => {
  initializeCityDatabase();
});

afterEach(() => {
  clearCityDatabase();
});

test('测试1', () => {
  expect(isCity('Vienna')).toBeTruthy();
});
test('测试2', () => {
  expect(isCity('San Juan')).toBeTruthy();
});

执行一个 test,beforeEach 和 afterEach 被调用一次。

一次设置

beforeAll(() => {
  return initializeCityDatabase();
});

afterAll(() => {
  return clearCityDatabase();
});

test('测试1', () => {
  expect(isCity('Vienna')).toBeTruthy();
});
test('测试1', () => {
  expect(isCity('San Juan')).toBeTruthy();
});

无论执行多少个 beforeAll 和 afterAll 被调用一次。

作用域 describe

可以通过 describe 做影响范围的分组。

describe('matching cities to foods', () => {
  // Applies only to tests in this describe block
  beforeEach(() => {
    return initializeFoodDatabase();
  });

  test('Vienna <3 sausage', () => {
    expect(isValidCityFoodPair('Vienna', 'Wiener Schnitzel')).toBe(true);
  });

  test('San Juan <3 plantains', () => {
    expect(isValidCityFoodPair('San Juan', 'Mofongo')).toBe(true);
  });
});

执行顺序

同级别下: beforeAll > beforeEach > afterEach > afterAll

任何情况: describe > test,这里指的优先执行 describe 回调函数,但不会执行回调里的 test 的处理方法。

强大的 Mock 功能

当测试对象具有外部依赖性时,可能想要“模拟它们”。“模拟”是将代码的某些依赖项替换为自己的实现。

测试绕不过 mock,它能带来多个好处:

  • 在开发工作流中,可以使软件开发与测试程序编写并行进行;
  • mock 出边界情况,提高测试覆盖率 (Testing coverage) ;
  • 对于三方受限资源或环境系统不稳定情况下,mock 所需数据,进行测试程序编写;

Jest 中的 mock 功能允许测试代码之间的连接,实现方式有:擦除函数实现捕获对函数的调用捕获调用构造函数后的实例 (new 方式) 、测试运行时的返回值配置等等。

Jest 中,通过 jest.fn 可以生成一个 mock 函数。

1. mock 函数调用

1-1. 获取函数调用信息

下面通过 mock 函数, 模拟 callback 函数。在 callback 被调用时,会捕获函数的相关调用信息。

通过 mockImplementation() 方法可以 mock 实现 (Implementation),在 mock 函数被执行的时候,实现也会被执行。

jest.fn(implementation) 是 jest.fn().mockImplementation(implementation) 的简写。

function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}
// 生成 mock 函数
const mockCallback = jest.fn(x => 42 + x);
// 测试 函数
forEach([0, 1], mockCallback);

// 断言:callback 的调用次数期望是2。
expect(mockCallback.mock.calls.length).toBe(2);

// 断言:callback 在第1次被调用时,第1个入参期望是0。
expect(mockCallback.mock.calls[0][0]).toBe(0);

// 断言:callback 在第2次被调用时,第1个入参期望是1。
expect(mockCallback.mock.calls[1][0]).toBe(1);

// 断言:callback 在第1次被调用时,返回值期望是2。
expect(mockCallback.mock.results[0].value).toBe(42);

所有的 mock 函数都有一个 mock 属性,如上可以获取调用信息。

1-2. 返回值配置

这里允许在测试函数期间,mock 出不同的调用次数下,配置不同的返回值,并支持链式调用,这种风格更加容易阅读和理解。

const myMock = jest.fn();
console.log(myMock()); 
// > undefined

myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);

console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true
1-3. 函数调用和返回值配置共同使用
const filterTestFn = jest.fn();

// mock 出第1次 filterTestFn 被调用时返回 true,第2次被调用时返回 false.
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);

const result = [11, 12].filter(num => filterTestFn(num));

console.log(result);
// > [11]
console.log(filterTestFn.mock.calls[0][0]); // 11
console.log(filterTestFn.mock.calls[0][1]); // 12
1-4. 获取实例
const mockFn = jest.fn();

const a = new mockFn();
const b = new mockFn();

mockFn.mock.instances[0] === a; // true
mockFn.mock.instances[1] === b; // true

2. mock 异步

// 语法糖 🍬
jest.fn().mockImplementation(() => Promise.resolve(value));

// 使用
test('async test1', async () => {
  const asyncMock = jest.fn().mockResolvedValue(43);
  await asyncMock(); // 43
});

// 支持链式调用
test('async test2', async () => {
  const asyncMock = jest
    .fn()
    .mockResolvedValue('default')
    .mockResolvedValueOnce('first call')
    .mockResolvedValueOnce('second call');

  await asyncMock(); // first call
  await asyncMock(); // second call
  await asyncMock(); // default
  await asyncMock(); // default
});

3. mock 模块

可以通过 jest.mock('module_name'),来模拟模块。

// users.js
import axios from 'axios';

class Users {
  static all() {
    return axios.get('/users.json').then(resp => resp.data);
  }
}

export default Users;
// users.test.js
import axios from 'axios';
import Users from './users';

jest.mock('axios'); // 显示调用

test('should fetch users', () => {
  const users = [{name: 'Bob'}];
  const resp = {data: users};
  axios.get.mockResolvedValue(resp);

  // or you could use the following depending on your use case:
  // axios.get.mockImplementation(() => Promise.resolve(resp))

  return Users.all().then(data => expect(data).toEqual(users));
});

常用工具库

1. jest-changed-files

用于校验最后一次 commit 中,哪些文件发生改变。

import {getChangedFilesForRoots} from 'jest-changed-files';

getChangedFilesForRoots(['/path/to/test'], {
  lastCommit: true,
  withAncestor: true,
}).then(files => {
  /*
  {
    repos: [],
    changedFiles: []
  }
  */
});

2. jest-diff

比较两个任意值,并打印美化过的不同的地方。

const diff = require('jest-diff').default;

const a = {a: {b: {c: 5}}};
const b = {a: {b: {c: 6}}};

const result = diff(a, b);

// print diff
console.log(result);

3. jest-docblock

导出 javascript 文件顶部的注释。

const {parseWithComments} = require('jest-docblock');

const code = `
/**
 * This is a sample
 *
 * @flow
 */

 console.log('Hello World!');
`;

const parsed = parseWithComments(code);

// prints an object with two attributes: comments and pragmas.
console.log(parsed);

4. jest-get-type

获取值类型。

const getType = require('jest-get-type');

const array = [1, 2, 3];
const nullValue = null;
const undefinedValue = undefined;

// prints 'array'
console.log(getType(array));
// prints 'null'
console.log(getType(nullValue));
// prints 'undefined'
console.log(getType(undefinedValue));

5. jest-validate

用于处理验证中的错误、警告、废弃等消息,并可以指定用户正确的配置。

const {validate} = require('jest-validate');

const configByUser = {
  transform: '<rootDir>/node_modules/my-custom-transform',
};

const result = validate(configByUser, {
  comment: '  Documentation: http://custom-docs.com',
  exampleConfig: {transform: '<rootDir>/node_modules/babel-jest'},
});

console.log(result);

6. jest-worker

给测试提供多线程能力

// heavy-task.js
module.exports = {
  myHeavyTask: args => {
    // long running CPU intensive task.
  },
};
// main.js
async function main() {
  const worker = new Worker(require.resolve('./heavy-task.js'));

  // run 2 tasks in parallel with different arguments
  const results = await Promise.all([
    worker.myHeavyTask({foo: 'bar'}),
    worker.myHeavyTask({bar: 'foo'}),
  ]);

  console.log(results);
}

main();

7. pretty-format

格式化输出可序列化的 javascript 内建类型。

const {format: prettyFormat} = require('pretty-format');

const val = {object: {}};
val.circularReference = val;
val[Symbol('foo')] = 'foo';
val.map = new Map([['prop', 'value']]);
val.array = [-0, Infinity, NaN];

console.log(prettyFormat(val));
/*
Object {
  "array": Array [
    -0,
    Infinity,
    NaN,
  ],
  "circularReference": [Circular],
  "map": Map {
    "prop" => "value",
  },
  "object": Object {},
  Symbol(foo): "foo",
}
*/