本文是前端开发工具系列之单元测试,主要会讨论利用常见的单元测试工具的原理和使用,保证前端开发时的代码正确性,该系列其他部分请参考这里。


本文后期会随着对自动化测试的探索不定时更新,最终会完成全套的自动化测试。

自动化测试分为三种类型

  • unit test 单元测试是对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
  • integration test 集成测试是对多个模块作为一个整体进行测试。
  • end-to-end 即E2E 端到端测试模拟真实用户的黑盒测试。

关于测试更多,可参考vue和react官方的描述

1 单元测试

单元测试和我们手动测试的逻辑是一样的,即执行某一个单元的逻辑,然后将执行结果和预期结果对比,如果一致则通过测试,否则失败。

一个测试框架通常包含两部分

  • Test Runner 测试容器,自动执行内部的测试代码(包括断言库)
  • Assertion Library 断言库,其中包含很多断言,即判断执行结果和预期是否一致。

比如

test('the best flavor is grapefruit', () => {//测试容器
expect(bestLaCroixFlavor()).toBe('grapefruit');//断言库
});
复制代码

执行结束后会自动将测试结果输出。

每个测试文件被称为test suite,每个具体的单元测试被称为test,即测试用例。

除了对常规的算法进行测试,还可以通过Snapshots测试ui。

前端测试框架这边推荐​​jest​​,也是react和vue官方推荐的测试工具

另外可参考

2 jest

jest和其他前端工具差不多,比如webpack,都可以在命令行或者npm script执行,可选的配置文件,各种钩子函数,用于各种具体实现的api。想来应该可以很容易上手,下面来了解一下。

2.1 基本使用

2.1.1 配置文件

配置文件可选,可通过jest --init进行交互式创建,文件名默认为jest.config.js或者通过--config参数指定。

具体配置选项查看​​这里​​。

配置完了可以在npm scripts中添加"test": "jest",,使用--watch或--watchAll可以在修改文件后自动测试。

2.1.2 代码组织

每次执行jest时,jest会根据​​testMatch​​​和​​testRegex​​的配置会查找相关测试代码,我们这里选择建立一个名为__tests__的文件夹,其中的.js, .jsx, .ts and .tsx文件就会被当作测试文件。

比如我们有一个待测试函数,路径为src/index.js

export const sum=(a,b)=>a+b
复制代码

测试文件__tests__/sum.test.js,其中使用的一些jest方法为全局方法,因此不需要显式引入。

import {sum} from '../src/index'
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
复制代码

执行`yarn test

yarn run v1.17.3
$ jest
PASS __tests__3/sum.test.js
√ adds 1 + 2 to equal 3 (2 ms)

Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.352 s, estimated 2 s
Ran all test suites.
Done in 2.39s.
复制代码

以上就是一个完整的单元测试,是我们测试系统的一个最小单元。

2.2 expect和mathers

当我们写一个具体的测试时,需要两部分进行对比,即使用​​expect​​将待测试结果与表示期望结果的​​matchers​​进行匹配。

最常用的expect语法是expect(value),后面添加.not表示取反。

这里主要讲一下常用matchers,除了以下之外的场景可查看​​expect​​这里的文档。

其实matcher本身就是expect对象的一个方法。

  1. Common Matchers

最简单的方式是精确匹配,其中toBe使用Object.is,如果想检查对象可以使用toEqual,后者递归处理。

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

这里的matchers用来检查各种和布尔有关的值

  • toBeNull 匹配null
  • toBeUndefined 匹配undefined
  • toBeDefined 非undefined
  • toBeTruthy if语句作为true处理的场景
  • toBeFalsy 和上面相反
  1. Numbers

包括用于比较的matchers

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);
});
复制代码

对于浮点类型的,由于误差,要使用

test('adding floating point numbers', () => {
const value = 0.1 + 0.2;
//expect(value).toBe(0.3); This won't work because of rounding error
expect(value).toBeCloseTo(0.3); // This works.
});
复制代码
  1. Strings

可以对字符串匹配正则

test('there is no I in team', () => {
expect('team').not.toMatch(/I/);
});

复制代码
  1. Arrays and iterables

使用toContain检查是否包含某元素

  1. Exceptions

使用toThrow

2.3 测试异步代码

对于异步执行的代码,jest需要知道合适结束,以便去执行下一个测试。

  1. Callbacks

对于以回调执行的异步,不能直接在回调中检查,因为jest执行结束当前代码即表示完成,而回调还未执行。

可在test的回调中添加参数done,当done被调用才算完成

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

fetchData(callback);
});
复制代码
  1. Promises

如果以promise处理异步,jest会等resolved后才处理,如果rejectd直接失败,注意此时要使用return,否则会在promise的数据返回之前结束。

test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
复制代码
test('the fetch fails with an error', () => {
expect.assertions(1);
return fetchData().catch(e => expect(e).toMatch('error'));
});
复制代码

如果test的回调是async函数,也可以结合await处理异步。

2.4 钩子函数

钩子中的异步和其他

  1. 每个测试都执行

使用beforeEach 或 afterEach

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

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

test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});
复制代码
  1. 在本文件所有测试开始之前和全都结束后执行

使用beforeAll and afterAll

3. 作用域

除了上面的全局作用域,还可以使用describe语句创建块作用域,describe语句会比其他全局作用域的test更早执行

2.5 Mock函数

mock函数可以用于清除函数的原来实现、捕获函数调用、捕获构造函数的new调用等。

有两种mock的方法,要么使用test代码创建mock函数,要么使用manual mock覆盖模块依赖。

2.5.1 创建mock函数

可以使用jest.fn()包装一个函数,然后在返回的​​包装后的函数​​的mock属性会包含它被调用的各种状态信息。

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);
复制代码

设置包装后函数的返回值

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模块

用于mock模块时,比如我们要验证一个axios请求

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

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

export default Users;
复制代码

我们没必要真的调用axios,而是使用jest.mock()对该模块的实现进行改写,比如使用mockResolvedValue来mock resolve的值

// 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));
});
复制代码

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
复制代码

自定义matcher

前面通过jest.fn()返回的mockfunc可以进行自定义

2.5.2 Manual Mocks

主要用于模拟数据存取,比如读取本地文件。具体参考​​这里​

2.6 Snapshot

快照用于判断ui是否有变化,比如用于测试react组件,这个方式不会渲染整个app而是会将react tree序列化,(话说对比不同不应该git干的事么)。

比如有一个组件

import React from 'react'

export const List=()=>{
return <span>5555</span>
}
复制代码

对应测试文件

import React from 'react';
import renderer from 'react-test-renderer';
import {List} from '../src/index';

it('renders correctly', () => {
const tree = renderer
.create(<List></List>)
.toJSON();
expect(tree).toMatchSnapshot();
});
复制代码

执行测试后会生成snap文件表示当前一个快照,下次执行时进行对比,如果不同则会报错。如果要更新快照要使用jest --updateSnapshot

2.7 dom操作

jest在node中polyfill了一套浏览器api,可以直接操作dom。

2.8 jest对象

jest对象也是在全局作用域,其中挂载了一些属性和方法,主要包括以下几类

  • Mock Modules
  • Mock functions
  • Mock timers
  • Misc


如果我的博客对你有帮助、如果你喜欢我的博客内容,请 “点赞” “评论” “收藏” 一键三连哦!