前端测试自动化实战:基于Jest与Cypress构建完整测试流水线
1. 项目概述:为什么前端测试自动化是必选项?
如果你还在手动刷新页面、点点点来验证功能,那可能已经落后了不止一个版本了。前端测试自动化,早已不是“锦上添花”的加分项,而是保障现代Web应用交付质量、提升团队协作效率的“基础设施”。想象一下,一个拥有几十上百个交互组件的单页应用,每次发版前,测试同学都要花上几天时间进行回归测试,不仅人力成本高,重复劳动让人疲惫,更可怕的是,人工测试难免会有疏漏,一个不起眼的边界条件就可能引发线上事故。这就是我们为什么要拥抱自动化测试——让机器去执行那些重复、枯燥但至关重要的验证工作,把人解放出来去做更有创造性的探索性测试和业务分析。
这个实战指南的核心,就是围绕Jest和Cypress这两个当前最主流、最强大的前端测试工具,构建一套从单元测试到端到端(E2E)测试的完整自动化流水线。Jest 以其“零配置”和强大的快照测试、Mock功能,成为单元和集成测试的绝对王者;而 Cypress 则以其独特的运行机制、实时重载和时光旅行调试,彻底改变了E2E测试的开发者体验。将它们组合起来,你就能覆盖从单个函数、组件的行为,到整个应用在真实浏览器中运行状态的全方位质量防线。这不仅仅是写几个测试用例那么简单,而是建立一套可持续运行、快速反馈、并能融入CI/CD流程的工程实践。无论你是正在从零搭建测试体系的前端团队负责人,还是希望提升个人工程化能力的开发者,这套“组合拳”都能为你提供一条清晰、可落地的路径。
2. 测试策略与工具选型:Jest + Cypress 为何是黄金组合?
在开始敲代码之前,我们必须先理清测试金字塔的概念,并理解为什么是Jest和Cypress,而不是其他工具。测试金字塔由下至上分别是:单元测试(最多)、集成测试(中等)、端到端测试(最少)。底层测试运行快、成本低、定位问题准,应作为主体;顶层测试模拟真实用户场景,但运行慢、维护成本高,应作为关键路径的保障。
2.1 Jest:单元与集成测试的基石
Jest 是 Facebook 出品的一个专注于“简单性”的JavaScript测试框架。它的优势非常明显:
- 开箱即用:几乎不需要配置,安装即跑,内置了测试运行器、断言库、Mock系统和覆盖率报告。
- 快照测试:这是Jest的杀手锏之一。它能捕获UI组件、配置文件甚至任何可序列化数据的“快照”,并与后续版本进行比对,非常适合检测UI的意外变更。
- 强大的Mocking:前端测试中,模拟HTTP请求、模块依赖、定时器是家常便饭。Jest提供了从函数、模块到定时器的一整套Mock方案,让你能轻松隔离测试环境。
- 并行与缓存:Jest默认并行运行测试,并利用缓存只运行改动的测试,速度极快。
注意:虽然Jest常被用于React生态,但它完全框架无关。在Vue、Angular甚至Node.js后端项目中,Jest同样表现出色。不要被它的“出身”局限了。
2.2 Cypress:端到端测试的革命者
传统的E2E测试工具如Selenium,是在浏览器外部通过WebDriver协议进行遥控,测试脚本和浏览器运行在不同的进程中。而Cypress采用了完全不同的架构:
- 同源架构:Cypress测试代码与应用程序运行在同一个浏览器循环(loop)中。这意味着它能直接访问DOM、Window对象,并能同步执行命令,彻底避免了“等待”和“竞态条件”这类Selenium中的经典难题。
- 时光旅行:Cypress在运行测试时会自动截图和录制视频。更重要的是,其内置的“时光旅行”调试工具,允许你在测试执行后,回退到任意命令执行时的状态,直观地查看当时的DOM、网络请求和Console日志。
- 实时重载:当你修改测试代码或应用代码时,Cypress会自动重新运行测试,提供无与伦比的开发体验。
- 网络流量控制:无需启动后端服务,Cypress就能轻松Stub(存根)和Spy(监听)网络请求,让你能测试各种边界场景(如网络错误、慢速响应)。
2.3 为什么是它们俩?
- 职责清晰,覆盖全面:Jest负责底层逻辑(工具函数、组件方法、状态管理)的正确性;Cypress负责顶层用户旅程(登录、下单、支付)的流畅性。两者结合,无死角覆盖。
- 开发者体验至上:两者都以提升开发者体验为核心目标。Jest的快反馈和Cypress的实时调试,让编写测试从“负担”变成“乐趣”。
- 生态与社区:它们都拥有极其活跃的社区和丰富的插件生态,遇到问题很容易找到解决方案或最佳实践。
- 与现代前端工具链无缝集成:无论是Webpack、Vite、Babel还是TypeScript,它们都有成熟的配置方案,能轻松融入你的项目。
3. 环境搭建与项目初始化
理论说再多,不如动手搭一个。我们假设你有一个基于Vite构建的React项目(其他框架原理相通)。让我们从零开始,搭建这个测试环境。
3.1 初始化项目与安装Jest
首先,如果你还没有项目,可以用以下命令快速创建一个:
npm create vite@latest my-frontend-app -- --template react-ts cd my-frontend-app npm install接下来,安装Jest及其相关依赖。虽然Vite官方推荐Vitest,但Jest的生态和稳定性目前依然更胜一筹。我们需要安装核心包和适用于React的预设。
npm install --save-dev jest @types/jest ts-jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-eventjest: Jest核心库。@types/jest: TypeScript类型定义。ts-jest: 让Jest能够处理TypeScript文件。jest-environment-jsdom: 提供一个类浏览器的DOM环境,用于测试涉及DOM操作的组件。@testing-library/react&@testing-library/jest-dom&@testing-library/user-event: React Testing Library (RTL) 三件套。这是当前React组件测试的事实标准,它鼓励你像用户一样测试组件,而非测试其内部实现细节。
3.2 配置Jest
在项目根目录创建jest.config.js文件:
/** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { // 使用 ts-jest 预设来处理 TypeScript preset: 'ts-jest', // 测试环境设置为 jsdom,以模拟浏览器环境 testEnvironment: 'jest-environment-jsdom', // 告诉 Jest 如何处理不同类型的文件 transform: { '^.+\\.tsx?$': 'ts-jest', }, // 匹配测试文件,通常放在 __tests__ 目录下或以 .test/.spec 结尾 testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'], // 设置模块别名(如果你的项目配置了,比如 @/ -> src/) moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', }, // 每次测试前自动执行的脚本,常用于设置全局的测试工具 setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], };然后创建jest.setup.js文件,用于引入一些全局的测试扩展:
// 引入 jest-dom 的扩展断言,如 toBeInTheDocument, toHaveClass 等 import '@testing-library/jest-dom'; // 可以在这里配置全局的测试前/后钩子3.3 安装与配置Cypress
Cypress的安装同样简单。我们安装其核心包和用于组件测试的包(可选,但推荐)。
npm install --save-dev cypress @cypress/react @cypress/webpack-dev-server安装完成后,初始化Cypress。这会创建默认的文件夹结构和配置文件。
npx cypress open第一次运行会弹出Cypress的图形化界面,并让你选择测试类型(E2E或组件测试)。选择E2E Testing,它会自动创建cypress.config.ts、cypress/fixtures、cypress/support和cypress/e2e目录。
我们需要调整cypress.config.ts来适配我们的Vite项目:
import { defineConfig } from 'cypress'; import webpackPreprocessor from '@cypress/webpack-dev-server'; export default defineConfig({ e2e: { // 设置测试文件的基础路径 specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', // 支持组件测试(如果需要) // supportComponentTesting: true, // 配置开发服务器 setupNodeEvents(on, config) { // 如果使用webpack,可以在这里配置,但Vite项目更推荐使用 vite-plugin-cypress 或直接使用Vite dev server // 我们这里采用更简单的方式:直接代理到本地开发服务器 }, // 基础URL,Cypress将在此URL下运行测试 baseUrl: 'http://localhost:5173', // Vite默认开发服务器端口 }, // 组件测试配置(如果启用) component: { devServer: { framework: 'react', bundler: 'vite', }, }, });一个更实用的技巧是,在package.json中添加脚本,同时启动开发服务器和Cypress:
{ "scripts": { "dev": "vite", "build": "vite build", "test:unit": "jest", "test:e2e": "cypress run", "test:e2e:open": "cypress open", "test": "npm run test:unit && npm run test:e2e" } }4. Jest单元与集成测试实战
环境搭好了,我们来写点真正的测试。我们从最简单的工具函数测试开始,再到复杂的React组件测试。
4.1 工具函数测试
假设我们有一个工具函数src/utils/math.ts:
// 一个简单的加法函数,但有一些边界处理 export function add(a: number, b: number): number { if (typeof a !== 'number' || typeof b !== 'number') { throw new TypeError('Parameters must be numbers'); } // 模拟一个浮点数精度问题 return parseFloat((a + b).toFixed(2)); }为其创建测试文件src/utils/math.test.ts:
import { add } from './math'; describe('add function', () => { // 测试正常功能 it('should add two positive numbers correctly', () => { expect(add(1, 2)).toBe(3); expect(add(0.1, 0.2)).toBe(0.3); // 注意浮点数精度,我们的函数已处理 }); // 测试负数 it('should handle negative numbers', () => { expect(add(-1, 5)).toBe(4); expect(add(-2, -3)).toBe(-5); }); // 测试边界/异常情况 it('should throw TypeError for non-number inputs', () => { // 注意:测试异步错误或抛出错误的函数,需要将断言包装在一个函数中 expect(() => add('1' as any, 2)).toThrow(TypeError); expect(() => add(1, null as any)).toThrow('Parameters must be numbers'); }); // 测试浮点数精度处理 it('should fix floating point precision', () => { // 0.1 + 0.2 在JS中等于 0.30000000000000004 expect(add(0.1, 0.2)).toBe(0.3); expect(add(1.005, 2.005)).toBe(3.01); // 1.005+2.005=3.01, toFixed(2)后正确 }); });运行npm run test:unit,Jest会找到这个测试文件并执行。describe用于分组,it(或test)用于定义一个具体的测试用例。expect是断言,toBe是匹配器(Matcher)。Jest提供了丰富的匹配器,如toEqual(深度比较对象)、toHaveBeenCalledWith(检查函数调用参数)等。
4.2 React组件测试(使用React Testing Library)
这是前端测试的重头戏。假设我们有一个简单的计数器组件src/components/Counter.tsx:
import { useState } from 'react'; interface CounterProps { initialCount?: number; } export function Counter({ initialCount = 0 }: CounterProps) { const [count, setCount] = useState(initialCount); const increment = () => setCount(count + 1); const decrement = () => setCount(count - 1); const reset = () => setCount(initialCount); return ( <div> <h2>import { render, screen, fireEvent } from '@testing-library/react'; import { Counter } from './Counter'; import '@testing-library/jest-dom'; // 引入扩展断言 describe('Counter Component', () => { // 测试初始渲染 it('renders with initial count', () => { render(<Counter initialCount={5} />); // 通过文本内容查找元素 const displayElement = screen.getByText(/count: 5/i); expect(displayElement).toBeInTheDocument(); // 使用 jest-dom 的扩展断言 }); // 测试交互:点击增加按钮 it('increments count when + button is clicked', () => { render(<Counter />); const incrementButton = screen.getByRole('button', { name: /increment/i }); const displayElement = screen.getByTestId('count-display'); // 使用>// src/services/api.ts export async function fetchUserData(userId: string) { const response = await fetch(`/api/users/${userId}`); return response.json(); } // src/components/UserProfile.tsx import { useEffect, useState } from 'react'; import { fetchUserData } from '../services/api';在测试中,我们不应该真的发起网络请求。我们需要Mock这个fetchUserData函数。
// src/components/UserProfile.test.tsx import { render, screen, waitFor } from '@testing-library/react'; import UserProfile from './UserProfile'; import { fetchUserData } from '../services/api'; // 1. 使用 jest.mock 自动模拟整个模块 jest.mock('../services/api'); // 2. 将模拟后的模块转换为 jest.Mocked 类型以获得类型安全 const mockedFetchUserData = fetchUserData as jest.MockedFunction<typeof fetchUserData>; describe('UserProfile', () => { it('displays user data after successful fetch', async () => { // 3. 为模拟函数设置返回值 const mockUser = { id: '1', name: 'John Doe', email: 'john@example.com' }; mockedFetchUserData.mockResolvedValueOnce(mockUser); // 模拟一次成功的异步调用 render(<UserProfile userId="1" />); // 初始应为加载状态 expect(screen.getByText(/loading/i)).toBeInTheDocument(); // 使用 waitFor 等待异步操作完成和UI更新 await waitFor(() => { expect(screen.getByText(mockUser.name)).toBeInTheDocument(); expect(screen.getByText(mockUser.email)).toBeInTheDocument(); }); // 验证函数被以正确的参数调用 expect(mockedFetchUserData).toHaveBeenCalledWith('1'); expect(mockedFetchUserData).toHaveBeenCalledTimes(1); }); it('displays error message when fetch fails', async () => { // 模拟一次失败的调用 mockedFetchUserData.mockRejectedValueOnce(new Error('Network Error')); render(<UserProfile userId="2" />); await waitFor(() => { expect(screen.getByText(/failed to load user/i)).toBeInTheDocument(); }); }); });实操心得:Mock是单元测试的灵魂,但不要过度Mock。如果一个测试文件里充满了
jest.mock,你可能需要反思组件的设计是否耦合过紧。理想情况下,应通过Props传递依赖,或者使用像@testing-library/react-hooks这样的工具来测试自定义Hook。
4.4 快照测试
快照测试用于捕获组件渲染输出的结构,防止意外更改。它非常适合用于不经常变化的展示型组件或配置对象。
// 在 Counter.test.tsx 中添加 it('matches snapshot', () => { const { container } = render(<Counter initialCount={42} />); expect(container.firstChild).toMatchSnapshot(); });第一次运行测试时,Jest会在__snapshots__目录下生成一个.snap文件,里面是组件渲染的字符串表示。后续运行时,Jest会将新的渲染结果与快照对比。如果不同,测试会失败。这时你需要检查差异:如果是预期的改动,按u键更新快照;如果是bug,则修复组件。
注意事项:快照测试不能替代具体的断言。它容易产生“虚假安全”(因为任何改动都会导致失败,你可能不假思索地更新快照)。应将其作为辅助手段,与具体的交互测试结合使用。
5. Cypress端到端测试实战
单元测试保证了“零件”的质量,E2E测试则要验证组装好的“汽车”能跑。Cypress让这个过程变得直观。
5.1 编写第一个E2E测试
假设我们有一个简单的待办事项应用。我们编写一个测试用户故事:“用户访问首页,添加一个新的待办事项,并验证它出现在列表中”。 在cypress/e2e/todo.cy.ts中:
describe('Todo Application', () => { // 每个测试用例运行前执行,通常用于访问被测页面 beforeEach(() => { // 访问本地开发服务器。baseUrl 在 cypress.config.ts 中配置 cy.visit('/'); }); it('should allow user to add a new todo item', () => { // 1. 断言页面加载成功,包含关键元素 cy.get('h1').should('contain.text', 'Todo List'); cy.get('input[placeholder="Add a new todo..."]').should('be.visible'); // 2. 用户输入文本并提交 const newTodoText = 'Learn Cypress E2E Testing'; cy.get('input[placeholder="Add a new todo..."]').type(newTodoText); cy.get('button').contains('Add').click(); // 3. 断言新事项出现在列表中,且输入框被清空 cy.get('.todo-list li') .should('have.length', 1) // 假设初始列表为空 .last() // 获取最后一项(即刚添加的) .should('contain.text', newTodoText); cy.get('input[placeholder="Add a new todo..."]').should('have.value', ''); // 输入框应清空 }); it('should mark a todo item as completed', () => { // 先添加一个事项 cy.get('input').type('Item to complete{enter}'); // {enter} 模拟回车键提交 // 找到这个事项的复选框并勾选 cy.get('.todo-list li') .first() .within(() => { // within 将查询范围限定在当前元素内 cy.get('input[type="checkbox"]').check(); }); // 断言事项被标记为完成(可能有样式变化) cy.get('.todo-list li') .first() .should('have.class', 'completed') // 假设完成的事项有 .completed 类 .find('label') // 找到文本标签 .should('have.css', 'text-decoration', 'line-through solid rgb(0, 0, 0)'); // 更具体的样式断言 }); it('should delete a todo item', () => { // 添加两个事项 cy.get('input').type('Item A{enter}'); cy.get('input').type('Item B{enter}'); // 删除第一个事项 cy.get('.todo-list li') .first() .within(() => { cy.get('button.delete').click(); // 假设每个事项有个删除按钮 }); // 断言只剩下一个事项,且是“Item B” cy.get('.todo-list li') .should('have.length', 1) .and('contain.text', 'Item B'); }); });Cypress的命令是链式调用的,并且具有自动重试机制。例如,cy.get(...).should('be.visible')会持续尝试查找元素直到它可见(默认超时4秒),这极大地增强了测试的稳定性,无需手动添加sleep。
5.2 网络请求的拦截与存根(Stubbing)
E2E测试不应该依赖不稳定的后端服务。Cypress可以轻松拦截和存根网络请求。假设我们的待办事项是从API加载的。
describe('Todo App with API', () => { it('loads initial todos from API', () => { // 在访问页面之前,拦截特定的API请求 cy.intercept('GET', '/api/todos', { statusCode: 200, body: [ { id: 1, text: 'Mocked Todo 1', completed: false }, { id: 2, text: 'Mocked Todo 2', completed: true }, ], }).as('getTodos'); // 给这个拦截请求起个别名 cy.visit('/'); // 等待这个拦截请求完成(可选,用于确保请求已发生) cy.wait('@getTodos'); // 断言页面显示了模拟的数据 cy.get('.todo-list li').should('have.length', 2); cy.contains('Mocked Todo 1').should('be.visible'); cy.contains('Mocked Todo 2').should('be.visible'); }); it('shows error message when API fails', () => { cy.intercept('GET', '/api/todos', { statusCode: 500, body: { error: 'Internal Server Error' }, delay: 1000, // 模拟网络延迟 }).as('failedRequest'); cy.visit('/'); cy.wait('@failedRequest'); // 断言错误提示出现 cy.get('.error-message').should('contain.text', 'Failed to load todos'); }); });cy.intercept()是Cypress最强大的功能之一。你可以用它来:
- 存根(Stub):直接返回模拟数据,不发送真实请求。
- 监听(Spy):让请求正常发出,但监听其请求和响应,用于断言。
- 修改响应:拦截真实请求并修改其响应体或状态码。
5.3 使用自定义命令和Fixtures
为了提高代码复用性,可以将常用操作封装为自定义命令。例如,登录操作在很多测试中都需要。 在cypress/support/commands.ts中添加:
// 声明自定义命令的类型(在 cypress/support/index.d.ts 或全局声明文件中更好) declare global { namespace Cypress { interface Chainable { /** * 自定义命令:使用给定凭据登录 * @example cy.login('test@example.com', 'password123') */ login(email?: string, password?: string): Chainable<Element>; } } } Cypress.Commands.add('login', (email = 'test@example.com', password = 'password123') => { cy.intercept('POST', '/api/login').as('loginRequest'); cy.visit('/login'); cy.get('input[name="email"]').type(email); cy.get('input[name="password"]').type(password); cy.get('button[type="submit"]').click(); cy.wait('@loginRequest'); // 可以在这里断言登录成功,比如跳转到首页 cy.url().should('include', '/dashboard'); });然后在测试中就可以直接使用cy.login()。
Fixtures用于存放静态测试数据,如JSON、图片等。在cypress/fixtures目录下创建example-todos.json:
[ { "id": 101, "text": "Fixture Todo One", "completed": false }, { "id": 102, "text": "Fixture Todo Two", "completed": true } ]在测试中使用:
cy.fixture('example-todos').then((todos) => { cy.intercept('GET', '/api/todos', todos); cy.visit('/'); // ... 断言 });6. 集成与持续集成(CI)流程
测试写好了,如何让它自动运行,成为质量守门员?
6.1 本地脚本集成
我们已经配置了package.json脚本。可以运行npm test来依次执行单元测试和E2E测试。但E2E测试需要应用在运行。我们可以使用concurrently或start-server-and-test这类工具来编排。
npm install --save-dev start-server-and-test修改package.json:
{ "scripts": { "dev": "vite", "build": "vite build", "test:unit": "jest", "test:e2e": "cypress run", "test:e2e:open": "cypress open", "test:e2e:ci": "start-server-and-test 'npm run dev' http://localhost:5173 'npm run test:e2e'", "test:ci": "npm run test:unit && npm run test:e2e:ci" } }start-server-and-test会先执行第一个命令(启动服务器),然后轮询第二个参数给出的URL直到可访问,最后执行第三个命令(运行测试)。测试结束后,它会自动关闭服务器。
6.2 集成到GitHub Actions
在项目根目录创建.github/workflows/test.yml:
name: CI Tests on: [push, pull_request] # 在推送代码或创建PR时触发 jobs: test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' # 使用你的项目Node版本 cache: 'npm' - name: Install dependencies run: npm ci # 使用 ci 而不是 install,确保依赖锁一致 - name: Run unit tests with Jest run: npm run test:unit # 可以添加覆盖率报告上传步骤 # env: # CI: true - name: Run E2E tests with Cypress uses: cypress-io/github-action@v6 with: build: npm run build # 先构建生产版本 start: npm run preview # 使用Vite预览服务器,或直接用 `npm run dev` 但需要配置 wait-on wait-on: 'http://localhost:4173' # Vite预览默认端口 # 或者使用 start-server-and-test # start: npm run test:e2e:ci # Cypress Action 会自动处理服务器的启动、等待和测试执行这个工作流会在每次代码变更时自动运行你的全套测试。如果测试失败,PR将无法合并,从而强制保证主分支代码的质量。
6.3 测试报告与可视化
- Jest:运行
jest --coverage会生成一个代码覆盖率报告(HTML格式),存放在coverage目录。你可以配置CI将其上传到如Codecov、Coveralls等服务。 - Cypress:运行
cypress run默认会在cypress/videos和cypress/screenshots中生成测试录像和失败截图。在CI中,你可以将这些作为构件(Artifacts)上传,方便失败时查看。Cypress Dashboard 服务(付费)提供了更强大的测试记录、并行化、负载均衡功能。
7. 常见问题、调试技巧与最佳实践
7.1 Jest常见问题
- 测试无法识别ESM模块:如果你的项目或依赖使用ES Modules,Jest可能需要额外配置。可以使用
jest.config.js中的transformIgnorePatterns排除某些不需要转换的node_modules,或者使用@jest/experimental开启对ESM的实验性支持。 - “act(...)”警告:在测试涉及状态更新的组件时(如使用
useEffect),RTL可能会输出此警告。使用await waitFor(...)或findBy*查询器(如screen.findByText)来等待异步更新。对于更复杂的情况,可以使用act从@testing-library/react导入并手动包装。 - Mock不生效:确保
jest.mock语句在文件顶部,在任何导入之前。Jest的模块模拟机制会在导入前生效。
7.2 Cypress常见问题
- “元素未找到”或“超时”:这是最常见的问题。首先,使用Cypress的选择器检查器(打开Cypress Test Runner,点击选择器工具)来确认你的选择器是否能唯一找到元素。其次,确保你的操作在正确的时机(例如,等待数据加载完成后再点击按钮)。善用
.should()进行断言式等待,而不是硬性等待cy.wait(5000)。 - 跨域问题:Cypress默认禁止访问不同顶级域名的页面。如果你的测试涉及导航到另一个域名(如SSO登录),需要配置
chromeWebSecurity: false在cypress.config.ts中,但这会降低一些安全性。更好的做法是使用cy.origin()来隔离跨域上下文。 - 测试不稳定(Flaky Tests):不稳定的测试是CI/CD的毒药。主要原因有:1)网络/API依赖:使用
cy.intercept()彻底存根不稳定的后端调用。2)动画/过渡效果:使用{ force: true }选项或cy.config('defaultCommandTimeout', 10000)增加超时。3)第三方小部件:如地图、聊天插件,考虑在测试环境中禁用它们,或使用cy.clock()来控制时间。
7.3 最佳实践清单
- 测试原则:
- 测试行为,而非实现:不要测试组件内部状态或方法名。测试用户能看到和交互的东西(文本、按钮、输入框)。
- 保持测试独立:每个测试不应该依赖其他测试的状态或外部环境。使用
beforeEach进行清理和初始化。 - 优先单元,谨慎E2E:测试金字塔。用大量快速、低成本的单元测试覆盖核心逻辑,用少量关键的E2E测试覆盖核心用户流程。
- Jest/RTL实践:
- 为交互元素添加
>
- 为交互元素添加
