REXLNT

Jest

How to test anything

photo by Simone Hutsch on unsplash.com

Quick Start

  1. 安装:npm i -D jest

  2. 编写 JS 函数:

    // sum.js
    function sum(a, b) {
      return a + b;
    }
    module.exports = sum;
  3. 编写 sum.test.js :

    // sum.test.js
    const sum = require('./sum');
    
    test('adds 1 + 2 to equal 3', () => {
      expect(sum(1, 2)).toBe(3);
    });
  4. 配置 package.json:

    {
      "scripts": {
        "test": "jest"
      }
    }
  5. 执行 npm run test,执行测试并输出测试结果:

    PASS  ./sum.test.js
    ✓ adds 1 + 2 to equal 3 (3ms)

Matchers

  • 通用类型:toBe() / toEqual()
  • 真值类型:toBeNull() / toBeUndefined() / toBeDefined() / toBeTruthy() / toBeFalsy()
  • 数值类型:toBeGreaterThan() / toBeGreaterThanOrEqual() / toBeLessThan() / toBeLessOrEqual()
  • 字符类型:toMatch()
  • 数组类型:toContain()
  • 异常类型:toThrow()

Asynchronous

Callback

test('the data is uyun', done => {
    function callback(data) {
        expect(data).toBe('uyun');
        // 如果 done 没有被调用,则测试失败
        done();
    }

    fetchData(callback);
});

Promise

test('the data is uyun', () => {
    // 声明至少有一次断言需要执行,如果未执行则认为本次测试失败
    expect.assertions(1);
    // 返回 Promise,jest 会等待 Promise 执行完成
    return fetchData().then(data => {
        expect(data).toBe('uyun');
    });
});

Async/Await

test('the data is uyun', async () => {
    // 声明至少有一次断言需要执行,如果未执行则认为本次测试失败
    expect.assertions(1);
    const data = await fetchData();
    expect(data).toBe('uyun');
});

Pre/Post Test

默认情况下,before*/after* 函数的作用域是当前测试文件,通过 descripe 函数可以将它们的作用域限制为特定区域内:

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'));
});

Jest 会在执行 test 之前优先执行所有的 describe 函数:

describe('outer', () => {
  console.log('describe outer-a');

  describe('describe inner 1', () => {
    console.log('describe inner 1');
    test('test 1', () => {
      console.log('test for describe inner 1');
      expect(true).toEqual(true);
    });
  });

  console.log('describe outer-b');

  test('test 1', () => {
    console.log('test for describe outer');
    expect(true).toEqual(true);
  });

  describe('describe inner 2', () => {
    console.log('describe inner 2');
    test('test for describe inner 2', () => {
      console.log('test for describe inner 2');
      expect(false).toEqual(false);
    });
  });

  console.log('describe outer-c');
});

// describe outer-a
// describe inner 1
// describe outer-b
// describe inner 2
// describe outer-c
// test for describe inner 1
// test for describe outer
// test for describe inner 2

如果在一堆测试中总有一个测试经常失败,可以尝试将 test 临时修改为 test.only,强制 Jest 只执行这一测试用例:

test.only('this will be the only test that runs', () => {
  expect(true).toBe(false);
});

test('this test will not run', () => {
  expect('A').toBe('A');
});

Mock

通过 jest.fn 生成 mock 函数,函数本身带有 mock 属性:

function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], 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);

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

mock modules:

// 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';

// mock axios
jest.mock('axios');

test('should fetch users', () => {
  const resp = {data: [{name: 'Bob'}]};
  // 返回一个 mock 数据
  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(users => expect(users).toEqual(resp.data));
});

mock 函数或者模块的具体实现有两种方式:

// 方式一:mock 函数实现
const myMockFn = jest.fn(cb => cb(null, true));

myMockFn((err, val) => console.log(val));
// > true

myMockFn((err, val) => console.log(val));
// > true

// 方式二:mock 模块
// foo.js
module.exports = function() {
  // some implementation;
};

// test.js
jest.mock('../foo'); // this happens automatically with automocking

const foo = require('../foo');
// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42

如果需要 mock 的函数具有多种形态,使用 mockImplementationOnce 函数:

const myMockFn = jest
  .fn()
  .mockImplementationOnce(cb => cb(null, true))
  .mockImplementationOnce(cb => cb(null, false));

myMockFn((err, val) => console.log(val));
// > true

myMockFn((err, val) => console.log(val));
// > false

mock this:

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

// is the same as

const otherObj = {
  myMethod: jest.fn(function() {
    return this;
  }),
};

mock function name:

const myMockFn = jest
  .fn()
  .mockReturnValue('default')
  .mockImplementation(scalar => 42 + scalar)
  .mockName('add42');

mock matchers:

// The mock function was called at least once
expect(mockFunc).toBeCalled();

// The mock function was called at least once with the specified args
expect(mockFunc).toBeCalledWith(arg1, arg2);

// The last call to the mock function was called with the specified args
expect(mockFunc).lastCalledWith(arg1, arg2);

// All calls and the name of the mock is written as a snapshot
expect(mockFunc).toMatchSnapshot();

Snapshot

创建测试文件:

import React from 'react';
import Link from '../Link.react';
import renderer from 'react-test-renderer';

it('renders correctly', () => {
  const tree = renderer
    .create(<Link page="http://www.facebook.com">Facebook</Link>)
    .toJSON();
  expect(tree).toMatchSnapshot();
});

生成 snapshot 文件:

exports[`renders correctly 1`] = `
<a
  className="normal"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  Facebook
</a>
`;