Jest 前端单元测试框架

二叶草 2020年3月10日20:40:13前端框架评论阅读模式
 

目前缺少单元测试在前端工程中十分常见,揣摩导致这种情况的原因主要有以下两个:

  1. 前端缺陷在功能测试中易于暴露(肉眼即可观察);
  2. 开发人员不了解单元测试方法,缺少实践经验,对单元测试能带来的好处存在怀疑。

缺少单元测试也带了一些常见的问题,比如:

  1. 在项目初期开发时,由于没有单元测试,单个组件无法即时调试,必须等待页面框架代码完成才能调试;
  2. 在调试过程中,对于一些极限值必须要求服务来 Mock 或者增加侵入式的调试代码,这也在一定程度上影响了开发效率;
  3. 在需要重构代码时,更加依赖功能测试,需要浪费较多资源部署测试环境,有时由于牵涉的业务功能点过多,甚至导致开发人员不敢对老代码进行重构。

完善的单元测试除了能解决以上问题,还会带来以下好处:

  1. 为了进行 Mock,通常会要求开发人员对代码进行重构解耦,这在一定程度使的代码结构更加趋于合理;
  2. 单元测试可以给出每项测试的响应时间,合理划分的单元测试有助于定位代码的性能问题;
  3. 单元测试还是一份很好的业务文档,每项测试的描述都可以体现业务逻辑。

综上,我们可以通过完善单元测试来提高代码质量,并在一定程度上提高开发效率。 进行单元测试通常需要使用单元测试框架,常见的 JS 单元测试框架有 mocha(https://mochajs.org/),jesmine(https://jasmine.github.io/),chai(https://www.chaijs.com/)等,本文主要通过官方文档实例讲述 Jest 测试框架的基本用法。

一、Jest 简介

Jest 是 Facebook 开源的一款 JS 单元测试框架,它也是 React 目前使用的单元测试框架。 目前除了 Facebook 外,Twitter、Nytimes、Airbnb 也在使用 Jest。Jest 除了基本的断言和 Mock 功能外,还有快照测试、实时监控模式、覆盖度报告等实用功能。 同时 Jest 几乎不需要做任何配置便可使用。

二、快速起步

通过 npm 添加依赖;

  1. npm install --save-dev jest

写一个 sum 函数,完成 a + b 的计算;

  1. // sum.js
  2. function sum(a, b) {
  3.  return a + b;
  4. }
  5. module.exports = sum;

编写 sum 测试;

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

配置 package.json;

  1. {
  2.  "scripts": {
  3.    "test": "jest"
  4.  }
  5. }

运行测试;

  1. npm test

使用 --watch 参数可以启动一个监控界面,当文件发生变化时,会便会运行相关的测试;

  1. npm test -- --watch

使用 --coverage 参数,测试结束时还会得到一份测试覆盖度报告。

  1. npm test -- --coverage

三、基本测试(匹配器)

Jest 基本测试通过 expect 实现,expect 函数返回一个期望值对象,该对象提供了大量工具方法用于做结果继定,使用十分方便。详细可参见 API 文档(https://jestjs.io/docs/en/expect),下面对需要注意的点做简单描述:

1. 相等判断:toBe 使用 Object.is 来判断相等,toEqual 会递归判断 Object 的每一个字段,对数值来说 toBe 和 toEqual 相同;

  1. test('two plus two is four', () => {
  2.  expect(2 + 2).toBe(4);
  3. });
  4. test('object assignment', () => {
  5.  const data = {one: 1};
  6.  data['two'] = 2;
  7.  expect(data).toEqual({one: 1, two: 2});
  8. });

2. 判断符点数:可使用 toBeCloseTo 来解决 JS 浮点精度带来的问题,如下示例;

  1. test('adding floating point numbers', () => {
  2.  const value = 0.1 + 0.2; // 0.30000000000000004
  3.  expect(value).toBeCloseTo(0.3);  // 测试通过
  4. });

3. 判断异常:使用 toThrow 可以测试某个函数在运行时是否会抛出异常。

  1. function compileAndroidCode() {
  2.  throw new ConfigError('you are using the wrong JDK');
  3. }
  4. test('compiling android goes as expected', () => {
  5.  expect(compileAndroidCode).toThrow();
  6.  expect(compileAndroidCode).toThrow(ConfigError);
  7.  // 可以匹配异常的消息内容,也可以用正则来匹配异常消息内容
  8.  expect(compileAndroidCode).toThrow('you are using the wrong JDK');
  9.  expect(compileAndroidCode).toThrow(/JDK/);
  10. });

四、异步测试

Jest 提供了三种方式来支持异步测试:回调函数,Promise 以及 async 函数,下面举例说明:

1. 回调函数:为 Jest 测试函数的添加一个参数 done,如果 done 在函数体内被调用,那么测试直到 done 被调用才会结束,否则测试函数会在执行结束时立即结束;

  1. // 无效测试示例
  2. test('the data is peanut butter', () => {
  3.  function callback(data) {
  4.    // 以下不会执行
  5.    expect(data).toBe('peanut butter');
  6.  }
  7.  // 异步获取数据
  8.  fetchData(callback);
  9.  // 测试会立即结束。
  10. });
  1. // 有效测试示例
  2. test('the data is peanut butter', done => {
  3.  function callback(data) {
  4.    expect(data).toBe('peanut butter');
  5.    // done被调用时,测试结束
  6.    done();
  7.  }
  8.  fetchData(callback);
  9. });

2. Promise:测试函数可以返回一个 Promise,Jest 会等待其 resolve,如果 reject 测试会直接失败;

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

如果需要测试 reject,可以使用 catch 来捕获异常,还需要使用 expect.assertions 来验证断言被调用的次数,以此保证 resolve 的 promise 返回失败。

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

expect 还提供了 resolves、rejects 匹配器,使用这两个匹配器时必须把断言 return 出来,否则测试会立即结束。

  1. test('the data is peanut butter', () => {
  2.  expect.assertions(1);
  3.  return expect(fetchData()).resolves.toBe('peanut butter');
  4. });
  5. test('the fetch fails with an error', () => {
  6.  expect.assertions(1);
  7.  return expect(fetchData()).rejects.toMatch('error');
  8. });

3. async 函数:使用 async 函数做为测试函数即可,非常简单。

  1. test('the data is peanut butter', async () => {
  2.  expect.assertions(1);
  3.  const data = await fetchData();
  4.  expect(data).toBe('peanut butter');
  5. });
  6. test('the fetch fails with an error', async () => {
  7.  expect.assertions(1);
  8.  try {
  9.    await fetchData();
  10.  } catch (e) {
  11.    expect(e).toMatch('error');
  12.  }
  13. });

五、测试前后逻辑处理

有时我们需要在测试前完成一些准备工作,测试后做一些清理工作,Jest 为这种场景提供了工具函数。

  1. beforeAll:当前文件中所有测试执行前触发,只执行一次;
  2. beforeEach:当前文件中每个测试执行前都会触发;
  3. afterEach: 当前文件中每个测试结束后都会触发;
  4. afterAll: 当前文件中所有测试执行结束后触发,只执行一次。

以上函数都支持异步调用,同上一节中异步测试的用法一致,支持回调函数,Promise 及 async 函数。 上述函数可用在不同的 describe 作用域,以此来完成不同 describe 下的独立逻辑。

示例:

  1. beforeAll(() => console.log('1 - beforeAll'));
  2. afterAll(() => console.log('1 - afterAll'));
  3. beforeEach(() => console.log('1 - beforeEach'));
  4. afterEach(() => console.log('1 - afterEach'));
  5. test('', () => console.log('1 - test'));
  6. describe('Scoped / Nested block', () => {
  7.  beforeAll(() => console.log('2 - beforeAll'));
  8.  afterAll(() => console.log('2 - afterAll'));
  9.  beforeEach(() => console.log('2 - beforeEach'));
  10.  afterEach(() => console.log('2 - afterEach'));
  11.  test('', () => console.log('2 - test'));
  12. });
  13. // 1 - beforeAll
  14. // 1 - beforeEach
  15. // 1 - test
  16. // 1 - afterEach
  17. // 2 - beforeAll
  18. // 1 - beforeEach
  19. // 2 - beforeEach
  20. // 2 - test
  21. // 2 - afterEach
  22. // 1 - afterEach
  23. // 2 - afterAll
  24. // 1 - afterAll

六、Mock 函数

使用 jest.fn 可以得到一个 mock 函数。

该函数可以用来测试代码间的联系,通过其 .mock 字段可以取到被调用的次数、传入的参数、返回值以及作为构造函数时的实例化对象。

  1. const mockCallback = jest.fn(x => 42 + x);
  2. [0, 1].forEach(mockCallback);
  3. // Mock函数被调用两次
  4. expect(mockCallback.mock.calls.length).toBe(2);
  5. // 第一次调用Mock函数时,第一个参数为0
  6. expect(mockCallback.mock.calls[0][0]).toBe(0);
  7. // 第二次调用Mock函数时,第一个参数为1
  8. expect(mockCallback.mock.calls[1][0]).toBe(1);
  9. // 第一次调用Mock函数的返回值为42
  10. expect(mockCallback.mock.results[0].value).toBe(42);
  1. const myMock = jest.fn();
  2. const a = new myMock();
  3. const b = {};
  4. const bound = myMock.bind(b);
  5. bound();
  6. console.log(myMock.mock.instances);
  7. // > [ <a>, <b> ]

Mock 函数还可以通过工具函数模拟返回值。

  1. const myMock = jest.fn();
  2. console.log(myMock());
  3. // > undefined
  4. myMock
  5.  .mockReturnValueOnce(10)
  6.  .mockReturnValueOnce('x')
  7.  .mockReturnValue(true);
  8. console.log(myMock(), myMock(), myMock(), myMock());
  9. // > 10, 'x', true, true

七、模拟模块

使用 jest.mock 可以模拟引入模块,例如需要模拟远端 API 返回值时,可以直接 mock 调用 API 工具类(axios)。 jest.mock 会自动根据被 mock 的模块组织 mock 对象。mock 对象将具有原模块的字段和方法,每个字段和方法都可以通过工具函数 mock 具体逻辑。

下例对 Users 类进行测试,使用 mockResolvedValue 来模拟 axios.get 的返回值:

  1. // users.js
  2. import axios from 'axios';
  3. class Users {
  4.  static all() {
  5.    return axios.get('/users.json').then(resp => resp.data);
  6.  }
  7. }
  8. export default Users;
  1. // users.test.js
  2. import axios from 'axios';
  3. import Users from './users';
  4. jest.mock('axios');
  5. test('should fetch users', () => {
  6.  const resp = {data: [{name: 'Bob'}]};
  7.  axios.get.mockResolvedValue(resp);
  8.  // 也可以使用下行代码来重写get方法的实现
  9.  // axios.get.mockImplementation(() => Promise.resolve(resp))
  10.  return Users.all().then(users => expect(users).toEqual(resp.data));
  11. });

当模块是一个函数时,mock 对象可以通过 mockImplemetation 来模拟函数实现:

  1. // foo.js
  2. module.exports = function() {
  3.  // 省略实现代码
  4. };
  1. // test.js
  2. jest.mock('../foo');
  3. const foo = require('../foo');
  4. // foo是一个mock函数
  5. foo.mockImplementation(() => 42);
  6. foo();
  7. // > 42

Jest 还可以通过重写一个完整的模块文件来 mock 模块,这个文件需要放在被 mock 模块文件同级目录下的 mocks 文件夹下。 对于 nodemodules 下的模块,mocks 需要与 nodemodules 文件夹平级。如下示例,为 node 的 fs 模块和 models/user.js 分别添加 mock 文件。

  1. ├── config
  2. ├── __mocks__
  3. │   └── fs.js
  4. ├── models
  5. │   ├── __mocks__
  6. │   │   └── user.js
  7. │   └── user.js
  8. ├── node_modules
  9. └── views

添加了 mock 文件之后,还需要在测试中调用 jest.mock('moduleName')来告知 Jest 使用 mock 对象。 下面这个示例,通过重写 fs 模块来测试 summarizeFilesInDirectorySync 函数。

  1. // FileSummarizer.js
  2. 'use strict';
  3. const fs = require('fs');
  4. function summarizeFilesInDirectorySync(directory) {
  5.  return fs.readdirSync(directory).map(fileName => ({
  6.    directory,
  7.    fileName,
  8.  }));
  9. }
  10. exports.summarizeFilesInDirectorySync = summarizeFilesInDirectorySync;
  1. // __mocks__/fs.js
  2. 'use strict';
  3. const path = require('path');
  4. // 先生成一个jest自动mock对象
  5. const fs = jest.genMockFromModule('fs');
  6. // 增加一个自定义函数使我们可以在测试中设置mock的fs模块api的返回值
  7. let mockFiles = Object.create(null);
  8. function __setMockFiles(newMockFiles) {
  9.  mockFiles = Object.create(null);
  10.  for (const file in newMockFiles) {
  11.    const dir = path.dirname(file);
  12.    if (!mockFiles[dir]) {
  13.      mockFiles[dir] = [];
  14.    }
  15.    mockFiles[dir].push(path.basename(file));
  16.  }
  17. }
  18. // 重写readdirSync方法,其中的返回值通过__setMockFiles方法来设置
  19. function readdirSync(directoryPath) {
  20.  return mockFiles[directoryPath] || [];
  21. }
  22. fs.__setMockFiles = __setMockFiles;
  23. fs.readdirSync = readdirSync;
  24. module.exports = fs;
  1. // __tests__/FileSummarizer-test.js
  2. 'use strict';
  3. jest.mock('fs');
  4. describe('listFilesInDirectorySync', () => {
  5.  const MOCK_FILE_INFO = {
  6.    '/path/to/file1.js': 'console.log("file1 contents");',
  7.    '/path/to/file2.txt': 'file2 contents',
  8.  };
  9.  beforeEach(() => {
  10.    // 为测试添加mock的文件列表
  11.    require('fs').__setMockFiles(MOCK_FILE_INFO);
  12.  });
  13.  test('includes all files in the directory in the summary', () => {
  14.    const FileSummarizer = require('../FileSummarizer');
  15.    const fileSummary = FileSummarizer.summarizeFilesInDirectorySync(
  16.      '/path/to',
  17.    );
  18.    expect(fileSummary.length).toBe(2);
  19.  });
  20. });

八、快照测试

快照测试是 Jest 内置了一种很有用的测试方式。快照测试可以用于保证界面不出现异常变化。 快照测试的基本原理是,渲染页面然后截图,将得到到截图与样本图片进行对比,以此来检查渲染是否符合预期。 两张图片对比不一致时,也有可能是预期发生了变化,这时就需要更新样本图片。

实际测试中,并不是必须对比图片,样本也可以是一份状态描述的字符串,这时只要对比序列化的字符串便可以验证渲染逻辑。

下例使用快照测试来对比一个 Object:

  1. // plain-object.test.js
  2. test('object equals', () => {
  3.  const user = {
  4.    createdAt: new Date(),
  5.    id: Math.floor(Math.random() * 20),
  6.    name: 'LeBron James',
  7.  };
  8.  expect(user).toMatchSnapshot();
  9. });
  1. // __snapshots__/plain-object.test.js.snap
  2. // Jest Snapshot v1, https://goo.gl/fbAQLP
  3. exports[`object equals 1`] = `
  4. Object {
  5.  "createdAt": 2018-09-28T03:51:32.541Z,
  6.  "id": 15,
  7.  "name": "LeBron James",
  8. }
  9. `;

以上测试会失败,因为 createAt 和 ID 都会和快照中的字段比对不一致;如果期望的 createAt 和 ID 都是不确定的值,我们可以给 toMatchSnapShot 增加参数以匹配任意日期或数字, 这样测试便可以通过了。实际使用时,并不需要手工编写快照样本,Jest 可以在首次执行时可以自动生成快照样本,当测试代码有变化时,还可以通过 jest -u 来更新样本。 生成的样本会放在测试文件同级目录下的 snapshots 文件夹中。

  1. // plain-object.test.js
  2. test('object equals', () => {
  3.  const user = {
  4.    createdAt: new Date(),
  5.    id: Math.floor(Math.random() * 20),
  6.    name: 'LeBron James',
  7.  };
  8.  expect(user).toMatchSnapshot({
  9.    createdAt: expect.any(Date),
  10.    id: expect.any(Number),
  11.  });
  12. });
  1. // Jest Snapshot v1, https://goo.gl/fbAQLP
  2. exports[`object equals 1`] = `
  3. Object {
  4.  "createdAt": Any<Date>,
  5.  "id": Any<Number>,
  6.  "name": "LeBron James",
  7. }
  8. `;

虽然样本文件可以生成并且更新十分简单,但是我们不应该随意的修改样本文件。 当对比出错时,首先要考虑是不是被测试代码问题, 只有确定是期望发生变化时才去更新样本,需要把样本文件与代码同等重视起来。做代码审查时,也需要审查样本文件的变化。

快速测试非常适合用来测试 React 组件的渲染逻辑。 React 提供了测试套件用于创建 React 渲染树,由于将 React 渲染树最终转化为 dom 结构或 native 组件的操作是由 ReactDom/ReactNative 完成的,可以认为这一步是可靠的。 因此只需要对比 React 渲染树的快照样本便可以对渲染逻辑进行验证。示例如下:

(https://github.com/facebook/jest/blob/master/examples/snapshot/__tests__/link.react.test.js)

  1. import React from 'react';
  2. import Link from '../Link.react';
  3. import renderer from 'react-test-renderer';
  4. it('renders correctly', () => {
  5.  const tree = renderer
  6.    .create(<Link page="http://www.facebook.com">Facebook</Link>)
  7.    .toJSON();
  8.  expect(tree).toMatchSnapshot();
  9. });
  1. exports[`renders correctly 1`] = `
  2. <a
  3.  className="normal"
  4.  href="http://www.facebook.com"
  5.  onMouseEnter={[Function]}
  6.  onMouseLeave={[Function]}
  7. >
  8.  Facebook
  9. </a>
  10. `;

使用快照测试验证 Redux 逻辑更加方便,reducer、actionCreator 返回值都是一个单纯的键值对对象,直接生成快照样本然后对比验证十分方便。如下示例:

  1. // reducer.js
  2. export const initialState = {
  3.  error: null,
  4.  isLoading: false,
  5.  data: null
  6. };
  7. export default function loadData(state = initialState, action) {
  8.  switch (action.type) {
  9.    case 'failure':
  10.      return {...state, error: action.error, isLoading: false};
  11.    case 'request':
  12.      return {...state, isLoading: true};
  13.    case 'success':
  14.      return {...state, isLoading: false, data: action.data};
  15.    default:
  16.      return state;
  17.  }
  18. }
  1. // reducer.test.js
  2. import reducer from './reducer';
  3. test('load data success', () => {
  4.  const action = { type: 'success', data: [1, 2, 3]};
  5.  const initState = reducer(undefined, {});
  6.  expect(reducer(initState, action)).toMatchSnapshot();
  7. });
  1. // Jest Snapshot v1, https://goo.gl/fbAQLP
  2. exports[`load data success 1`] = `
  3. Object {
  4.  "data": Array [
  5.    1,
  6.    2,
  7.    3,
  8.  ],
  9.  "error": null,
  10.  "isLoading": false,
  11. }
  12. `;

九、总结

以上内容主要是对 Jest 的主要功能介绍,更详细的文档可以参考 Jest 官网(https://jestjs.io/)。 Jest 是一款优秀的单元测试工具,其内置的快照测试更加简化了一些常见场景测试工作。 Jest 对 React 的支持十分完善,React 本身就是使用 Jest 做单元测试的,仅凭这一点便可使其成为基于 React 前端工程的首选单测框架。 最后祝愿大家在以后的工作中简单快速的完成单元测试,完成高质量工程代码。

本文来源于:Jest 前端单元测试框架-变化吧门户
特别声明:以上文章内容仅代表作者本人观点,不代表变化吧门户观点或立场。如有关于作品内容、版权或其它问题请于作品发表后的30日内与变化吧联系。

  • 赞助本站
  • 微信扫一扫
  • weinxin
  • 加入Q群
  • QQ扫一扫
  • weinxin
二叶草
Go语言接口规则 前端框架

Go语言接口规则

Go语言接口规则 接口是一个或多个方法签名的集合。任何类型的方法集中只要拥有该接口对应的全部方法签名。就表示它 "实现" 了该接口,无须在该类型上显式声明实现了哪个接口。对应方法,是指有相同名称、参数...
Go语言中处理 HTTP 服务器 前端框架

Go语言中处理 HTTP 服务器

1 概述 包 net/http 提供了HTTP服务器端和客户端的实现。本文说明关于服务器端的部分。 快速开始: package main import (   "log"   "net/http" )...

发表评论