SYSTEM: ONLINE
VER. ...
SLEET LOG
SLEET'S LOG
/2024年8月27日/5 MIN READ

React 自动化测试

React 测试 note

Jest 的安装

对于一个不具备单测能力的 React 项目(如 vite),可以通过以下方式安装并配置 Jest

bash
# 安装依赖 npm install --save-dev jest @types/jest @jest/types # 初始化 Jest 配置 npx jest --init # Choose the test environment that will be used for testing: 如果要涉及 dom 的单测就选 yes;只涉及 node 的纯逻辑就选 no # Which provider should be used to instrument code for coverage:选择 babel,因为它可以转 ES5,避免兼容性问题 # 增加 babel 对应配置 npm install --save-dev babel-jest @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript # 安装 ts-node npm install ts-node --save-dev # 如果碰到类似“找不到 jest-environment-jsdom”的报错,就自行安装一下对应的依赖 npm install jest-environment-jsdom --save-dev

在根目录创建一个 babel.config.cjs 用于配置 babel

js
// ./babel.config.cjs module.exports = { presets: [ ["@babel/preset-env", { targets: { node: "current" } }], ["@babel/preset-react", { runtime: "automatic" }], // 自动导入 React,不然后续单测的开发会要求对 React 进行 import。 "@babel/preset-typescript", ], };

配置额外的扩展名识别

因为 jest 不使用 webpack 等打包工具,因此不知道如何加载除了 js/jsx 之外的其他文件拓展名。所以需要加一个转换器

ts
// jest.config.ts export default { // ... other config transform: { // ... "^.+.(js|ts|tsx)$": "<rootDir>/node_modules/babel-jest", }, };

Svg mock 转换

Jest 无法识别 svg。所以要对它进行 mock,返回相同的输出结果

ts
// jest.config.ts export default { // ... other config transform: { // ... "^.+.svg$": "<rootDir>/svg-transform.js", }, }; // ./svg-transform.js export default { process() { return { code: "module.exports = {};" }; }, getCacheKey() { return "svgTransform"; // SVG固定返回这个字符串 }, };

CSS 代理

由于 Jest 本身不知道如何处理不同扩展的文件,我们可以通过配置代理的方式,告知 Jest 将对此对象模拟为导入的 CSS 模块

bash
npm install --save-dev identity-obj-proxy
ts
// jest.config.ts export default { // ... other config moduleNameMapper: { ".(css|less)$": "identity-obj-proxy", // 有使用 sass 需求的话可以把正则换成 ^\.(css|less|sass|scss)$ }, };

React Testing Library 的安装

安装相关依赖:

bash
# @testing-library/jest-dom:用于 dom、样式类型等元素的选取 # @testing-library/react:提供针对 React 的单测渲染能力 # @testing-library/user-event:用于单测场景下事件的模拟。 npm install @testing-library/jest-dom @testing-library/react @testing-library/user-event --save-dev

为了能让 expect 适配 React Testing Library 提供的相关断言,需要全局导入一下 @testing-library/jest-dom

在根目录新建一个 jest-dom-setup.js

js
// jest_dom_setup.js import "@testing-library/jest-dom";

然后将该文件配置到 jest.config.ts 中:

ts
// jest.config.ts export default { // 字段意义:将指定的配置文件,在安装测试框架之后、执行测试代码本身之前运行 setupFilesAfterEnv: ["<rootDir>/jest-dom-setup.js"], };

Jest 断言

一个最简单的单元测试长这样:

tsx
// ./src/App.test.tsx import React from 'react'; import { render, screen } from '@testing-library/react'; import App from './App'; // describe 表示一组分组,其中可以包括多组 test describe("test", () => { // test 用于定义单个的用例 test('renders learn react link', () => { render(<App />); const linkElement = screen.getByText(/learn react/i); expect(linkElement).toBeInTheDocument(); }); }

常见断言场景

基础类型的比较

ts
import React from "react"; test("examples for jest expect", () => { // tobe expect(1 + 1).toBe(2); // not tobe expect(1 + 1).not.toBe(3); // 判断 Boolean expect(true).toBe(true); expect(true).toBeTruthy(); expect(false).toBeFalsy(); // 判断 undefined expect(undefined).toBe(undefined); expect(undefined).not.toBeDefined(); expect(undefined).toBeUndefined(); // 判断函数返回值 const test = () => {}; expect(test()).toBeUndefined(); // 针对浮点数比较,用上面的 api 会出问题。需要使用专门提供的 `toBeCloseTo`,用于判断对象和预期的精度是否足够接近 expect(0.2 + 0.1).toBeCloseTo(0.3); });

引用类型的比较

ts
import React from "react"; test("examples for jest expect", () => { // 对于深拷贝或是属性完全相同的对象,需要使用 toEqual。toEqual 会深度递归对象的每一个属性,进行深度比较。只要原始值相同,就能通过断言 // toEqual 同样可以用于简单类型 const a = { name: "name", age: 1 }; const b = JSON.parse(JSON.stringify(a)); expect(a).not.toBe(b); expect(a).toEqual(b); });

数值比较

ts
import React from "react"; test("examples for jest expect", () => { // > expect(3).toBeGreaterThan(2); // < expect(3).toBeLessThan(4); // >= expect(3).toBeGreaterThanOrEqual(3); // <= expect(3).toBeLessThanOrEqual(3); });

正则匹配

ts
import React from "react"; test("examples for jest expect", () => { // toMatch 会匹配字符串是否能满足正则的验证 expect("regexp validation").toMatch(/regexp/); // toMatchObj 用于验证 value 是否是匹配对象的子集 const obj1 = { props1: "test", props2: "regexp validation" }; const obj2 = { props1: "test" }; // 由于 obj2 是 obj1 的子集,所以验证通过 expect(obj1).toMatchObj(obj2); });

表单验证

ts
import React from "react"; test("examples for jest expect", () => { // toContain 判断某个值是否存在于数组中 expect([1, 2, 3]).toContain(1); // arrayContaining 匹配接收的数组。此处和 toEqual 使用可以用于判定数组 [1, 2] 是否是数组 [1, 2, 3] 的子集 expect([1, 2, 3]).toEqual(expect.arrayContaining([1, 2])); // toContainEqual 判定某个对象元素是否在数组中 expect([{ a: 1, b: 2 }]).toContainEqual({ a: 1, b: 2 }); // toHaveLength 断言数组长度 expect([1, 2, 3]).toHaveLength(3); // toHaveProperty 断言对象中是否包含某个属性 // 针对多层级的对象,可以通过 xx.yy 的方式进行传参断言 const obj = { props1: 1, props2: { props3: 2, }, }; expect(obj).toHaveProperty("props1"); expect(obj).toHaveProperty("props2.props3"); });

错误抛出

ts
import React from "react"; test("examples for jest expect", () => { const throwError = () => { const err = new Error("console err"); throw err; }; expect(throwError).toThrow(); expect(throwError).toThrowError(); const catchError = () => { try { const err = new Error("console err"); throw err; } catch (err) { console.log(err); } }; expect(catchError).not.toThrow(); expect(catchError).not.toThrowError(); });

自定义断言

可以使用 Expect.extend 来自定义断言

同步的匹配器

e.g. 断言一个数字是否在 0-10 之间

ts
test("0-10 sync test", () => { const toBeBetweenZeroAndTen = (num: number) => { if (num >= 0 && num <= 10) { return { message: () => "", pass: true, }; } return { message: () => "expect num to be a number between 0 and 10", pass: false, }; }; expect.extend({ toBeBetweenZeroAndTen, }); expect(3).toBeBetweenZeroAndTen(); expect(13).not.toBeBetweenZeroAndTen(); });

异步的匹配器

ts
test("0-10 async test", async () => { const toBeBetweenZeroAndTen = async (num: number) => { const res = await new Promise<{ message: () => string; pass: boolean }>( (resolve) => { setTimeout(() => { if (num >= 0 && num <= 10) { resolve({ message: () => "", pass: true, }); } else { resolve({ message: () => "expected num to be a number between zero and ten", pass: false, }); } }, 1000); } ); return ( res || { message: () => "expected num to be a number between zero and ten", pass: false, } ); }; expect.extend({ toBeBetweenZeroAndTen, }); await expect(8).toBeBetweenZeroAndTen(); await expect(11).not.toBeBetweenZeroAndTen(); });
Article Index