前端测试
约 726 字大约 2 分钟
测试单元测试E2ETDD
2024-08-13
测试类型
| 类型 | 目标 | 速度 | 隔离性 |
|---|---|---|---|
| 单元测试 | 函数/组件 | 快 | 高 |
| 集成测试 | 模块交互 | 中 | 中 |
| E2E 测试 | 完整流程 | 慢 | 低 |
Jest 单元测试
基本结构
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// math.test.js
import { add, subtract } from './math';
describe('Math operations', () => {
test('adds two numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('subtracts two numbers', () => {
expect(subtract(5, 3)).toBe(2);
});
});常用匹配器
// 相等
expect(value).toBe(5); // 原始值比较
expect(value).toEqual(obj); // 深比较
// 真假
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
// 数字
expect(value).toBeGreaterThan(3);
expect(value).toBeLessThan(10);
// 字符串
expect(str).toMatch(/pattern/);
// 数组
expect(arr).toContain(item);
expect(arr).toHaveLength(3);
// 异常
expect(() => throwError()).toThrow();异步测试
// Promise
test('fetches data', async () => {
const data = await fetchData();
expect(data).toEqual({ id: 1 });
});
// Async/await
test('fetches user', async () => {
const user = await getUser(1);
expect(user.name).toBe('John');
});Vue Testing Library
import { render, screen, fireEvent } from '@testing-library/vue';
import LoginForm from './LoginForm.vue';
test('submits form with valid data', async () => {
render(LoginForm);
await fireEvent.update(screen.getByLabelText('Email'), 'test@example.com');
await fireEvent.update(screen.getByLabelText('Password'), 'password123');
await fireEvent.click(screen.getByText('Submit'));
expect(screen.queryByText('Success')).toBeInTheDocument();
});React Testing Library
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
test('form submission', async () => {
render(<LoginForm />);
await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/password/i), 'password123');
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
expect(screen.getByText(/success/i)).toBeInTheDocument();
});E2E 测试 (Cypress)
基本结构
// cypress/e2e/login.cy.js
describe('Login', () => {
beforeEach(() => {
cy.visit('/login');
});
it('should login with valid credentials', () => {
cy.get('[data-testid="email"]').type('test@example.com');
cy.get('[data-testid="password"]').type('password123');
cy.get('[data-testid="submit"]').click();
cy.url().should('include', '/dashboard');
cy.get('[data-testid="welcome"]').should('contain', 'Welcome');
});
it('should show error with invalid credentials', () => {
cy.get('[data-testid="email"]').type('wrong@example.com');
cy.get('[data-testid="password"]').type('wrongpassword');
cy.get('[data-testid="submit"]').click();
cy.get('[data-testid="error"]').should('contain', 'Invalid credentials');
});
});常用命令
cy.visit(url) // 访问页面
cy.get(selector) // 获取元素
cy.click() // 点击
cy.type() // 输入文本
cy.should() // 断言
cy.request() // HTTP 请求
cy.intercept() // 拦截请求TDD (测试驱动开发)
开发流程
1. 编写一个失败的测试 (Red)
2. 编写最少代码让测试通过 (Green)
3. 重构代码 (Refactor)
4. 重复示例
// Step 1: 编写测试
test('should return true for palindrome', () => {
expect(isPalindrome('level')).toBe(true);
expect(isPalindrome('hello')).toBe(false);
});
// Step 2: 编写实现
const isPalindrome = (str) => {
const reversed = str.split('').reverse().join('');
return str === reversed;
};
// Step 3: 重构 (如需要)测试覆盖率
# Jest
npm test -- --coverage
# 输出示例
----------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
----------------|---------|----------|---------|---------|
All files | 85.23 | 72.50 | 100.00 | 85.23 |覆盖率指标
| 指标 | 含义 |
|---|---|
| Statements | 语句覆盖率 |
| Branches | 分支覆盖率 |
| Functions | 函数覆盖率 |
| Lines | 行覆盖率 |
Mock 技术
Jest Mock
// Mock 函数
const mockFn = jest.fn();
mockFn.mockReturnValue('result');
mockFn('arg'); // 返回 'result'
// Mock 模块
jest.mock('./api', () => ({
fetchUser: jest.fn().mockResolvedValue({ id: 1, name: 'John' }),
}));
// Mock 定时器
jest.useFakeTimers();
jest.advanceTimersByTime(1000);Mock Service Worker
// src/mocks/handlers.js
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/user', () => {
return HttpResponse.json({ id: 1, name: 'John' });
}),
];
// src/mocks/browser.js
import { setupWorker } from 'msw/browser';
export const worker = setupWorker(...handlers);CI 中的测试
# GitHub Actions
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run test:unit -- --coverage
- run: npm run test:e2e