前端自动化测试概述
0x01 概述
- 在敏捷迭代开发中,测试能够及时暴露问题并修正,从而降低需求不匹配风险,更快体现产品价值
- 测试能够提高开发效率,降低代码复杂度,使代码易于重构,结构也更加完善
- 重构:在不改变软件可观测行为的前提下,改善代码内部设计或实现
- 测试类型:(成本逐渐升高、速度逐渐变慢、距离用户更近)
- 单元测试(unit test)
- 集成测试(integration test)
- 端到端测试(e2e test)
- 实际意义:
- 安全重构已有代码
- 快速回归已有功能
- 保存业务上下文
0x02 基础单元测试
(1)概述
- 官网:https://jestjs.io/zh-Hans/
- Jest 由 Meta(原 Facebook)开发,现由 OpenJS 管理
- 特点:快速、开箱即用、守护模式、快照测试
- 通过命令
jest启用,常用选项:--watch:实时监听测试变化--coverage:查看测试覆盖度
(2)基础使用
-
使用命令
npm init -y快速创建一个 NodeJS 环境 -
使用命令
npm install --save-dev jest安装 Jest 到开发依赖 -
修改 package.json
"scripts": { "test": "jest" }, -
创建目录 src 用于存放代码文件
-
创建并编写以下文件:
-
src\calc.js
module.exports = { add: (a, b) => { return a + b; }, }; -
src\calc.test.js
/** * 测试计算模块的核心功能 * * 该测试套件主要验证计算模块中基础算术功能的正确性 */ const { add } = require("./calc"); // 主测试套件:涵盖计算模块的基础运算验证 describe("计算模块", () => { // 测试用例:验证加法运算的基础功能 // 预期行为:正确执行两个数字的加法运算并返回合法结果 test("计算1+2", () => { // 验证加法函数对整数参数的处理 // Given const number = 1; const anotherNumber = 2; // When const result = add(number, anotherNumber); // Then expect(result).toBe(3); }); });说明 Given Arrange 准备测试数据
可以抽取到 beforeEach 或 mock 模块When Act 采取行动
调用相应的模块,执行对应的函数或组件渲染方法Then Assert 断言
借助 Matchers 的能力,Jest 可以扩展自定义 Matcher
-
-
使用命令
npm run test执行测试
(3)常见 Matcher
-
.toBe():严格相等,即=== -
.toEqual():相等,即==const obj1 = { name: "obj", }; const obj2 = { name: "obj", }; describe("等于判断", () => { test("toBe", () => { expect(obj1).toBe(obj2); // failed }); test("toEqual", () => { expect(obj1).toEqual(obj2); // passed }); }); -
.toBeFalsy():判断布尔值是否为 falsedescribe("布尔值判断", () => { test("true", () => { expect(true).toBeFalsy(); // failed }); test("false", () => { expect(false).toBeFalsy(); // passed }); }); -
.toHaveLength():判断数组长度test("长度是否为3", () => { expect([1, 2, 3]).toHaveLength(3); }); -
.toHaveBeenCalled():判断当前方法是否被调用 -
.toHaveBeenCalledTimes():判断当前方法被调用次数const mockFn = jest.fn(); describe("调用判断", () => { test("是否被调用", () => { expect(mockFn).toHaveBeenCalled(); // failed mockFn(1, 2, 3); expect(mockFn).toHaveBeenCalled(); // passed }); test("调用次数", () => { mockFn(1, 2, 3); expect(mockFn).toHaveBeenCalledTimes(1); // passed expect(mockFn).toHaveBeenCalledTimes(2); // failed }); }); -
.toThrow():判断抛出的异常const myError = new Error("myError"); test("异常为 myError", () => { mockFn.mockImplementation(() => { throw myError; }); expect(mockFn).toThrow(myError); }); -
.toMatchSnapshot():捕获组件或对象的输出,并将其存储为快照文件,判断此次快照与上次快照是否匹配 -
.toMatchInlineSnapshot():捕获组件或对象的输出,并将其内联存储到测试文件,判断此次快照与上次快照是否匹配describe("快照判断", () => { test("默认", () => { expect(1 + 1).toMatchSnapshot(); }); test("内联", () => { expect(1 + 1).toMatchInlineSnapshot(`2`); }); }); -
.extend():扩展自定义 Matchertest("加一的结果是否大于 0", () => { expect.extend({ toBeGreaterThanZero(received) { const pass = received + 1 > 0; if (pass) { return { message: () => `expected ${received} not to be greater than 0`, pass: true, }; } return { message: () => `expected ${received} to be greater than 0`, pass: false, }; }, }); expect(0).toBeGreaterThanZero(); // passed expect(-1).toBeGreaterThanZero(); // failed });
(4)模块间依赖
- 测试单元分类:
- 社交型:需要依赖其他模块来实现功能的测试模块
- 独立型:无需依赖其他模块来实现功能的测试模块
- Mock:用于实现代替外部模块
Stub:用于模拟特定行为
Spy:用于监听模块行为 - 易测性:模块职责越单一,单元测试越方便,体现可维护性
Mock
-
src\service.js
module.exports = { getNames() { return ["Alex", "Bob", "Charles"]; }, }; -
src\app.js
const service = require("./service"); module.exports = { searchNames: (term) => { const matches = service.getNames().filter((name) => name.includes(term)); return matches.length > 3 ? matches.slice(0, 3) : matches; }, }; -
src\app.test.js
const { searchNames } = require("./app"); test("empty", () => { const keyword = "David"; const result = searchNames(keyword); expect(result).toEqual([]); }); test("Alex and Charles", () => { const keyword = "e"; const result = searchNames(keyword); expect(result).toEqual(["Alex", "Charles"]); }); -
引入 Mock 替代 Service 模块
const { searchNames } = require("./app"); jest.mock("./service", () => ({ getNames: jest.fn(() => ["David"]), })); test("David", () => { const keyword = "David"; const result = searchNames(keyword); expect(result).toEqual(["David"]); }); test("empty", () => { const keyword = "Alex"; const result = searchNames(keyword); expect(result).toEqual([]); }); -
分情况实现 Mock
const { searchNames } = require("./app"); const { getNames } = require("./service"); jest.mock("./service", () => ({ getNames: jest.fn(), })); test("David", () => { const keyword = "David"; getNames.mockImplementation(() => ["David"]); const result = searchNames(keyword); expect(result).toEqual(["David"]); }); test("empty", () => { const keyword = "Alex"; getNames.mockImplementation(() => ["Alice"]); const result = searchNames(keyword); expect(result).toEqual([]); }); -
异常情况 Mock
const { searchNames } = require("./app"); const { getNames } = require("./service"); jest.mock("./service", () => ({ getNames: jest.fn(), })); test("undefined and null", () => { getNames.mockImplementation(() => []); expect(searchNames(undefined)).toEqual([]); expect(searchNames(null)).toEqual([]); }); test("UPPERCASE", () => { const keyword = "alex"; getNames.mockImplementation(() => ["Alex"]); const result = searchNames(keyword); expect(result).toEqual([]); });
快照测试
基于上述 Mock 中的文件
-
src\app.test.js
const { searchNames } = require("./app"); test("undefined and null", () => { expect(searchNames("David")).toMatchInlineSnapshot(); }); -
运行测试后,
.toMatchInlineSnapshot()的参数值会自动替换成相应的结果const { searchNames } = require("./app"); test("undefined and null", () => { expect(searchNames("David")).toMatchInlineSnapshot(`[]`); });
.toMatchSnapshot()运行后会生成一个文件,相比.toMatchInlineSnapshot()可读性较差
0x03 UI 单元测试
- 前端组件化极大地方便了对 UI 的单元测试
(1)Testing Library
-
Testing Library 可用于实现 UI 单元测试,适用于当前主流前端框架,如 Vue、React 等
-
查询组件渲染元素
方法 无结果 唯一结果 多个结果 是否异步 getBy 报错 返回 报错 findBy 报错 返回 报错 是 queryBy 返回 null返回 报错 getAllBy 报错 数组 数组 findAllBy 报错 数组 数组 是 queryAllBy 空数组 []数组 数组 -
交互行为测试:
click()、dblClick()、keyboard()、hover()、paste()等 -
举例:React + Testing Library
-
使用命令
npx create-react-app react-test创建名为 react-test 的 React 项目- 此时项目中自带 Testing Library
-
移除不必要的文件,修改以下文件
App.js
import { useState } from "react"; function App() { const [name, setName] = useState(""); return ( <div className="App"> <input type="text" placeholder="Type your name" value={name} onChange={(e) => setName(e.target.value)} /> <p> My name is <span>{name}</span> </p> </div> ); } export default App;App.test.js
import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import App from "./App"; test("my name", () => { render(<App />); const myName = "SRIGT"; const element = screen.getByPlaceholderText("Type your name"); userEvent.type(element, myName); expect(screen.getByText(myName)).toBeInTheDocument(); }); -
使用命令
npm run test执行测试
-
(2)异步测试(MSW)
-
MSW(Mock Service Worker)能够在 Service Worker 一级进行拦截并模拟返回的结果
-
以 API 网络异步请求为例:
-
使用命令
npm install --save-dev msw@1安装 MSW -
修改 App.js
import { useEffect, useState } from "react"; function App() { const [name, setName] = useState(""); useEffect(async () => { const response = await fetch("https://randomuser.me/api/?inc=name", { method: "GET", }); const reply = await response.json(); const { first, last } = reply.results[0].name; setName(`${first} ${last}`); }, []); return ( <div className="App"> <input type="text" placeholder="Type your name" value={name} onChange={(e) => setName(e.target.value)} /> <p> My name is <span>{name}</span> </p> </div> ); } export default App; -
新建 src\mocks\handlers.js
import { rest } from "msw"; export const handlers = [ rest.get("https://randomuser.me/api/", (request, response, context) => { request.url.searchParams = { inc: "name", }; return response( context.status(200), context.json({ results: [ { name: { title: "Mr", first: "Eli", last: "Sanchez", }, }, ], }) ); }), ]; -
新建 src\mocks\server.js
import { setupServer } from "msw/node"; import { handlers } from "./handlers"; export const server = setupServer(...handlers); -
修改 src\setupTests.js
import "@testing-library/jest-dom"; import { server } from "./mocks/server"; beforeAll(() => server.listen()); // 开始所有前监听 afterEach(() => server.resetHandlers()); // 完成每个后重置 afterAll(() => server.close()); // 结束所有后关闭 -
修改 App.test.js
import App from "./App"; test("random name", async () => { render(<App />); const text = await screen.findByText(/Eli Sanchez/gi); expect(text).toBeInTheDocument(); });
-
(3)组件级集成测试(Cypress)
- 官网:https://www.cypress.io/
- Cypress 是基于浏览器的 E2E 测试框架
-End-

浙公网安备 33010602011771号