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)解决方案
不稳定的测试是自动化测试的噩梦。以下是几种常见原因和解决方法:
定时器问题:
// 错误方式 setTimeout(() => { expect(element).toBeVisible(); }, 1000); // 正确方式 await waitFor(() => { expect(element).toBeVisible(); });网络请求不稳定:
// 使用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());状态污染:
// 每个测试前重置状态 beforeEach(() => { cleanup(); initTestState(); });
6.2 测试覆盖率优化技巧
提高测试覆盖率不是目的,而是手段。以下是一些实用建议:
忽略不必要的覆盖:
/* istanbul ignore next */ function shouldNeverBeTested() { return magic(); }聚焦关键路径:
- 优先测试业务核心逻辑
- 其次测试容易出错的边界条件
- 最后考虑全覆盖
使用覆盖率报告:
jest --coverage --collectCoverageFrom='src/**/*.{js,jsx,ts,tsx}'
7. 测试驱动开发(TDD)实战
7.1 TDD三步循环
红阶段:编写一个失败测试
// 测试一个尚未实现的formatDate函数 test('应该格式化日期为YYYY-MM-DD', () => { expect(formatDate(new Date(2023, 0, 1))).toBe('2023-01-01'); });绿阶段:实现最简单可通过的代码
function formatDate(date) { return '2023-01-01'; // 硬编码先通过测试 }重构阶段:改进实现而不改变行为
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常见误区
过度测试实现细节:
// 错误:测试内部状态而非行为 test('应该使用useState钩子', () => { render(<Counter />); expect(Counter.useState).toHaveBeenCalled(); }); // 正确:测试可见行为 test('点击按钮应该增加计数', () => { render(<Counter />); fireEvent.click(screen.getByText('+')); expect(screen.getByText('1')).toBeInTheDocument(); });忽视重构阶段:很多开发者停留在"绿阶段",导致代码质量下降
测试覆盖但无断言:
// 错误:执行了代码但没有验证 test('应该调用API', () => { fetchData(); // 没有断言 });
8. 视觉回归测试实践
8.1 Storybook + Chromatic方案
视觉回归测试能捕捉到像素级别的UI变化:
- 首先在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>;- 配置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.json9.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 测试代码重构技巧
- 提取公共工具函数:
// 代替重复的渲染代码 function renderButton(props = {}) { const defaultProps = { onClick: jest.fn(), children: '按钮' }; return render(<Button {...defaultProps} {...props} />); }- 使用工厂函数生成测试数据:
function createUser(overrides = {}) { return { id: faker.datatype.uuid(), name: faker.name.fullName(), email: faker.internet.email(), ...overrides }; }- 遵循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 serve15. 测试环境管理
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-tests15.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" --coverage18.2 覆盖率差异报告
生成与主分支的覆盖率差异:
# 生成当前分支覆盖率 npm run test:coverage # 与主分支比较 npx coverage-diff ./.coverage/coverage-final.json origin/main/.coverage/coverage-final.json19. 测试性能优化
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.js20. 测试类型进阶
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(); }); });