# Jest

Jest 是一款优雅、简洁的 JavaScript 测试框架。同时 Jest 支持 Babel (opens new window)TypeScript (opens new window)Node (opens new window)React (opens new window)Angular (opens new window)Vue (opens new window) 等诸多框架。

# 安装

$ npm install jest -D
1

添加 test script

// package.json
{
  "scripts": {
    "test": "jest"
  }
}
1
2
3
4
5
6

使用 babel

$ npm install babel-jest @babel/core @babel/preset-env -D
1

新建 babel.config.js文件

module.exports = {
  presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
};
1
2
3

# Jest 的基本流程

Jest 使用匹配器进行测试,例如 toBe().

test("adds 1 + 2 to equal 3", () => {
  expect(sum(1, 2)).toBe(3);
});
1
2
3

Jest 支持很多匹配器,详情请参考 Expect (opens new window).

Jest 还可以使用 describe() 将多个相关的测试组合在一起,例如:

describe('math', () => {
  test('add', () => {
    expect(1 + 2).toBe(3);
  });

  test('subtract', () => {
    expect(3 - 1).toBe(2);
  });
});
1
2
3
4
5
6
7
8
9

# 自定义匹配器

可以使用 expect.extend(matchers) (opens new window) 创建自定义匹配器。

匹配器返回一个对象:{ pass: boolean, message: () => string }pass 确定是否匹配,message 返回错误信息。

passtrue 时,message 应该返回 expect(x).not.yourMatcher() 失败时的错误信息;

passfalse 时,message 应该返回 expect(x).yourMatcher() 失败时的错误信息。

例如,创建一个匹配范围的自定义匹配器 toBeWithinRange

import {expect} from '@jest/globals';

function toBeWithinRange(actual, floor, ceiling) {
  if (
    typeof actual !== 'number' ||
    typeof floor !== 'number' ||
    typeof ceiling !== 'number'
  ) {
    throw new Error('These must be of type number!');
  }

  const pass = actual >= floor && actual <= ceiling;
  const message = () => {
    return pass
      ? `expected ${this.utils.printReceived(
          actual
        )} not to be within range ${this.utils.printExpected(
          `${floor} - ${ceiling}`
        )}`
      : `expected ${this.utils.printReceived(
          actual
        )} to be within range ${this.utils.printExpected(
          `${floor} - ${ceiling}`
        )}`;
  };
  return { pass, message };
}

expect.extend({
  toBeWithinRange,
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import '../toBeWithinRange';

test('is within range', () => expect(100).toBeWithinRange(90, 110));
test('is NOT within range', () => expect(101).not.toBeWithinRange(0, 100));
test('asymmetric ranges', () => {
  expect({apples: 6, bananas: 3}).toEqual({
    apples: expect.toBeWithinRange(1, 10),
    bananas: expect.not.toBeWithinRange(11, 20),
  });
});
1
2
3
4
5
6
7
8
9
10

如果不想每个 test 文件都引入 toBeWithinRange,可以将 expect.extend 移入 setupFilesAfterEnv (opens new window) 脚本中。

自定义匹配器也支持异步,返回 { pass: boolean, message: string } promise。

# 测试异步代码

Jest 也能测试异步代码,有三种方法:

# 返回 Promise

通过在测试里返回 promise,Jest 会等待 promise 完成,然后进行相应的测试。

test("test promise that resolved by returning promise", () => {
  return fetchData().then(data => {
    expect(data).toBe("data");
  });
});

test("test promise that rejected by returning promise", () => {
  expect.assertions(1);
  return fetchDataWithError().catch(error => {
    expect(error).toBe("error");
  });
});
1
2
3
4
5
6
7
8
9
10
11
12

当测试 promise 应该 rejected 时,需要设置 expect.assertions(1),否则当 promise resolved 的时候,测试是通过的。

# Async/Await

Jest 也可以使用 async/await 测试异步代码。

test("test promise that resolved with async/await", async () => {
  const data = await fetchData();
  expect(data).toBe("data");
});

test("test promise that rejected by async/await", async () => {
  expect.assertions(1);
  try {
    await fetchDataWithError();
  } catch (error) {
    expect(error).toBe("error");
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13

同样当测试 promise 应该 rejected 时,需要设置 expect.assertions(1).

# resolves / rejects 修饰符

结合 async/await.resolves / .rejects 修饰符,可以很方便的测试异步代码。

test("test promise that resolved by `resolves` modifier", async () => {
  await expect(fetchData()).resolves.toBe("data");
});

test("test promise that rejected with `rejects` modifier", async () => {
  await expect(fetchDataWithError()).rejects.toBe("error");
});
1
2
3
4
5
6
7

# 回调

Jest 通过提供 done 参数来测试 callback.

test("callback", done => {
  function callback(error, data) {
    if (error) {
      done(error);
      return;
    }
    try {
      expect(data).toBe("data");
      done();
    } catch (error) {
      done(error);
    }
  }
  fetchDataWithCallback(callback);
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Jest 会等 done 回调函数被调用执行结束后,再结束测试。

# 安装和移除

写测试的时候需要在测试前做一些准备工作,Jest 提供了 beforeEachbeforeAll 辅助函数,

相应地需要在测试后进行一些收尾工作,Jest 提供了 afterEachafterAll 辅助函数。

执行顺序如下:

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', () => {
  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'));
});

// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

当一个文件有多个 beforeEach,以它们定义的顺序执行;

当一个文件有多个 afterEach 时,以它们定义的顺序反向执行。

# 模拟函数

模拟函数允许你通过擦除函数的实际实现来测试代码之间的链接。

# 使用 Mock 函数

Jest 通过 jest.fn 定义一个 mock 函数,来代替真正的函数,然后可以对这个 mock 函数进行测试,比如测试被调用的次数、调用时的传入的参数、函数的返回值等等。

const mockCallback = jest.fn(x => 42 + x);
test('forEach mock function', () => {
  forEach([0, 1], mockCallback);

  // The mock function was called twice
  expect(mockCallback.mock.calls).toHaveLength(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);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

mock 函数有一个 mock 属性,定义了很多与函数调用相关的属性,详情请参考 Mock Functions (opens new window).

# 模拟实现

通过 jest.mockImplementationjest.mockImplementationOnce 方法来替换函数的实现。

jest.fn(x => 42 + x) 等价于 jest.fn().mockImplementation(x => 42 + x)

const foo = jest
  .fn(() => "never call")
  .mockImplementationOnce(() => "first call")
  .mockImplementationOnce(() => "second call")
  .mockImplementation(() => "default");
test("mock implementation", () => {
  expect(foo()).toBe("first call");
  expect(foo()).toBe("second call");
  expect(foo()).toBe("default");
  expect(foo()).toBe("default");
});
1
2
3
4
5
6
7
8
9
10
11

# 模拟返回值

如果 mock 函数只是返回一个值,可以使用 jest.mockReturnValuejest.mockReturnValueOnce 代替

const fn = jest.mockReturnValue(10)
// 等价于
// const fn = jest.fn(() => 10);
// const fn = jest.mockImplementation(() => 10);
1
2
3
4

# 模拟返回 this

有时候为了方便级联调用,需要返回 this,可以使用 jest.mockReturnThis

const myObj = {
  myMethod: jest.fn().mockReturnThis(),
};

/*
等价于
const myObj = {
  myMethod: jest.fn(function () {
    return this;
  }),
};
*/
1
2
3
4
5
6
7
8
9
10
11
12

# 模拟模块

# 模拟整个模块

Jest 可以 mock 整个模块,例如下面的代码 mock axios 模块




 












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

jest.mock('axios');

test('should fetch users', () => {
  const users = [{name: 'Bob'}];
  const resp = {data: users};
  
  // 或者使用 `mockImplementation`
  // axios.get.mockImplementation(() => Promise.resolve(resp))
  axios.get.mockResolvedValue(resp);
 
  return Users.all().then(data => expect(data).toEqual(users));
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 模拟部分模块

Jest 也可以 mock 模块的一部分,例如

// foo-bar-baz.js
export const foo = "foo";
export const bar = () => "bar";
export default () => "baz";
1
2
3
4

然后我们可以使用 jest.mock(moduleName, factory) 模拟 foo-bar-baz的部分实现,

关键点是使用 jest.requireActual 获取模块原来的定义,然后覆盖部分实现。



 
 
 
 
 
 
 
 
 
 










import defaultExport, {bar, foo} from './foo-bar-baz';

jest.mock('../foo-bar-baz', () => {
  const originalModule = jest.requireActual('../foo-bar-baz');

  return {
    __esModule: true, // Use it when dealing with esModules
    ...originalModule,
    default: jest.fn(() => 'mocked baz'),
    foo: 'mocked foo',
  };
});

test('should do a partial mock', () => {
  const defaultExportResult = defaultExport();
  expect(defaultExportResult).toBe('mocked baz');
  expect(defaultExport).toHaveBeenCalled();

  expect(foo).toBe('mocked foo');
  expect(bar()).toBe('bar');
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 手动模拟

# 模拟用户模块

手动模拟自定义模块,需要在模块同级目录下创建一个 __mocks__ 目录,然后在这个目录下新建模块同名文件。比如要手动模拟 modules/user.js,需要创建 modules/__mocks__/user.js

// modules/__mocks__/user.js
const user = jest.createMockFromModule("../user.js");
user.upperName = jest.fn(() => "MOCK")
export default user;
1
2
3
4

然后在测试的时候,调用 jest.mock('./moduleName'),调用模块的 mock 实现。

import user from "../src/modules/user"
jest.mock('../src/modules/user');

test("manual mock", () => {
  expect(user.upperName("anything")).toBe("MOCK");
});
1
2
3
4
5
6

automock 打开时,即使没有调用 jest.mock('./moduleName'),在测试时使用的也是模块的 mock 实现。如果不想要 mock 实现,则需要明确调用 jest.unmock('./moduleName')

# 模拟 Node 模块

Jest 除了可以模拟自定义模块,还可以模拟第三方 node modules。在 node_modules 同级目录下创建 __mocks__ 目录,然后在这个目录下新建模块同名文件,比如要手动模拟 axios,创建 projectName/__mocks__/axios.js

如果是 scoped modules,例如:模拟 @scope/project-name 模块,则创建 __mocks__/@scope/project-name.js 文件。

在测试的时候,不需要像模拟自定义模块一样,调用 jest.mock('axios')。但是如果是模拟 Node 的内置模块(例如:fspath),则需要显式调用 jest.mock("path"),因为默认情况下内置模块不会被模拟。

# 模拟模块的一个方法

如果只想模拟模块的一个方法,有下面三种方式:

# 手动模拟

引入 Module,然后 mock 掉想要 mock 的方法

import * as moduleApi from '@module/api';

moduleApi.functionToMock = jest.fn().mockReturnValue({ someObjectProperty: 42 });
1
2
3

# jest.spyOn(object, methodName)

jest.spyOn (opens new window) 创建一个类似于 jest.fn 的模拟函数,但也跟踪调用 object[methodName]

import * as moduleApi from '@module/api';

jest.spyOn(moduleApi, 'functionToMock').mockReturnValue({ someObjectProperty: 42 });
1
2
3

# jest.requireActual(moduleName)

通过在 jest.mock(path, moduleFactory) 里调用 jest.requireActual(moduleName),取得模块的真实实现,然后 mock 掉想要 mock 的方法。

import { functionToMock } from "@module/api"; 

jest.mock("@module/api", () => {
    const original = jest.requireActual("@module/api");
    return {
        ...original,
        functionToMock: jest.fn()
    };
});
1
2
3
4
5
6
7
8
9

# 模拟 ES6 类

ES6 类其实就是一个带有语法糖的构造函数,因此可以使用 mock 函数来模拟 ES6 类。有四种方式:

// sound-player.js
export default class SoundPlayer {
  constructor() {
    this.foo = 'bar';
  }

  playSoundFile(fileName) {
    console.log('Playing sound file ' + fileName);
  }
}

// sound-player-consumer.js
import SoundPlayer from './sound-player';
export default class SoundPlayerConsumer {
  constructor() {
    this.soundPlayer = new SoundPlayer();
  }

  playSomethingCool() {
    const coolSoundFileName = 'song.mp3';
    this.soundPlayer.playSoundFile(coolSoundFileName);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 自动模拟

调用 jest.mock('./sound-player'),将返回一个自动模拟(automatic mock)的版本 ,它将 ES6 类替换为模拟构造函数,并将其所有方法替换为总是返回 undefined 的模拟函数。

# 手动模拟

__mocks__ 目录下创建手动 mock

// __mocks__/sound-player.js
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
  return {playSoundFile: mockPlaySoundFile};
});

export default mock;
1
2
3
4
5
6
7

或者另一个 class

// __mocks__/sound-player.js
export default class SoundPlayer {
  constructor() {
    console.log('Mock SoundPlayer: constructor was called');
  }

  playSoundFile() {
    console.log('Mock SoundPlayer: playSoundFile was called');
  }
}
1
2
3
4
5
6
7
8
9
10

# jest.mock(path, moduleFactory)

jest.mock(path, moduleFactory) 接受一个模块工厂参数。模块工厂是一个返回模拟的函数。

import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
  return jest.fn().mockImplementation(() => {
    return {playSoundFile: mockPlaySoundFile};
  });
});
1
2
3
4
5
6
7

由于对 Jest .mock() 的调用被提升到文件的顶部,因此 Jest 阻止对范围外变量的访问。默认情况下,不能先定义变量,然后在工厂中使用它。但是 Jest 将对以 mock 开头的变量禁用此类检查。然而,它仍然是由您来保证他们将被初始化的时间。

# mockImplementation()

您可以通过在现有模拟上调用 mockImplementation() 来替换上述所有模拟,以便更改单个测试或所有测试的实现。

import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player');

describe('When SoundPlayer throws an error', () => {
  beforeAll(() => {
    SoundPlayer.mockImplementation(() => {
      return {
        playSoundFile: () => {
          throw new Error('Test error');
        },
      };
    });
  });

  it('Should throw an error when calling playSomethingCool', () => {
    const soundPlayerConsumer = new SoundPlayerConsumer();
    expect(() => soundPlayerConsumer.playSomethingCool()).toThrow();
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 模拟类的某个方法

可以使用 spyOn mock 类的某个方法

import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';

const playSoundFileMock = jest
  .spyOn(SoundPlayer.prototype, 'playSoundFile')
  .mockImplementation(() => {
    console.log('mocked function');
  });

it('player consumer plays music', () => {
  const player = new SoundPlayerConsumer();
  player.playSomethingCool();
  expect(playSoundFileMock).toHaveBeenCalled();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

类的静态方法、getter/setter 方法也可以使用这种方式

import SoundPlayer from './sound-player';

const staticMethodMock = jest
  .spyOn(SoundPlayer, 'brand') // 静态方法
  .mockImplementation(() => 'some-mocked-brand');

const getterMethodMock = jest
  .spyOn(SoundPlayer.prototype, 'foo', 'get') // getter 方法
  .mockImplementation(() => 'some-mocked-result');

it('custom methods are called', () => {
  const player = new SoundPlayer();
  const foo = player.foo;
  const brand = SoundPlayer.brand();

  expect(staticMethodMock).toHaveBeenCalled();
  expect(getterMethodMock).toHaveBeenCalled();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# Demo

jest-demo (opens new window)

# References