单元测试是软件质量的重要保证。在 Github
上挑选一款软件,单元测试覆盖率是评价软件成熟度的一个重要指标。通常成熟可靠的开源产品都有完善的单元测试,并且覆盖率可以达到 80% 以上。
本章任务
- 搭建Jest环境
- 编写有关Jest的函数
- 引入DOM仿真,完成一个前端页面测试
【task1
】搭建 Jest
环境
- 安装依赖
npm i jest -g
- 根目录创建
add.js
测试文件
文件名:add.js
const add = (a, b) => a + b;
module.exports = add;
- 编写
Jest
测试函数
文件名:src/tests/add.test.js
const add = require("../../add");
describe("测试Add函数", () => {
test("add(1,2) === 3", () => {
expect(add(1, 2)).toBe(3);
});
test("add(1,1) === 2", () => {
expect(add(1, 1)).toBe(2);
});
});
- 运行
jest
测试命令
jest
Jest会自动到项目里面查找所有测试用例文件。
如果你想不想使用全局的jest:
pnpm i jest -D
测试命令需要加上npx
npx jest
- 控制台查看结果
PASS tests/add.test.js
测试Add函数
√ add(1,2) === 3 (1 ms)
√ add(1,1) === 2
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.483 s
Ran all test suites.
至此测试环境可用。
【task2
】使用Jest的Mock模拟无法执行的函数
如果被测试的代码,调用了一个网络请求 API
,比如 axios
,但是那个网络地址并不存在或者没有联网,这个时候应该如何测试呢?
- 安装
axios
开发依赖
pnpm i axios -D
- 根目录创建
fetch.js
测试文件
const axios = require('axios')
exports.getData = () => axios.get('/abc/bcd')
单元测试是针对开发的最小单位展开的测试,通常是函数。遇到函数调用函数的情况,比如 A 函数调用 B 函数,测试的主体是 A 函数,B 函数应该与测试无关,应该孤立 B 函数来测试 A 函数。对于上面的
getData
函数来讲,调用了axios.get
函数,应该模拟一个axios.get
函数来替换掉原有的axios.get
函数。模拟的axios.get
函数不会调用网络请求,只具有根据输入返回相应结果的功能。这个就是Mock函数。单元测试的任务是验证
getData
函数的功能是否正确,而不是axios.get
函数或者网络接口是否正确。
- 编写测试文件
首先使用 jest.mock
创建一个 axios
的 mock 对象。实际上就是创建了一个虚拟的 axios
函数替换原函数。然后通过 mockResolvedValue
定义调用 axios.get
函数的返回值。这个时候再调用getData()
方法的时候 ,函数内部的 axios.get
是虚拟 mock 函数。调用时不会发生真正的网络请求,只会返回预定的结果。
文件名:src/tests/fetch.test.js
const { getData } = require("../fetch");
const axios = require("axios");
jest.mock("axios");
it("fetch", async () => {
// 模拟第一次接收到的数据
axios.get.mockResolvedValueOnce("123");
// 模拟每一次接收到的数据
axios.get.mockResolvedValue("456");
const data1 = await getData();
const data2 = await getData();
expect(data1).toBe("123");
expect(data2).toBe("456");
});
- 执行测试命令
jest
- 控制台查看结果
PASS src/tests/add.test.js
PASS src/tests/fetch.test.js
Test Suites: 2 passed, 2 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 0.707 s, estimated 1 s
Ran all test suites.
jest.fn()
jest.fn()
是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()
会返回undefined
作为返回值。
test('测试jest.fn()调用', () => {
let mockFn = jest.fn();
let result = mockFn(1, 2, 3);
// 断言mockFn的执行后返回undefined
expect(result).toBeUndefined();
// 断言mockFn被调用
expect(mockFn).toBeCalled();
// 断言mockFn被调用了一次
expect(mockFn).toBeCalledTimes(1);
// 断言mockFn传入的参数为1, 2, 3
expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
})
【task3
】测试前端页面
前端程序和纯 JS
的区别在于运行时不同。前端程序运行于浏览器端,会直接调用 Dom
对象。但是 Node
中并没有 Dom
模型。
解决的办法有两个 :
- 将测试用例放到浏览器中运行;
- 用
dom
仿真模拟一个dom
对象。
最佳的选择是后者,因为你的测试程序会放到不同的环境中执行,你不可能要求 CI 服务器中也有浏览器。而且放入浏览器再执行,效率也是一个大问题。
模拟一个 dom
对象需要用到 dom
仿真,常见的有 jsdom
、happydom
等
- 安装依赖
pnpm i jsdom -D
- 配置
jsdom
在 jest 中引入 jsdom
,需要编写一个 jsdom-config.js
文件。
文件名:jsdom-config.js
// jsdom-config.js
const jsdom = require('jsdom') // eslint-disable-line
const { JSDOM } = jsdom
const dom = new JSDOM('<!DOCTYPE html><head/><body></body>', {
url: 'http://localhost/',
referrer: 'https://example.com/',
contentType: 'text/html',
userAgent: 'Mellblomenator/9000',
includeNodeLocations: true,
storageQuota: 10000000,
})
global.window = dom.window
global.document = window.document
global.navigator = window.navigator
- 编写
dom.js
被测文件
函数中创建一个 div 元素。
文件名:dom.js
exports.generateDiv = () => {
const div = document.createElement("div");
div.className = "c1";
document.body.appendChild(div);
};
- 编写
dom
的测试文件
在测试程序中,被测试函数创建了一个 div 元素,接着就可以在 dom 仿真中获取 div 元素了。也可以用断言来判断代码功能是否正常。
文件名:src/tests/dom.test.js
const { generateDiv } = require('../dom')
require('../../jsdom-config')
describe('Dom测试', () => {
test('测试dom操作', () => {
generateDiv()
expect(document.getElementsByClassName('c1').length).toBe(1)
})
})
- 执行
jest
命令
jest
- 控制台查看结果
PASS src/tests/fetch.test.js
PASS src/tests/add.test.js
PASS src/tests/dom.test.js
Test Suites: 3 passed, 3 total
Tests: 5 passed, 5 total
Snapshots: 0 total
Time: 1.647 s
Ran all test suites.
这个就是 dom
测试。前端常用的 Vue
、React
程序也都可以使用这样的方法进行测试。