前端单元测试

221 阅读4分钟

概念

在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试 [来源请求] ,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。能进行单元测试的函数/组件,一定是低耦合的,这也从一定程度上保证了我们的代码质量。通过 given-when-then 的结构,可以让你写出比较清晰的测试结构,既易于阅读,也易于编写。

为什么要做单元测试

单元测试可以让开发工作更加高效,单元测试适合的场景是敏捷开发。敏捷迭代开发最重要的就是要快,快速迭代、持续交付用户价值,而没有单元测试很难快起来。因为每次开发、发布都需要投入人力测试,并且随着项目迭代和复杂度会上升,也需要我们定期重构,而没有单元测试也不敢轻易重构,这样下去代码会变成“屎山”,代码越来越烂,写的就越来越慢,就会出现更多的 bug,而为了快速修复 bug 又不敢重构,进一步导致代码变烂,形成恶性循环。在这样的场景中,单元测试就是很有必要的。

Jest

Jest 是一款由 Facebook 提供的 JS 测试框架,支持 Babel、TS、Node、React、Angular、Vue 等诸多前端框架。

测试的执行方法

test('test unit name', () => {
  // 执行测试代码
});
// or
it('test unit name', () => {
  // 执行测试代码
});

执行断言的方法

test('test unit name', () => {
  expect() 
  // expect.匹配器
})

常用匹配器

toBe // 值匹配
toEqual // 对象、数组的深度匹配
toStrictEqual // 更严格的匹配
not // 不匹配,后边可以跟其他匹配符
toBeTruthy // true 匹配
toBeFalsy // false 匹配
toBeNull // null 匹配
toBeNaN // NaN 匹配
toBeCloseTo // 浮点数运算匹配
toHaveBeenCalled // 是否执行回调匹配
toThrow // 异常匹配

测试函数

export const sum = (a, b) => a + b;

test("1+2=3", () => {
  expect(sum(1, 2)).toBe(3);
})

测试回调

const map = (arr, callback) => arr.map(callback);

test("map [1,2,3] 回调函数执行3次", () => {
  const mockFn = jest.fn((x) => x * 2);
  map([1, 2, 3], mockFn);
  // 获取执行次数
  expect(mockFn.mock.calls.length).toBe(3);
});

test("map [1,2,3] 回调函数返回 2,4,6", () => {
  const mockFn = jest.fn((x) => x * 2);
  map([1, 2, 3], mockFn);
  expect(mockFn.mock.results[0].value).toBe(2);
  expect(mockFn.mock.results[1].value).toBe(4);
  expect(mockFn.mock.results[2].value).toBe(6);
});

测试异步

const fetchData = () =>
  new Promise((resolve) => {
    setTimeout(() => {
      resolve("result");
    }, 300);
  });

test("返回值为 result", async () => {
  const data = await fetchData();
  expect(data).toBe("result");
});

模块间的依赖

绝大部分项目中都有模块的依赖,Jest 中可以使用 Fake/Stub/Mock/Spy 等方式去处理模块间的依赖关系。

// 替代整个模块, 可以用mock的数据代替模块返回的内容,从而保证我们的期望
import player from "./player";

const mockPlayFile = jest.fn();

jest.mock("./player", () => {
  return jest.fn().mockImplementation(() => {
    return { playFile: mockPlayFile };
  });
});

查看测试覆盖率

jest --coverage

在项目中集成 Jest

1. 创建项目 & 安装依赖
## 使用 vite 模板创建 react-ts 项目
yarn create vite react-jest  --template react-ts

## 安装 jest
yarn add jest @types/jest -D

## 使用 babel 转译
yarn add babel-jest @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript -D
## 或者使用 ts-jest
yarn add ts-jest ts-node -D

## 安装测试 UI、Event 等内容的工具
yarn add react-test-renderer @testing-library/jest-dom @testing-library/react @testing-library/user-event -D

## mock 以及 eslint 工具
yarn add identity-obj-proxy -D
yarn add eslint-plugin-jest -D

## 解决 environment 报错
yarn add jest-environment-jsdom -D
2. 创建测试目录
├── __test__          
│   ├── __mock__
|   |   └── fileMock.js
│   └── App.test.tsx         
└── src
3. 配置文件
// package.json 添加脚本命令
{
  "scripts": {
    "test": "jest"
  }
}
// jest.config.ts 配置 jest
import type { JestConfigWithTsJest } from "ts-jest";

const jestConfig: JestConfigWithTsJest = {
  // 环境
  testEnvironment: "jsdom", 
  // 是否生成测试覆盖率
  collectCoverage: true,
  // 匹配的文件
  transform: {
    "^.+\\.tsx?$": "ts-jest",
  },
  // 模块映射
  moduleNameMapper: {
    "\\.(gif|ttf|eot|svg|png)$": "<rootDir>/__test__/__mocks__/fileMock.js",
    "\\.(css|less|sass|scss)$": "identity-obj-proxy",
  },
};

export default jestConfig;
// fileMock.js 添加资源文件的 mock
module.exports = "test-file-stub";
4. 运行测试

举个简单的例子:

// App.test.tsx 用到了 testing-library 测试库
import { render, screen } from "@testing-library/react";
import user from "@testing-library/user-event";
import App from "../src/App";

test("正确显示button的内容", async () => {
  // 渲染
  render(<App />);

  // 获取页面元素
  const buttonCount = await screen.findByRole("button");
  expect(buttonCount.innerHTML).toBe("count is 0");

  // 模拟用户点击行为
  await user.click(buttonCount);
  expect(buttonCount.innerHTML).toBe("count is 1");
});
yarn test

$ jest
 PASS  __test__/App.test.tsx
  ✓ 正确显示button的内容 (112 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |     100 |      100 |     100 |     100 |                   
 App.tsx  |     100 |      100 |     100 |     100 |                   
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.532 s, estimated 4 s
Ran all test suites.
✨  Done in 5.80s.

Mocha

Mocha 是比较灵活成熟的 JavaScript 单元测试框架,由于没有内部集成,所以可以自主选择断言库和侦听库。另外 Mocha 独特的功能是它不止可以在 Node.js 里运行测试,还可以在浏览器里运行测试。

Jasmine

Jasmine 是 Jest 的底层库,主攻 BDD(Behavior-Driven development,即行为驱动开发) 断言库与异步测试的自动化测试框架,没有外部依赖。运行在 Node.js 上,没有外部库,所以可以兼容所有的框架和库,更加灵活,配置过程也更加繁琐。

其他

除了以上的几种还有 AVATape 等单元测试库,Testing Library 等组件测试库以及其他测试工具。