☰
前端测试全攻略:从Jest到Cypress实战
2026/10/29 15:03:13 网站建设 项目流程

1. 前端测试入门:为什么它如此重要?

刚入行前端那会儿,我最讨厌的就是写测试。总觉得"我写的代码怎么可能出错?"直到有一次,我在深夜上线了一个看似简单的样式改动,结果导致整个产品页面的布局在iOS设备上完全崩溃。那次事故让我彻底明白了前端测试的价值——它就像是代码的安全网,在你失足时能稳稳接住你。

前端测试本质上是通过自动化手段验证用户界面(UI)和交互逻辑是否符合预期。与后端测试不同,前端测试需要处理浏览器兼容性、DOM操作、异步加载等独特挑战。一个完整的前端测试方案通常包含以下几个层次:

  • 单元测试(Unit Testing):针对最小可测试单元(通常是函数或组件)的测试
  • 集成测试(Integration Testing):验证多个单元协同工作时的表现
  • 端到端测试(E2E Testing):模拟真实用户操作流程的完整场景测试
  • 视觉回归测试(Visual Regression Testing):捕捉UI层面的意外变化

2. 现代前端测试工具链解析

2.1 测试框架选型指南

选择测试工具就像挑选趁手的兵器,需要考虑项目规模、团队习惯和技术栈。以下是2023年主流的前端测试工具对比:

工具类型推荐选择适用场景学习曲线
测试运行器Jest/Vitest大多数React/Vue项目低
断言库Chai需要灵活断言语法时中
E2E测试Cypress/Playwright需要真实浏览器环境的重度交互应用高
组件测试Testing Library隔离测试React/Vue组件中
视觉回归Storybook+Chromatic设计系统/UI组件库高

我个人在React项目中常用的组合是Jest + Testing Library + Playwright,这套组合覆盖了从单元测试到E2E测试的全场景需求。

2.2 Jest配置实战

Jest作为Facebook推出的测试框架,几乎成为了前端单元测试的事实标准。下面是一个典型的jest.config.js配置:

module.exports = { preset: 'ts-jest', testEnvironment: 'jsdom', setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], moduleNameMapper: { '\\.(css|less)$': 'identity-obj-proxy', '^@/(.*)$': '<rootDir>/src/$1' }, coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } } };

关键配置解析:

  • testEnvironment: 'jsdom':模拟浏览器环境
  • moduleNameMapper:处理样式文件和路径别名
  • coverageThreshold:设置测试覆盖率最低要求

提示:在CI环境中,建议将coverageThreshold提高5-10个百分点,确保代码质量

3. 组件测试深度实践

3.1 React组件测试方法论

使用React Testing Library测试组件时,要牢记一个核心原则:"测试用户看到和交互的方式,而不是实现细节"。下面是一个按钮组件的测试案例:

import { render, screen, fireEvent } from '@testing-library/react'; import Button from './Button'; describe('Button组件', () => { it('应该显示正确的文本', () => { render(<Button>点击我</Button>); expect(screen.getByText('点击我')).toBeInTheDocument(); }); it('点击时应触发回调', () => { const handleClick = jest.fn(); render(<Button onClick={handleClick}>按钮</Button>); fireEvent.click(screen.getByRole('button')); expect(handleClick).toHaveBeenCalledTimes(1); }); it('禁用状态下不应触发回调', () => { const handleClick = jest.fn(); render( <Button disabled onClick={handleClick}> 禁用按钮 </Button> ); fireEvent.click(screen.getByText('禁用按钮')); expect(handleClick).not.toHaveBeenCalled(); }); });

3.2 Vue 3组合式API测试技巧

Vue 3的Composition API带来了新的测试挑战。下面是测试一个useCounter组合函数的示例:

import { ref } from 'vue'; import { renderHook } from '@testing-library/vue'; import useCounter from './useCounter'; describe('useCounter', () => { it('应该初始化计数器', () => { const { result } = renderHook(() => useCounter()); expect(result.current.count.value).toBe(0); }); it('应该增加计数', async () => { const { result } = renderHook(() => useCounter()); await result.current.increment(); expect(result.current.count.value).toBe(1); }); it('应该支持自定义初始值', () => { const initialValue = ref(10); const { result } = renderHook(() => useCounter(initialValue)); expect(result.current.count.value).toBe(10); }); });

4. E2E测试进阶技巧

4.1 Cypress最佳实践

Cypress的核心理念是"测试应该像开发一样愉快"。下面是一个电商网站购物流程的测试示例:

describe('购物流程', () => { beforeEach(() => { cy.visit('https://example.com'); }); it('应该完成从浏览到下单的全流程', () => { cy.get('.product-card').first().click(); cy.get('[data-testid="add-to-cart"]').click(); cy.contains('去结算').click(); cy.get('[placeholder="请输入收货地址"]').type('测试地址123号'); cy.contains('提交订单').click(); cy.url().should('include', '/order-success'); }); it('应该显示空购物车提示', () => { cy.visit('/cart'); cy.contains('购物车是空的').should('be.visible'); cy.get('.empty-cart-image').should('exist'); }); });

4.2 Playwright多浏览器测试

Playwright支持Chromium、WebKit和Firefox三大引擎,非常适合跨浏览器测试:

const { test, expect } = require('@playwright/test'); test.describe('登录测试', () => { ['chromium', 'webkit', 'firefox'].forEach((browserType) => { test(`应该在${browserType}上成功登录`, async ({ browser }) => { const context = await browser.newContext(); const page = await context.newPage(); await page.goto('https://example.com/login'); await page.fill('#username', 'testuser'); await page.fill('#password', 'password123'); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/dashboard/); await expect(page.locator('.welcome-message')).toContainText('欢迎回来'); }); }); });

5. 测试策略与持续集成

5.1 分层测试金字塔实践

合理的测试策略应该遵循测试金字塔原则:

/\ / \ / UI \ /______\ / \ / Integration /____________\ / \ / Unit \ /__________________\

具体到前端项目,我推荐的测试分布是:

  • 70%单元测试(组件/工具函数)
  • 20%集成测试(组件交互)
  • 10%E2E测试(关键用户旅程)

5.2 GitHub Actions集成示例

将前端测试纳入CI/CD流水线可以及早发现问题。下面是一个基本的GitHub Actions配置:

name: Frontend CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 16 - run: npm ci - run: npm run test:unit - run: npm run test:e2e - uses: actions/upload-artifact@v3 if: failure() with: name: test-results path: | coverage/ cypress/screenshots/ cypress/videos/

6. 常见问题与调试技巧

6.1 测试不稳定(Flaky Tests)解决方案

不稳定的测试是自动化测试的噩梦。以下是几种常见原因和解决方法:

  1. 定时器问题:

    // 错误方式 setTimeout(() => { expect(element).toBeVisible(); }, 1000); // 正确方式 await waitFor(() => { expect(element).toBeVisible(); });
  2. 网络请求不稳定:

    // 使用Mock Service Worker(MSW)拦截API请求 import { setupServer } from 'msw/node'; import { rest } from 'msw'; const server = setupServer( rest.get('/api/user', (req, res, ctx) => { return res(ctx.json({ name: '测试用户' })); }) ); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close());
  3. 状态污染:

    // 每个测试前重置状态 beforeEach(() => { cleanup(); initTestState(); });

6.2 测试覆盖率优化技巧

提高测试覆盖率不是目的,而是手段。以下是一些实用建议:

  1. 忽略不必要的覆盖:

    /* istanbul ignore next */ function shouldNeverBeTested() { return magic(); }
  2. 聚焦关键路径:

    • 优先测试业务核心逻辑
    • 其次测试容易出错的边界条件
    • 最后考虑全覆盖
  3. 使用覆盖率报告:

    jest --coverage --collectCoverageFrom='src/**/*.{js,jsx,ts,tsx}'

7. 测试驱动开发(TDD)实战

7.1 TDD三步循环

  1. 红阶段:编写一个失败测试

    // 测试一个尚未实现的formatDate函数 test('应该格式化日期为YYYY-MM-DD', () => { expect(formatDate(new Date(2023, 0, 1))).toBe('2023-01-01'); });
  2. 绿阶段:实现最简单可通过的代码

    function formatDate(date) { return '2023-01-01'; // 硬编码先通过测试 }
  3. 重构阶段:改进实现而不改变行为

    function formatDate(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }

7.2 TDD常见误区

  1. 过度测试实现细节:

    // 错误:测试内部状态而非行为 test('应该使用useState钩子', () => { render(<Counter />); expect(Counter.useState).toHaveBeenCalled(); }); // 正确:测试可见行为 test('点击按钮应该增加计数', () => { render(<Counter />); fireEvent.click(screen.getByText('+')); expect(screen.getByText('1')).toBeInTheDocument(); });
  2. 忽视重构阶段:很多开发者停留在"绿阶段",导致代码质量下降

  3. 测试覆盖但无断言:

    // 错误:执行了代码但没有验证 test('应该调用API', () => { fetchData(); // 没有断言 });

8. 视觉回归测试实践

8.1 Storybook + Chromatic方案

视觉回归测试能捕捉到像素级别的UI变化:

  1. 首先在Storybook中定义组件故事:
// Button.stories.js export default { title: 'Components/Button', component: Button, }; export const Primary = () => <Button variant="primary">主要按钮</Button>; export const Secondary = () => <Button variant="secondary">次要按钮</Button>;
  1. 配置Chromatic进行视觉测试:
# 安装 npm install --save-dev chromatic # 运行测试 npx chromatic --project-token=<your-project-token>

8.2 处理动态内容

对于包含动态内容(如日期、随机数)的组件,需要固定测试数据:

// 使用Mock Date固定时间 beforeAll(() => { jest.useFakeTimers(); jest.setSystemTime(new Date(2023, 0, 1)); }); afterAll(() => { jest.useRealTimers(); });

9. 性能测试入门

9.1 使用Lighthouse CI

Lighthouse不仅可以测量性能,还能集成到CI流程中:

// .github/workflows/lighthouse.yml name: Lighthouse Audit on: [push] jobs: lighthouse: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 - run: npm ci - run: npm run build - uses: treosh/lighthouse-ci-action@v9 with: urls: | http://localhost:3000/ http://localhost:3000/dashboard budgetPath: ./lighthouse-budget.json

9.2 组件级性能测试

使用React Profiler API测试组件渲染性能:

import { unstable_trace as trace } from 'scheduler/tracing'; test('组件渲染不应超过100ms', () => { const renderTime = []; trace('性能测试', performance.now(), () => { render(<ComplexComponent />); renderTime.push(performance.now()); }); expect(renderTime[0]).toBeLessThan(100); });

10. 测试代码质量保障

10.1 测试代码的Lint规则

测试代码也需要保持高质量。推荐使用eslint-plugin-testing-library:

// .eslintrc.js module.exports = { plugins: ['testing-library'], overrides: [ { files: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'], extends: ['plugin:testing-library/react'] } ] };

10.2 测试代码重构技巧

  1. 提取公共工具函数:
// 代替重复的渲染代码 function renderButton(props = {}) { const defaultProps = { onClick: jest.fn(), children: '按钮' }; return render(<Button {...defaultProps} {...props} />); }
  1. 使用工厂函数生成测试数据:
function createUser(overrides = {}) { return { id: faker.datatype.uuid(), name: faker.name.fullName(), email: faker.internet.email(), ...overrides }; }
  1. 遵循DRY原则:但不要过度抽象,保持测试可读性

11. 微前端架构测试策略

11.1 模块隔离测试

在微前端架构中,每个微应用应该有自己的测试套件:

// app1/__tests__/bootstrap.test.js describe('微应用A', () => { beforeAll(() => { // 模拟微前端环境 window.__MICRO_APP_ENVIRONMENT__ = true; }); it('应该正确挂载', () => { const container = document.createElement('div'); container.id = 'app1-container'; document.body.appendChild(container); require('../bootstrap'); expect(container.querySelector('.app1-root')).toBeInTheDocument(); }); });

11.2 集成测试方案

使用Playwright测试微前端集成场景:

test('应该正确加载微应用A和B', async ({ page }) => { await page.goto('http://localhost:3000'); // 验证微应用A await expect(page.frameLocator('#app1-iframe').locator('.app1-title')) .toHaveText('应用A'); // 验证微应用B await expect(page.frameLocator('#app2-iframe').locator('.app2-title')) .toHaveText('应用B'); // 验证跨应用通信 await page.frameLocator('#app1-iframe').getByText('发送消息').click(); await expect(page.frameLocator('#app2-iframe').locator('.message')) .toHaveText('来自应用A的消息'); });

12. 测试数据管理进阶

12.1 测试数据工厂模式

使用工厂模式创建测试数据:

// tests/factories/user.js export const UserFactory = { create(overrides = {}) { return { id: uuid(), name: '测试用户', email: 'test@example.com', ...overrides }; }, createAdmin() { return this.create({ role: 'admin' }); }, createWithPosts(count = 3) { const user = this.create(); user.posts = Array(count).fill().map((_, i) => ({ id: uuid(), title: `测试文章${i + 1}`, content: '...' })); return user; } };

12.2 Mock数据库方案

对于复杂的数据操作,可以使用内存数据库:

// 使用SQLite内存数据库测试数据访问层 const sqlite3 = require('sqlite3').verbose(); test('用户DAO应该正确保存用户', async () => { const db = new sqlite3.Database(':memory:'); await createTables(db); // 创建测试表结构 const userDao = new UserDao(db); const testUser = UserFactory.create(); const savedUser = await userDao.save(testUser); expect(savedUser.id).toBeDefined(); const fetchedUser = await userDao.findById(savedUser.id); expect(fetchedUser.name).toBe(testUser.name); });

13. 无障碍(A11Y)测试实践

13.1 自动化无障碍测试

使用axe-core进行无障碍扫描:

import { axe } from 'jest-axe'; test('组件应该符合无障碍标准', async () => { const { container } = render(<LoginForm />); const results = await axe(container); expect(results).toHaveNoViolations(); });

13.2 键盘导航测试

验证组件是否支持键盘操作:

test('下拉菜单应该支持键盘导航', async () => { render(<Dropdown items={['选项1', '选项2', '选项3']} />); // 打开下拉菜单 const trigger = screen.getByRole('button'); trigger.focus(); fireEvent.keyDown(trigger, { key: 'ArrowDown' }); // 验证菜单项 const menuItems = screen.getAllByRole('menuitem'); expect(menuItems).toHaveLength(3); // 模拟键盘导航 fireEvent.keyDown(menuItems[0], { key: 'ArrowDown' }); expect(menuItems[1]).toHaveFocus(); });

14. 测试报告与可视化

14.1 生成HTML测试报告

使用jest-html-reporter生成可视化报告:

// jest.config.js module.exports = { reporters: [ 'default', ['jest-html-reporter', { pageTitle: '测试报告', outputPath: 'test-report.html', includeFailureMsg: true }] ] };

14.2 使用Allure生成高级报告

Allure提供了更专业的测试报告:

# 安装依赖 npm install -D allure-jest # 运行测试并生成报告 jest --allure allure serve

15. 测试环境管理

15.1 Docker化测试环境

使用Docker确保测试环境一致性:

# Dockerfile.test FROM node:16 WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . CMD ["npm", "test"]
# 构建并运行测试 docker build -t frontend-tests -f Dockerfile.test . docker run --rm frontend-tests

15.2 测试数据清理策略

确保测试不会留下垃圾数据:

afterEach(async () => { // 清理测试数据库 await TestDB.clean(); // 清理本地存储 window.localStorage.clear(); window.sessionStorage.clear(); // 重置Mock jest.clearAllMocks(); });

16. 移动端测试专项

16.1 设备模拟测试

使用Playwright模拟移动设备:

test('应该在移动端正确显示', async ({ browser }) => { const iPhone = devices['iPhone 13']; const context = await browser.newContext({ ...iPhone, locale: 'zh-CN' }); const page = await context.newPage(); await page.goto('https://example.com'); await expect(page.locator('.mobile-menu')).toBeVisible(); });

16.2 触摸事件测试

测试触摸交互:

test('应该支持滑动操作', async () => { render(<ImageGallery images={[...]} />); const gallery = screen.getByTestId('image-gallery'); const firstImage = screen.getByAltText('图片1'); // 模拟滑动 fireEvent.touchStart(gallery, { touches: [{ clientX: 100, clientY: 0 }] }); fireEvent.touchMove(gallery, { touches: [{ clientX: 50, clientY: 0 }] }); fireEvent.touchEnd(gallery); await waitFor(() => { expect(firstImage).not.toBeVisible(); }); });

17. 国际化(i18n)测试

17.1 多语言切换测试

验证国际化功能:

test('应该正确切换语言', async () => { render( <I18nProvider> <App /> </I18nProvider> ); // 默认中文 expect(screen.getByText('欢迎')).toBeInTheDocument(); // 切换英语 fireEvent.click(screen.getByText('English')); await waitFor(() => { expect(screen.getByText('Welcome')).toBeInTheDocument(); }); });

17.2 RTL(从右到左)布局测试

测试阿拉伯语等RTL语言支持:

test('应该支持RTL布局', () => { render( <div dir="rtl"> <Button>测试按钮</Button> </div> ); const button = screen.getByRole('button'); expect(button).toHaveStyle('text-align: right'); });

18. 测试代码覆盖率优化

18.1 增量覆盖率检查

只检查修改文件的覆盖率:

# 获取修改的文件 CHANGED_FILES=$(git diff --name-only main... | grep 'src/.*\.js$' | tr '\n' ' ') # 只运行这些文件的测试 jest --collectCoverageFrom="$CHANGED_FILES" --coverage

18.2 覆盖率差异报告

生成与主分支的覆盖率差异:

# 生成当前分支覆盖率 npm run test:coverage # 与主分支比较 npx coverage-diff ./.coverage/coverage-final.json origin/main/.coverage/coverage-final.json

19. 测试性能优化

19.1 并行测试执行

利用多核CPU并行运行测试:

// jest.config.js module.exports = { maxWorkers: '50%', // 使用一半CPU核心 testEnvironment: 'jsdom', // 其他配置... };

19.2 测试文件分割

将大型测试文件拆分为多个小文件:

__tests__/ components/ Button/ Button.render.test.js Button.interaction.test.js Button.accessibility.test.js

20. 测试类型进阶

20.1 属性类型测试

验证React组件的propTypes:

test('应该验证props类型', () => { const spy = jest.spyOn(console, 'error').mockImplementation(); render(<Button size="invalid-size" />); expect(spy).toHaveBeenCalledWith( expect.stringContaining('Invalid prop `size`') ); spy.mockRestore(); });

20.2 快照测试最佳实践

合理使用快照测试:

test('应该渲染正确的UI结构', () => { const { asFragment } = render(<ComplexComponent />); // 第一次运行生成快照 // 后续运行会与保存的快照比较 expect(asFragment()).toMatchSnapshot(); });

提示:快照测试最适合用于不会频繁变化的静态组件,对于业务逻辑组件建议谨慎使用

21. 测试驱动设计(TDD)进阶

21.1 测试优先的组件设计

从测试角度设计更合理的组件API:

// 先写测试 test('Modal应该支持无障碍关闭', () => { render( <Modal onClose={mockClose}> <button autoFocus>测试</button> </Modal> ); // 按ESC应该关闭 fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' }); expect(mockClose).toHaveBeenCalled(); // 点击遮罩层应该关闭 fireEvent.click(screen.getByTestId('modal-overlay')); expect(mockClose).toHaveBeenCalled(); // 焦点应该被限制在模态框内 expect(screen.getByText('测试')).toHaveFocus(); });

21.2 测试作为文档

良好的测试本身就是最好的文档:

describe('usePagination', () => { it('应该初始化页码为1', () => { const { result } = renderHook(() => usePagination({ totalItems: 100 })); expect(result.current.currentPage).toBe(1); }); it('应该计算总页数', () => { const { result } = renderHook(() => usePagination({ totalItems: 100, itemsPerPage: 10 }) ); expect(result.current.totalPages).toBe(10); }); it('应该支持跳转页面', () => { const { result } = renderHook(() => usePagination({ totalItems: 100 })); act(() => { result.current.goToPage(3); }); expect(result.current.currentPage).toBe(3); }); });

22. 测试与TypeScript集成

22.1 类型测试工具

使用@ts-expect-error测试类型错误:

test('应该拒绝无效的props类型', () => { // @ts-expect-error size应该是'small'|'medium'|'large' const button = <Button size="extra-large" />; // 正常情况不应该有类型错误 const validButton = <Button size="medium" />; });

22.2 类型断言测试

验证自定义类型守卫:

function isAdmin(user: User): user is Admin { return user.role === 'admin'; } test('isAdmin类型守卫应该正确工作', () => { const admin = { role: 'admin', permissions: [] }; const regularUser = { role: 'user' }; expect(isAdmin(admin)).toBe(true); if (isAdmin(admin)) { // 在这个块内,TypeScript知道admin有permissions属性 expect(admin.permissions).toEqual([]); } expect(isAdmin(regularUser)).toBe(false); });

23. 测试与设计系统集成

23.1 组件契约测试

确保组件符合设计系统规范:

describe('Button组件规范', () => { it('应该包含设计系统定义的CSS变量', () => { render(<Button>测试</Button>); const button = screen.getByRole('button'); expect(button).toHaveStyle({ '--button-primary-color': '#0066ff', '--button-border-radius': '4px' }); }); it('应该正确应用尺寸类', () => { const { rerender } = render(<Button size="small">小按钮</Button>); expect(screen.getByRole('button')).toHaveClass('btn-sm'); rerender(<Button size="large">大按钮</Button>); expect(screen.getByRole('button')).toHaveClass('btn-lg'); }); });

23.2 视觉回归测试集成

将Storybook故事作为测试用例:

import * as stories from './Button.stories'; describe('Button视觉测试', () => { const testCases = Object.values(stories) .filter(story => typeof story === 'function') .map(story => [story.storyName, story]); test.each(testCases)('%s应该正确渲染', (_, Story) => { const { container } = render(<Story />); expect(container).toMatchSnapshot(); }); });

24. 测试与状态管理集成

24.1 Redux测试策略

测试Redux store和action:

describe('counter reducer', () => { let store; beforeEach(() => { store = configureStore({ reducer: { counter: counterReducer } }); }); it('应该处理初始状态', () => { expect(store.getState().counter).toEqual({ value: 0 }); }); it('应该处理increment', () => { store.dispatch(increment()); expect(store.getState().counter).toEqual({ value: 1 }); }); it('应该处理带payload的incrementByAmount', () => { store.dispatch(incrementByAmount(3)); expect(store.getState().counter).toEqual({ value: 3 }); }); });

24.2 React Context测试

测试使用Context的组件:

const renderWithTheme = (ui, { theme = 'light', ...options } = {}) => { const Wrapper = ({ children }) => ( <ThemeProvider value={theme}>{children}</ThemeProvider> ); return render(ui, { wrapper: Wrapper, ...options }); }; test('应该根据主题显示正确的样式', () => { renderWithTheme(<ThemedButton>按钮</ThemedButton>, { theme: 'dark' }); expect(screen.getByRole('button')).toHaveStyle({ backgroundColor: '#333', color: '#fff' }); });

25. 测试与GraphQL集成

25.1 Mock GraphQL请求

使用MockedProvider测试Apollo组件:

const mocks = [ { request: { query: GET_USER, variables: { id: '1' } }, result: { data: { user: { id: '1', name: '测试用户' } } } } ]; test('应该显示用户数据', async () => { render( <MockedProvider mocks={mocks} addTypename={false}> <UserProfile userId="1" /> </MockedProvider> ); // 初始加载状态 expect(screen.getByText('加载中...')).toBeInTheDocument(); // 等待数据加载 await waitFor(() => { expect(screen.getByText('测试用户')).toBeInTheDocument(); }); });

25.2 测试GraphQL错误状态

test('应该处理GraphQL错误', async () => { const errorMock = { request: { query: GET_USER, variables: { id: '1' } }, error: new Error('发生错误') }; render( <MockedProvider mocks={[errorMock]} addTypename={false}> <UserProfile userId="1" /> </MockedProvider> ); await waitFor(() => { expect(screen.getByText('加载失败')).toBeInTheDocument(); }); });

26. 测试与Web Workers集成

26.1 测试Web Worker通信

// worker.js self.onmessage = function(e) { const result = heavyCalculation(e.data); self.postMessage(result); }; // worker.test.js test('worker应该正确处理消息', async () => { const worker = new Worker('./worker.js'); const testData = { value: 5 }; const promise = new Promise(resolve => { worker.onmessage = e => resolve(e.data); }); worker.postMessage(testData); const result = await promise; expect(result).toEqual({ value: 25 }); // 假设heavyCalculation是平方计算 worker.terminate(); });

26.2 Mock Web Worker

在测试中避免真实Worker的开销:

jest.mock('./worker.js', () => { return jest.fn().mockImplementation(() => ({ postMessage: jest.fn(), terminate: jest.fn(), onmessage: null })); }); test('组件应该使用worker处理数据', () => { const worker = require('./worker.js'); render(<DataProcessor />); fireEvent.click(screen.getByText('开始处理')); expect(worker.mock.calls.length).toBe(1); expect(worker().postMessage).toHaveBeenCalledWith( expect.objectContaining({ type: 'start' }) ); });

27. 测试与Web Components集成

27.1 测试自定义元素

// 注册自定义元素 class MyElement extends HTMLElement { connectedCallback() { this.innerHTML = `<button>点击我</button>`; } } customElements.define('my-element', MyElement); // 测试 test('自定义元素应该正确渲染', () => { document.body.innerHTML = '<my-element></my-element>'; const element = document.querySelector('my-element'); expect(element).toBeInTheDocument(); expect(element.shadowRoot.querySelector('button')).toBeInTheDocument(); });

27.2 测试与Web Components交互

test('应该与自定义元素交互', async () => { render( <div> <my-element id="test-el"></my-element> </div> ); const element = document.getElementById('test-el'); const button = element.shadowRoot.querySelector('button'); const mockClick = jest.fn(); element.addEventListener('click', mockClick); button.click(); expect(mockClick).toHaveBeenCalled(); });

28. 测试与WebSocket集成

28.1 Mock WebSocket连接

let mockServer; beforeEach(() => { mockServer = new WS('ws://localhost:1234'); }); afterEach(() => { WS.clean(); }); test('应该处理WebSocket消息', async () => { render(<ChatApp />); // 模拟服务器发送消息 await mockServer.connected; mockServer.send(JSON.stringify({ type: 'message', text: '测试消息' })); await waitFor(() => { expect(screen.getByText('测试消息')).toBeInTheDocument(); }); });

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询