Jest 使用语法

3 minute read

Jest 是当下流行的 JavaScript 测试工具。 由Facebook 出品,不仅可用来测试 JavaScript 代码,还包括 React 应用。有以下优点:

  • 零配置

    自动测试 test 文件夹或 .spec.js .test.js 结尾的文件

  • 速度快、沙盒化

    使用 workers 并行化测试提高性能,console.log 的输出可以和测试结果一并打印,沙盒化测试,两次测试不会出现影响冲突。

  • 代码覆盖报告

    jest –coverage 自动输出测试报告

开始

npm install --save-dev jest

package.json 中增加测试命令

{
  "scripts": {
    "test": "jest"
  }
}
// sum.js
export default function(a, b) {
  return a + b;
}

// sum.test.js
import sum from './sum'
test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

匹配器 Matchers

Jest 使用 matchers 来测试各种各样的值是否满足预期。一个测试用例函数test 接收连个参数,用例的描述(string 类型)与一个函数,函数内部包含匹配语句。

如测试值

test('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});

测试对象

test('object assignment', () => {
  const data = {one: 1};
  data['two'] = 2;
  expect(data).toEqual({one: 1, two: 2});
});

主要的 Matcher 种类:

Truthiness

  • .toBeNull() 只匹配 null
  • .toBeUndefined() 只匹配 undefined
  • .toBeDefined() toBeUndefined 的取反
  • .toBeTruthy() 匹配是否等同 if 语句的真
  • .toBeFalsy() 匹配是否等同 if 语句的假

每个断言对应的有 not 修饰, 如 expect(n).not.toBeNull()

Number 数值比较

  • .toBeGreaterThan(3)
  • .toBeGreaterThanOrEqual(3.5)
  • .toBeLessThan(5)
  • .toBeLessThanOrEqual(4.5)

小数比较,由于JS 精度问题使用 .toBeCloseTo()

String

使用 toMatch 匹配是否包含某正则表达式

  • .toMatch(/re/)
  • .not.toMatch(/re/)

Array

是否包含某些元素

expect([1, 2, 3]).toContain(3) // passed

异常

function compileAndroidCode() {
  throw new ConfigError('you are using the wrong JDK');
}

test('compiling android goes as expected', () => {
  expect(compileAndroidCode).toThrow();
  expect(compileAndroidCode).toThrow(ConfigError);

  // 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 运行至代码尾部即结束,此时无法等到异步的回调函数被调用,因此 test 的箭头函数需加上参数done函数

test('the data is peanut butter', done => {
  function callback(data) {
    expect(data).toBe('peanut butter');
    done();
  }

  fetchData(callback);
});

Promise

Jest 将等待 Promise 被 resolve,reject 如果没被捕获,test 将返回fail

test('the data is peanut butter', () => {
  expect.assertions(1);
  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});

在 test 内需将 Promise 对象返回,否则测试会在Promise返回前结束。

expect.assertions(1) 声明断言被调用的次数,由于 Promise 被 resolve test 既通过,因此需使用 assertions 确保断言被调用

test('the fetch fails with an error', () => {
  expect.assertions(1);
  return fetchData().catch(e => expect(e).toMatch('error'));
});

// jest 20.0.0 版本以上增加了resolves 和 rejects 断言
test('the data is peanut butter', () => {
  expect.assertions(1);
  return expect(fetchData()).resolves.toBe('peanut butter');
});

test('the fetch fails with an error', () => {
  expect.assertions(1);
  return expect(fetchData()).rejects.toMatch('error');
});

Async/Await

test 的函数参数使用 async 关键字修饰

test('the data is peanut butter', async () => {
  expect.assertions(1);
  await expect(fetchData()).resolves.toBe('peanut butter');
});

test('the fetch fails with an error', async () => {
  expect.assertions(1);
  await expect(fetchData()).rejects.toMatch('error');
});

预处理和后处理

  • 多次设置 beforeEach AfterEach
  • 一次设置 beforeAll afterAll
    beforeEach(() => {
     // to do something
    })
    

    测试块 describe 语句,包含一个块

    describe('Scoped block', () => {
    beforeEach(() => console.log('2 - beforeEach'));
    afterEach(() => console.log('2 - afterEach'));
    test('', () => console.log('test'));
    });
    

    Mock Functions

    两种方式:

    1. 创建一个 mock function 用于测试
    2. 实现一个人工的 mock 覆盖原模块依赖

jest.fn() 返回一个mock 函数

const mockFunc = jest.fn();

mockFunc.mock.calls.length 函数被调用后,length 增加

mockFunc.mock.calls[0][0] 第一次调用的传入的第一个参数

mock 返回值及函数实现

  • .mockReturnValueOnce 添加一次返回值
  • .mockReturnValue 固定返回值
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

const myMockFn = jest
  .fn(() => 'default')
  .mockImplementationOnce(() => 'first call')
  .mockImplementationOnce(() => 'second call');

console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'

SnapShot Testing

测试 UI 是否发生了非预期变化的工具。抓取 React 树的快照或其它序列化的值来简化测试、分析监控状态变化。

Test renderer

react-test-renderer 包提供了一个 React 渲染器,在不依赖 DOM 或移动原生环境下,将 React 组件渲染为纯 JavaScript 对象。利用它很容易抓出组件的渲染快照,

import renderer from 'react-test-renderer';

({ name }) => <h1> Hello {name} </h1>;
const tree = renderer
    .create(<Hello name="Snapshot testing" />)
    .toJSON();
console.log(tree);
// { type: 'h1',
// props: {},
//  children: [ ' Hello ', 'Snapshot testing', ' ' ] }

// 使用 toMatchSnapshot 匹配组件快照是否发生了变化
it('renders correctly', () => {
  const tree = renderer
    .create(<Hello name="Snapshot testing" />)
    .toJSON();
  expect(tree).toMatchSnapshot();
});

首次执行会生成一份快照文件,生成.snap 文件放于__snapshots__文件夹下,再次执行时会读取快照,进行比对。当组件视图更改时,需手动进行升级,使用命令 jest –updateSnapshot 更新快照,或使用 jest –watch 命令进行watch 交互模式,根据提示进行更新。

enzyme – DOM testing 利器

enzyme 是 Airbnb 发行的测试工具,很方便断言、模拟操作、遍历 React 组件的输出。enzyme 对组件内构成元素的选择、操纵与 jQuery 类似,可以选择元素比对内容、触发事件确认绑定函数是否被调用。

import React from "react";
import Enzyme, { shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";

Enzyme.configure({ adapter: new Adapter() });

function Counter({ value, onDecrement, onIncrement }) {
  return (
    <div>
      <button onClick={onDecrement}>-</button>
      <span>{value}</span>
      <button onClick={onIncrement} />
    </div>
  );
}

function setup(value = 0) {
  const actions = {
    onIncrement: jest.fn(),
    onDecrement: jest.fn()
  };
  const component = shallow(<Counter value={value} {...actions} />);

  return {
    component: component,
    actions: actions,
    buttons: component.find("button"),
    p: component.find("span")
  };
}

describe("Counter component", () => {
  it("should display count", () => {
    const { p } = setup();
    expect(p.text()).toEqual("0");
  });

  it("first button should call onIncrement", () => {
    const { buttons, actions } = setup();
    buttons.at(0).simulate("click");
    expect(actions.onDecrement).toBeCalled();
  });

  it("second button should call onDecrement", () => {
    const { buttons, actions } = setup();
    buttons.at(1).simulate("click");
    expect(actions.onIncrement).toBeCalled();
  });
});