引言
前端测试是现代Web开发中不可或缺的重要环节,它不仅能够保证代码质量,还能提高开发效率,降低维护成本。随着前端应用复杂度的不断增加,建立完善的测试体系变得越来越重要。
本文将深入探讨前端测试的各个层面,从单元测试到集成测试,再到端到端测试,提供一套完整的前端测试解决方案。我们将涵盖测试策略设计、工具选择、最佳实践以及自动化测试流程等关键内容。
1. 前端测试概述
1.1 测试金字塔理论
前端测试遵循测试金字塔理论,从底层到顶层分为:
- 单元测试(Unit Tests): 测试独立的函数、组件或模块
- 集成测试(Integration Tests): 测试组件间的交互
- 端到端测试(E2E Tests): 测试完整的用户流程
1.2 测试策略管理器
javascript
// 前端测试策略管理器
class FrontendTestingManager {
constructor(config = {}) {
this.config = {
unitTestRatio: 0.7,
integrationTestRatio: 0.2,
e2eTestRatio: 0.1,
coverageThreshold: {
statements: 80,
branches: 75,
functions: 80,
lines: 80
},
testEnvironments: ['jsdom', 'node', 'browser'],
enableParallelTesting: true,
enableWatchMode: true,
enableCoverageReport: true,
enableVisualTesting: true,
enablePerformanceTesting: true,
enableAccessibilityTesting: true,
...config
};
this.testSuites = new Map();
this.testResults = [];
this.coverageData = null;
this.testMetrics = {
totalTests: 0,
passedTests: 0,
failedTests: 0,
skippedTests: 0,
executionTime: 0,
coverage: {}
};
this.init();
}
// 初始化
init() {
this.setupTestEnvironments();
this.registerTestSuites();
this.setupTestReporting();
this.setupCoverageTracking();
}
// 设置测试环境
setupTestEnvironments() {
this.environments = {
unit: {
framework: 'jest',
environment: 'jsdom',
setupFiles: ['<rootDir>/src/setupTests.js'],
testMatch: ['**/__tests__/**/*.test.{js,jsx,ts,tsx}'],
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.js',
'!src/serviceWorker.js'
]
},
integration: {
framework: 'jest',
environment: 'jsdom',
testMatch: ['**/__tests__/**/*.integration.{js,jsx,ts,tsx}'],
setupFilesAfterEnv: ['<rootDir>/src/setupIntegrationTests.js']
},
e2e: {
framework: 'playwright',
browsers: ['chromium', 'firefox', 'webkit'],
testDir: './e2e',
testMatch: '**/*.e2e.{js,ts}',
baseURL: 'http://localhost:3000'
}
};
}
// 注册测试套件
registerTestSuites() {
// 单元测试套件
this.testSuites.set('unit', {
type: 'unit',
runner: new UnitTestRunner(this.environments.unit),
priority: 1,
parallel: true
});
// 集成测试套件
this.testSuites.set('integration', {
type: 'integration',
runner: new IntegrationTestRunner(this.environments.integration),
priority: 2,
parallel: true
});
// E2E测试套件
this.testSuites.set('e2e', {
type: 'e2e',
runner: new E2ETestRunner(this.environments.e2e),
priority: 3,
parallel: false
});
// 视觉测试套件
if (this.config.enableVisualTesting) {
this.testSuites.set('visual', {
type: 'visual',
runner: new VisualTestRunner(),
priority: 4,
parallel: true
});
}
// 性能测试套件
if (this.config.enablePerformanceTesting) {
this.testSuites.set('performance', {
type: 'performance',
runner: new PerformanceTestRunner(),
priority: 5,
parallel: false
});
}
// 可访问性测试套件
if (this.config.enableAccessibilityTesting) {
this.testSuites.set('accessibility', {
type: 'accessibility',
runner: new AccessibilityTestRunner(),
priority: 6,
parallel: true
});
}
}
// 运行所有测试
async runAllTests(options = {}) {
const startTime = Date.now();
try {
console.log('🧪 Starting test execution...');
// 重置测试指标
this.resetTestMetrics();
// 按优先级排序测试套件
const sortedSuites = Array.from(this.testSuites.entries())
.sort(([, a], [, b]) => a.priority - b.priority);
// 运行测试套件
for (const [name, suite] of sortedSuites) {
if (options.suites && !options.suites.includes(name)) {
continue;
}
console.log(`📋 Running ${name} tests...`);
const suiteResult = await this.runTestSuite(name, suite, options);
this.processTestResult(name, suiteResult);
}
// 生成测试报告
const report = await this.generateTestReport();
// 检查覆盖率阈值
this.validateCoverageThreshold();
const endTime = Date.now();
this.testMetrics.executionTime = endTime - startTime;
console.log('✅ Test execution completed');
console.log('📊 Test Summary:', this.getTestSummary());
return {
success: this.testMetrics.failedTests === 0,
metrics: this.testMetrics,
report: report
};
} catch (error) {
console.error('❌ Test execution failed:', error);
throw error;
}
}
// 运行单个测试套件
async runTestSuite(name, suite, options) {
try {
const result = await suite.runner.run({
...options,
parallel: suite.parallel && this.config.enableParallelTesting
});
return {
suite: name,
type: suite.type,
success: result.success,
tests: result.tests || [],
coverage: result.coverage || null,
duration: result.duration || 0,
errors: result.errors || []
};
} catch (error) {
return {
suite: name,
type: suite.type,
success: false,
tests: [],
coverage: null,
duration: 0,
errors: [error.message]
};
}
}
// 处理测试结果
processTestResult(suiteName, result) {
this.testResults.push(result);
// 更新测试指标
if (result.tests) {
result.tests.forEach(test => {
this.testMetrics.totalTests++;
switch (test.status) {
case 'passed':
this.testMetrics.passedTests++;
break;
case 'failed':
this.testMetrics.failedTests++;
break;
case 'skipped':
this.testMetrics.skippedTests++;
break;
}
});
}
// 合并覆盖率数据
if (result.coverage) {
this.mergeCoverageData(result.coverage);
}
// 记录错误
if (result.errors && result.errors.length > 0) {
console.error(`❌ Errors in ${suiteName} tests:`, result.errors);
}
}
// 合并覆盖率数据
mergeCoverageData(coverage) {
if (!this.coverageData) {
this.coverageData = coverage;
} else {
// 简单的覆盖率合并逻辑
Object.keys(coverage).forEach(file => {
if (this.coverageData[file]) {
// 合并文件覆盖率
this.coverageData[file] = this.mergeCoverageFile(
this.coverageData[file],
coverage[file]
);
} else {
this.coverageData[file] = coverage[file];
}
});
}
}
// 合并单个文件的覆盖率
mergeCoverageFile(existing, newCoverage) {
return {
statements: this.mergeCoverageMetric(existing.statements, newCoverage.statements),
branches: this.mergeCoverageMetric(existing.branches, newCoverage.branches),
functions: this.mergeCoverageMetric(existing.functions, newCoverage.functions),
lines: this.mergeCoverageMetric(existing.lines, newCoverage.lines)
};
}
// 合并覆盖率指标
mergeCoverageMetric(existing, newMetric) {
return {
total: Math.max(existing.total, newMetric.total),
covered: Math.max(existing.covered, newMetric.covered),
percentage: Math.max(existing.percentage, newMetric.percentage)
};
}
// 验证覆盖率阈值
validateCoverageThreshold() {
if (!this.coverageData || !this.config.enableCoverageReport) {
return;
}
const overallCoverage = this.calculateOverallCoverage();
const threshold = this.config.coverageThreshold;
const violations = [];
Object.keys(threshold).forEach(metric => {
if (overallCoverage[metric] < threshold[metric]) {
violations.push({
metric,
actual: overallCoverage[metric],
expected: threshold[metric]
});
}
});
if (violations.length > 0) {
console.warn('⚠️ Coverage threshold violations:', violations);
this.testMetrics.coverageViolations = violations;
}
this.testMetrics.coverage = overallCoverage;
}
// 计算整体覆盖率
calculateOverallCoverage() {
if (!this.coverageData) {
return { statements: 0, branches: 0, functions: 0, lines: 0 };
}
const totals = {
statements: { total: 0, covered: 0 },
branches: { total: 0, covered: 0 },
functions: { total: 0, covered: 0 },
lines: { total: 0, covered: 0 }
};
Object.values(this.coverageData).forEach(fileCoverage => {
Object.keys(totals).forEach(metric => {
totals[metric].total += fileCoverage[metric].total;
totals[metric].covered += fileCoverage[metric].covered;
});
});
const result = {};
Object.keys(totals).forEach(metric => {
const { total, covered } = totals[metric];
result[metric] = total > 0 ? Math.round((covered / total) * 100) : 0;
});
return result;
}
// 生成测试报告
async generateTestReport() {
const report = {
timestamp: new Date().toISOString(),
summary: this.getTestSummary(),
suites: this.testResults,
coverage: this.coverageData ? this.calculateOverallCoverage() : null,
metrics: this.testMetrics,
environment: {
node: process.version,
platform: process.platform,
ci: process.env.CI || false
}
};
// 保存报告到文件
if (this.config.enableTestReporting) {
await this.saveTestReport(report);
}
return report;
}
// 保存测试报告
async saveTestReport(report) {
try {
const fs = require('fs').promises;
const path = require('path');
const reportDir = path.join(process.cwd(), 'test-reports');
await fs.mkdir(reportDir, { recursive: true });
// JSON报告
const jsonReportPath = path.join(reportDir, 'test-report.json');
await fs.writeFile(jsonReportPath, JSON.stringify(report, null, 2));
// HTML报告
const htmlReport = this.generateHTMLReport(report);
const htmlReportPath = path.join(reportDir, 'test-report.html');
await fs.writeFile(htmlReportPath, htmlReport);
console.log('📄 Test reports saved:', { jsonReportPath, htmlReportPath });
} catch (error) {
console.error('Failed to save test report:', error);
}
}
// 生成HTML报告
generateHTMLReport(report) {
return `
<!DOCTYPE html>
<html>
<head>
<title>Test Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.summary { background: #f5f5f5; padding: 15px; border-radius: 5px; }
.suite { margin: 20px 0; border: 1px solid #ddd; border-radius: 5px; }
.suite-header { background: #e9e9e9; padding: 10px; font-weight: bold; }
.test { padding: 10px; border-bottom: 1px solid #eee; }
.passed { color: green; }
.failed { color: red; }
.skipped { color: orange; }
.coverage { margin: 20px 0; }
.coverage-bar { width: 200px; height: 20px; background: #ddd; border-radius: 10px; overflow: hidden; }
.coverage-fill { height: 100%; background: linear-gradient(to right, red, yellow, green); }
</style>
</head>
<body>
<h1>Frontend Test Report</h1>
<div class="summary">
<h2>Summary</h2>
<p>Total Tests: ${report.summary.total}</p>
<p>Passed: <span class="passed">${report.summary.passed}</span></p>
<p>Failed: <span class="failed">${report.summary.failed}</span></p>
<p>Skipped: <span class="skipped">${report.summary.skipped}</span></p>
<p>Success Rate: ${report.summary.successRate}%</p>
<p>Execution Time: ${report.summary.duration}ms</p>
</div>
${report.coverage ? `
<div class="coverage">
<h2>Coverage</h2>
<p>Statements: ${report.coverage.statements}%</p>
<p>Branches: ${report.coverage.branches}%</p>
<p>Functions: ${report.coverage.functions}%</p>
<p>Lines: ${report.coverage.lines}%</p>
</div>
` : ''}
<div class="suites">
<h2>Test Suites</h2>
${report.suites.map(suite => `
<div class="suite">
<div class="suite-header">${suite.suite} (${suite.type})</div>
${suite.tests.map(test => `
<div class="test ${test.status}">
${test.name} - ${test.status}
${test.duration ? ` (${test.duration}ms)` : ''}
${test.error ? `<br><small>${test.error}</small>` : ''}
</div>
`).join('')}
</div>
`).join('')}
</div>
<footer>
<p>Generated at: ${report.timestamp}</p>
</footer>
</body>
</html>
`;
}
// 设置测试报告
setupTestReporting() {
// 配置测试报告选项
this.reportingConfig = {
formats: ['json', 'html', 'junit'],
outputDir: './test-reports',
includeConsoleOutput: true,
includeCoverage: this.config.enableCoverageReport
};
}
// 设置覆盖率跟踪
setupCoverageTracking() {
if (!this.config.enableCoverageReport) return;
// 配置覆盖率收集
this.coverageConfig = {
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.{js,jsx,ts,tsx}',
'!src/**/*.test.{js,jsx,ts,tsx}'
],
coverageReporters: ['text', 'lcov', 'html', 'json'],
coverageDirectory: './coverage'
};
}
// 重置测试指标
resetTestMetrics() {
this.testMetrics = {
totalTests: 0,
passedTests: 0,
failedTests: 0,
skippedTests: 0,
executionTime: 0,
coverage: {},
coverageViolations: []
};
this.testResults = [];
this.coverageData = null;
}
// 获取测试摘要
getTestSummary() {
const { totalTests, passedTests, failedTests, skippedTests, executionTime } = this.testMetrics;
return {
total: totalTests,
passed: passedTests,
failed: failedTests,
skipped: skippedTests,
successRate: totalTests > 0 ? Math.round((passedTests / totalTests) * 100) : 0,
duration: executionTime
};
}
// 监听模式
async startWatchMode() {
if (!this.config.enableWatchMode) {
console.log('Watch mode is disabled');
return;
}
console.log('👀 Starting watch mode...');
const chokidar = require('chokidar');
// 监听源文件变化
const watcher = chokidar.watch(['src/**/*.{js,jsx,ts,tsx}', '**/*.test.{js,jsx,ts,tsx}'], {
ignored: /node_modules/,
persistent: true
});
let debounceTimer;
watcher.on('change', (path) => {
console.log(`📝 File changed: ${path}`);
// 防抖处理
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
try {
console.log('🔄 Re-running tests...');
await this.runAllTests({ watch: true });
} catch (error) {
console.error('Watch mode test execution failed:', error);
}
}, 1000);
});
return watcher;
}
// 获取状态
getStatus() {
return {
config: this.config,
environments: this.environments,
testSuites: Array.from(this.testSuites.keys()),
metrics: this.testMetrics,
lastResults: this.testResults.slice(-5)
};
}
// 清理
cleanup() {
this.testSuites.clear();
this.testResults = [];
this.coverageData = null;
this.resetTestMetrics();
}
}
2.2 React组件测试实践
javascript
// React组件测试示例
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import { configureStore } from '@reduxjs/toolkit';
import userEvent from '@testing-library/user-event';
// 示例组件:用户登录表单
const LoginForm = ({ onSubmit, loading = false }) => {
const [formData, setFormData] = React.useState({
email: '',
password: ''
});
const [errors, setErrors] = React.useState({});
const validateForm = () => {
const newErrors = {};
if (!formData.email) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}
if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 6) {
newErrors.password = 'Password must be at least 6 characters';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validateForm()) {
onSubmit(formData);
}
};
const handleChange = (field) => (e) => {
setFormData(prev => ({
...prev,
[field]: e.target.value
}));
// 清除错误
if (errors[field]) {
setErrors(prev => ({
...prev,
[field]: ''
}));
}
};
return (
<form onSubmit={handleSubmit} data-testid="login-form">
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
value={formData.email}
onChange={handleChange('email')}
data-testid="email-input"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<div id="email-error" role="alert" data-testid="email-error">
{errors.email}
</div>
)}
</div>
<div>
<label htmlFor="password">Password:</label>
<input
id="password"
type="password"
value={formData.password}
onChange={handleChange('password')}
data-testid="password-input"
aria-invalid={!!errors.password}
aria-describedby={errors.password ? 'password-error' : undefined}
/>
{errors.password && (
<div id="password-error" role="alert" data-testid="password-error">
{errors.password}
</div>
)}
</div>
<button
type="submit"
disabled={loading}
data-testid="submit-button"
>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
);
};
// 测试套件
describe('LoginForm', () => {
let mockOnSubmit;
beforeEach(() => {
mockOnSubmit = jest.fn();
});
afterEach(() => {
jest.clearAllMocks();
});
// 基础渲染测试
test('renders login form with all fields', () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
expect(screen.getByTestId('login-form')).toBeInTheDocument();
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument();
});
// 用户交互测试
test('allows user to enter email and password', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
const emailInput = screen.getByTestId('email-input');
const passwordInput = screen.getByTestId('password-input');
await user.type(emailInput, 'test@example.com');
await user.type(passwordInput, 'password123');
expect(emailInput).toHaveValue('test@example.com');
expect(passwordInput).toHaveValue('password123');
});
// 表单验证测试
test('shows validation errors for empty fields', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
const submitButton = screen.getByTestId('submit-button');
await user.click(submitButton);
await waitFor(() => {
expect(screen.getByTestId('email-error')).toHaveTextContent('Email is required');
expect(screen.getByTestId('password-error')).toHaveTextContent('Password is required');
});
expect(mockOnSubmit).not.toHaveBeenCalled();
});
test('shows validation error for invalid email', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
const emailInput = screen.getByTestId('email-input');
const submitButton = screen.getByTestId('submit-button');
await user.type(emailInput, 'invalid-email');
await user.click(submitButton);
await waitFor(() => {
expect(screen.getByTestId('email-error')).toHaveTextContent('Email is invalid');
});
});
test('shows validation error for short password', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
const passwordInput = screen.getByTestId('password-input');
const submitButton = screen.getByTestId('submit-button');
await user.type(passwordInput, '123');
await user.click(submitButton);
await waitFor(() => {
expect(screen.getByTestId('password-error')).toHaveTextContent('Password must be at least 6 characters');
});
});
// 成功提交测试
test('submits form with valid data', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
const emailInput = screen.getByTestId('email-input');
const passwordInput = screen.getByTestId('password-input');
const submitButton = screen.getByTestId('submit-button');
await user.type(emailInput, 'test@example.com');
await user.type(passwordInput, 'password123');
await user.click(submitButton);
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
});
});
});
// 加载状态测试
test('shows loading state when submitting', () => {
render(<LoginForm onSubmit={mockOnSubmit} loading={true} />);
const submitButton = screen.getByTestId('submit-button');
expect(submitButton).toBeDisabled();
expect(submitButton).toHaveTextContent('Logging in...');
});
// 错误清除测试
test('clears errors when user starts typing', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
const emailInput = screen.getByTestId('email-input');
const submitButton = screen.getByTestId('submit-button');
// 触发验证错误
await user.click(submitButton);
await waitFor(() => {
expect(screen.getByTestId('email-error')).toBeInTheDocument();
});
// 开始输入,错误应该清除
await user.type(emailInput, 't');
await waitFor(() => {
expect(screen.queryByTestId('email-error')).not.toBeInTheDocument();
});
});
// 可访问性测试
test('has proper accessibility attributes', () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
const emailInput = screen.getByTestId('email-input');
const passwordInput = screen.getByTestId('password-input');
expect(emailInput).toHaveAttribute('aria-invalid', 'false');
expect(passwordInput).toHaveAttribute('aria-invalid', 'false');
});
test('associates error messages with inputs', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
const submitButton = screen.getByTestId('submit-button');
await user.click(submitButton);
await waitFor(() => {
const emailInput = screen.getByTestId('email-input');
const emailError = screen.getByTestId('email-error');
expect(emailInput).toHaveAttribute('aria-invalid', 'true');
expect(emailInput).toHaveAttribute('aria-describedby', 'email-error');
expect(emailError).toHaveAttribute('role', 'alert');
});
});
// 快照测试
test('matches snapshot', () => {
const { container } = render(<LoginForm onSubmit={mockOnSubmit} />);
expect(container.firstChild).toMatchSnapshot();
});
test('matches snapshot with loading state', () => {
const { container } = render(<LoginForm onSubmit={mockOnSubmit} loading={true} />);
expect(container.firstChild).toMatchSnapshot();
});
});
// Redux连接组件测试
describe('LoginForm with Redux', () => {
let store;
let mockOnSubmit;
beforeEach(() => {
mockOnSubmit = jest.fn();
// 创建测试store
store = configureStore({
reducer: {
auth: (state = { loading: false, error: null }, action) => {
switch (action.type) {
case 'auth/loginStart':
return { ...state, loading: true, error: null };
case 'auth/loginSuccess':
return { ...state, loading: false, error: null };
case 'auth/loginFailure':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
}
});
});
const renderWithProviders = (component) => {
return render(
<Provider store={store}>
<BrowserRouter>
{component}
</BrowserRouter>
</Provider>
);
};
test('integrates with Redux store', () => {
renderWithProviders(<LoginForm onSubmit={mockOnSubmit} />);
// 验证组件正常渲染
expect(screen.getByTestId('login-form')).toBeInTheDocument();
// 验证初始状态
const state = store.getState();
expect(state.auth.loading).toBe(false);
expect(state.auth.error).toBe(null);
});
});
3. 集成测试深度实践
3.1 集成测试运行器
javascript
// 集成测试运行器
class IntegrationTestRunner {
constructor(config = {}) {
this.config = {
framework: 'jest',
environment: 'jsdom',
testMatch: ['**/__tests__/**/*.integration.{js,jsx,ts,tsx}'],
setupFilesAfterEnv: [],
testTimeout: 30000,
maxWorkers: 1,
...config
};
this.testSuites = [];
this.mockServices = new Map();
this.testDatabase = null;
this.testServer = null;
this.init();
}
// 初始化
init() {
this.setupTestEnvironment();
this.setupMockServices();
this.setupTestDatabase();
this.setupTestServer();
}
// 设置测试环境
setupTestEnvironment() {
// 配置测试环境变量
process.env.NODE_ENV = 'test';
process.env.API_BASE_URL = 'http://localhost:3001';
process.env.DATABASE_URL = 'sqlite::memory:';
// 配置全局测试设置
global.testConfig = {
apiTimeout: 5000,
dbTimeout: 3000,
retryAttempts: 3
};
}
// 设置Mock服务
setupMockServices() {
this.mockServices.set('api', {
baseUrl: process.env.API_BASE_URL,
endpoints: new Map(),
middleware: [],
requests: []
});
this.mockServices.set('auth', {
users: new Map(),
sessions: new Map(),
tokens: new Map()
});
this.mockServices.set('storage', {
data: new Map(),
config: {
maxSize: 1024 * 1024, // 1MB
ttl: 3600000 // 1 hour
}
});
}
// 设置测试数据库
setupTestDatabase() {
this.testDatabase = {
connection: null,
tables: new Map(),
async connect() {
// 模拟数据库连接
this.connection = {
connected: true,
database: 'test_db',
tables: this.tables
};
console.log('📊 Test database connected');
return this.connection;
},
async disconnect() {
if (this.connection) {
this.connection.connected = false;
this.connection = null;
}
console.log('📊 Test database disconnected');
},
async seed(data = {}) {
// 填充测试数据
Object.keys(data).forEach(table => {
this.tables.set(table, data[table]);
});
console.log('🌱 Test database seeded');
},
async clean() {
// 清理测试数据
this.tables.clear();
console.log('🧹 Test database cleaned');
},
async query(sql, params = []) {
// 模拟数据库查询
console.log('🔍 Database query:', sql, params);
// 简单的查询模拟
if (sql.includes('SELECT')) {
const tableName = sql.match(/FROM\s+(\w+)/i)?.[1];
return this.tables.get(tableName) || [];
}
if (sql.includes('INSERT')) {
const tableName = sql.match(/INTO\s+(\w+)/i)?.[1];
const table = this.tables.get(tableName) || [];
table.push(params);
this.tables.set(tableName, table);
return { insertId: table.length };
}
return { affectedRows: 1 };
}
};
}
// 设置测试服务器
setupTestServer() {
this.testServer = {
port: 3001,
routes: new Map(),
middleware: [],
server: null,
async start() {
// 模拟服务器启动
this.server = {
listening: true,
port: this.port,
routes: this.routes
};
console.log(`🚀 Test server started on port ${this.port}`);
return this.server;
},
async stop() {
if (this.server) {
this.server.listening = false;
this.server = null;
}
console.log('🛑 Test server stopped');
},
addRoute(method, path, handler) {
const key = `${method.toUpperCase()} ${path}`;
this.routes.set(key, handler);
},
addMiddleware(middleware) {
this.middleware.push(middleware);
}
};
// 添加默认路由
this.setupDefaultRoutes();
}
// 设置默认路由
setupDefaultRoutes() {
// 健康检查
this.testServer.addRoute('GET', '/health', () => ({
status: 'ok',
timestamp: new Date().toISOString()
}));
// 用户认证
this.testServer.addRoute('POST', '/auth/login', (req) => {
const { email, password } = req.body;
const authService = this.mockServices.get('auth');
// 简单的认证逻辑
if (email === 'test@example.com' && password === 'password123') {
const token = `token_${Date.now()}`;
const user = { id: 1, email, name: 'Test User' };
authService.tokens.set(token, user);
return {
success: true,
token,
user
};
}
return {
success: false,
error: 'Invalid credentials'
};
});
// 用户信息
this.testServer.addRoute('GET', '/auth/me', (req) => {
const token = req.headers.authorization?.replace('Bearer ', '');
const authService = this.mockServices.get('auth');
const user = authService.tokens.get(token);
if (user) {
return { success: true, user };
}
return { success: false, error: 'Unauthorized' };
});
// 数据API
this.testServer.addRoute('GET', '/api/users', async () => {
const users = await this.testDatabase.query('SELECT * FROM users');
return { success: true, data: users };
});
this.testServer.addRoute('POST', '/api/users', async (req) => {
const result = await this.testDatabase.query(
'INSERT INTO users (name, email) VALUES (?, ?)',
[req.body.name, req.body.email]
);
return {
success: true,
data: { id: result.insertId, ...req.body }
};
});
}
// 运行集成测试
async run(options = {}) {
const startTime = Date.now();
try {
console.log('🔗 Running integration tests...');
// 启动测试环境
await this.startTestEnvironment();
// 运行测试
const result = await this.runTests(options);
// 清理测试环境
await this.cleanupTestEnvironment();
const endTime = Date.now();
result.duration = endTime - startTime;
console.log(`✅ Integration tests completed in ${result.duration}ms`);
return result;
} catch (error) {
console.error('❌ Integration test execution failed:', error);
// 确保清理
await this.cleanupTestEnvironment();
throw error;
}
}
// 启动测试环境
async startTestEnvironment() {
console.log('🏗️ Setting up integration test environment...');
// 连接测试数据库
await this.testDatabase.connect();
// 填充测试数据
await this.seedTestData();
// 启动测试服务器
await this.testServer.start();
// 设置全局fetch mock
this.setupFetchMock();
console.log('✅ Integration test environment ready');
}
// 填充测试数据
async seedTestData() {
const testData = {
users: [
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'user' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'admin' },
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com', role: 'user' }
],
posts: [
{ id: 1, title: 'Test Post 1', content: 'Content 1', userId: 1 },
{ id: 2, title: 'Test Post 2', content: 'Content 2', userId: 2 }
],
comments: [
{ id: 1, content: 'Test comment 1', postId: 1, userId: 2 },
{ id: 2, content: 'Test comment 2', postId: 1, userId: 3 }
]
};
await this.testDatabase.seed(testData);
}
// 设置Fetch Mock
setupFetchMock() {
const originalFetch = global.fetch;
global.fetch = jest.fn(async (url, options = {}) => {
const method = options.method || 'GET';
const routeKey = `${method.toUpperCase()} ${url.replace(process.env.API_BASE_URL, '')}`;
const handler = this.testServer.routes.get(routeKey);
if (handler) {
const req = {
url,
method,
headers: options.headers || {},
body: options.body ? JSON.parse(options.body) : null
};
try {
const response = await handler(req);
return {
ok: true,
status: 200,
json: async () => response,
text: async () => JSON.stringify(response)
};
} catch (error) {
return {
ok: false,
status: 500,
json: async () => ({ error: error.message }),
text: async () => JSON.stringify({ error: error.message })
};
}
}
// 回退到原始fetch
return originalFetch(url, options);
});
}
// 运行测试
async runTests(options = {}) {
const jest = require('jest');
const jestConfig = {
testEnvironment: this.config.environment,
testMatch: this.config.testMatch,
setupFilesAfterEnv: this.config.setupFilesAfterEnv,
testTimeout: this.config.testTimeout,
maxWorkers: this.config.maxWorkers,
verbose: true,
runInBand: true // 集成测试通常需要串行运行
};
return new Promise((resolve, reject) => {
jest.runCLI(jestConfig, [process.cwd()])
.then(({ results }) => {
const tests = [];
const errors = [];
results.testResults.forEach(testFile => {
testFile.testResults.forEach(test => {
tests.push({
name: test.title,
file: testFile.testFilePath,
status: test.status,
duration: test.duration,
error: test.failureMessages.length > 0 ? test.failureMessages[0] : null
});
});
if (testFile.failureMessage) {
errors.push(testFile.failureMessage);
}
});
resolve({
success: results.success,
tests,
errors,
numTotalTests: results.numTotalTests,
numPassedTests: results.numPassedTests,
numFailedTests: results.numFailedTests
});
})
.catch(reject);
});
}
// 清理测试环境
async cleanupTestEnvironment() {
console.log('🧹 Cleaning up integration test environment...');
// 停止测试服务器
await this.testServer.stop();
// 清理测试数据库
await this.testDatabase.clean();
await this.testDatabase.disconnect();
// 恢复全局对象
if (global.fetch && global.fetch.mockRestore) {
global.fetch.mockRestore();
}
// 清理Mock服务
this.mockServices.forEach(service => {
if (typeof service === 'object' && service !== null) {
Object.keys(service).forEach(key => {
if (service[key] instanceof Map) {
service[key].clear();
} else if (Array.isArray(service[key])) {
service[key].length = 0;
}
});
}
});
console.log('✅ Integration test environment cleaned up');
}
// 获取状态
getStatus() {
return {
config: this.config,
database: {
connected: this.testDatabase.connection?.connected || false,
tables: this.testDatabase.tables.size
},
server: {
running: this.testServer.server?.listening || false,
routes: this.testServer.routes.size
},
mockServices: Array.from(this.mockServices.keys())
};
}
}
3.2 用户管理集成测试示例
javascript
// __tests__/integration/user-management.integration.test.js
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import { store } from '../../src/store';
import UserManagement from '../../src/components/UserManagement';
import { IntegrationTestRunner } from '../../src/utils/testing';
// 集成测试套件
describe('User Management Integration Tests', () => {
let testRunner;
beforeAll(async () => {
// 初始化集成测试环境
testRunner = new IntegrationTestRunner({
testTimeout: 30000,
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js']
});
await testRunner.startTestEnvironment();
});
afterAll(async () => {
// 清理集成测试环境
await testRunner.cleanupTestEnvironment();
});
beforeEach(async () => {
// 每个测试前重置数据
await testRunner.testDatabase.clean();
await testRunner.seedTestData();
});
// 测试组件
const renderUserManagement = () => {
return render(
<Provider store={store}>
<BrowserRouter>
<UserManagement />
</BrowserRouter>
</Provider>
);
};
describe('用户认证流程', () => {
test('应该能够成功登录并获取用户信息', async () => {
renderUserManagement();
// 查找登录表单
const emailInput = screen.getByLabelText(/邮箱/i);
const passwordInput = screen.getByLabelText(/密码/i);
const loginButton = screen.getByRole('button', { name: /登录/i });
// 输入登录信息
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'password123' } });
// 点击登录
fireEvent.click(loginButton);
// 等待登录成功
await waitFor(() => {
expect(screen.getByText(/欢迎, Test User/i)).toBeInTheDocument();
});
// 验证用户信息显示
expect(screen.getByText('test@example.com')).toBeInTheDocument();
// 验证API调用
expect(global.fetch).toHaveBeenCalledWith(
'http://localhost:3001/auth/login',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Content-Type': 'application/json'
}),
body: JSON.stringify({
email: 'test@example.com',
password: 'password123'
})
})
);
});
test('应该处理登录失败的情况', async () => {
renderUserManagement();
const emailInput = screen.getByLabelText(/邮箱/i);
const passwordInput = screen.getByLabelText(/密码/i);
const loginButton = screen.getByRole('button', { name: /登录/i });
// 输入错误的登录信息
fireEvent.change(emailInput, { target: { value: 'wrong@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'wrongpassword' } });
fireEvent.click(loginButton);
// 等待错误消息显示
await waitFor(() => {
expect(screen.getByText(/登录失败/i)).toBeInTheDocument();
});
// 验证用户未登录
expect(screen.queryByText(/欢迎/i)).not.toBeInTheDocument();
});
test('应该能够获取当前用户信息', async () => {
// 先模拟已登录状态
const authService = testRunner.mockServices.get('auth');
const token = 'test_token_123';
const user = { id: 1, email: 'test@example.com', name: 'Test User' };
authService.tokens.set(token, user);
// 设置localStorage中的token
localStorage.setItem('authToken', token);
renderUserManagement();
// 等待用户信息加载
await waitFor(() => {
expect(screen.getByText(/Test User/i)).toBeInTheDocument();
});
// 验证API调用
expect(global.fetch).toHaveBeenCalledWith(
'http://localhost:3001/auth/me',
expect.objectContaining({
headers: expect.objectContaining({
'Authorization': 'Bearer test_token_123'
})
})
);
});
});
describe('用户数据管理', () => {
test('应该能够获取用户列表', async () => {
renderUserManagement();
// 等待用户列表加载
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
expect(screen.getByText('Bob Johnson')).toBeInTheDocument();
});
// 验证用户信息显示
expect(screen.getByText('john@example.com')).toBeInTheDocument();
expect(screen.getByText('jane@example.com')).toBeInTheDocument();
expect(screen.getByText('bob@example.com')).toBeInTheDocument();
});
test('应该能够创建新用户', async () => {
renderUserManagement();
// 查找创建用户表单
const nameInput = screen.getByLabelText(/姓名/i);
const emailInput = screen.getByLabelText(/邮箱/i);
const createButton = screen.getByRole('button', { name: /创建用户/i });
// 输入新用户信息
fireEvent.change(nameInput, { target: { value: 'New User' } });
fireEvent.change(emailInput, { target: { value: 'new@example.com' } });
// 点击创建
fireEvent.click(createButton);
// 等待用户创建成功
await waitFor(() => {
expect(screen.getByText('New User')).toBeInTheDocument();
});
// 验证API调用
expect(global.fetch).toHaveBeenCalledWith(
'http://localhost:3001/api/users',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
name: 'New User',
email: 'new@example.com'
})
})
);
// 验证数据库中的数据
const users = await testRunner.testDatabase.query('SELECT * FROM users');
expect(users).toContainEqual(
expect.objectContaining({
name: 'New User',
email: 'new@example.com'
})
);
});
test('应该能够编辑用户信息', async () => {
renderUserManagement();
// 等待用户列表加载
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
// 点击编辑按钮
const editButton = screen.getByTestId('edit-user-1');
fireEvent.click(editButton);
// 修改用户信息
const nameInput = screen.getByDisplayValue('John Doe');
fireEvent.change(nameInput, { target: { value: 'John Smith' } });
// 保存修改
const saveButton = screen.getByRole('button', { name: /保存/i });
fireEvent.click(saveButton);
// 等待修改成功
await waitFor(() => {
expect(screen.getByText('John Smith')).toBeInTheDocument();
});
// 验证原名称不再显示
expect(screen.queryByText('John Doe')).not.toBeInTheDocument();
});
test('应该能够删除用户', async () => {
renderUserManagement();
// 等待用户列表加载
await waitFor(() => {
expect(screen.getByText('Bob Johnson')).toBeInTheDocument();
});
// 点击删除按钮
const deleteButton = screen.getByTestId('delete-user-3');
fireEvent.click(deleteButton);
// 确认删除
const confirmButton = screen.getByRole('button', { name: /确认删除/i });
fireEvent.click(confirmButton);
// 等待用户被删除
await waitFor(() => {
expect(screen.queryByText('Bob Johnson')).not.toBeInTheDocument();
});
});
});
describe('组件集成', () => {
test('应该正确处理组件间的数据流', async () => {
renderUserManagement();
// 测试用户选择
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
// 选择用户
const userRow = screen.getByTestId('user-row-1');
fireEvent.click(userRow);
// 验证用户详情显示
await waitFor(() => {
expect(screen.getByTestId('user-details')).toBeInTheDocument();
});
// 验证用户详情内容
expect(screen.getByText('用户ID: 1')).toBeInTheDocument();
expect(screen.getByText('角色: user')).toBeInTheDocument();
});
test('应该正确处理加载状态', async () => {
renderUserManagement();
// 验证初始加载状态
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
// 等待加载完成
await waitFor(() => {
expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument();
});
// 验证内容已加载
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
describe('错误处理', () => {
test('应该处理网络错误', async () => {
// 模拟网络错误
global.fetch.mockRejectedValueOnce(new Error('Network Error'));
renderUserManagement();
// 等待错误消息显示
await waitFor(() => {
expect(screen.getByText(/网络错误/i)).toBeInTheDocument();
});
// 验证重试按钮存在
expect(screen.getByRole('button', { name: /重试/i })).toBeInTheDocument();
});
test('应该处理服务器错误', async () => {
// 模拟服务器错误
global.fetch.mockResolvedValueOnce({
ok: false,
status: 500,
json: async () => ({ error: 'Internal Server Error' })
});
renderUserManagement();
// 等待错误消息显示
await waitFor(() => {
expect(screen.getByText(/服务器错误/i)).toBeInTheDocument();
});
});
});
describe('性能集成测试', () => {
test('应该在合理时间内加载用户列表', async () => {
const startTime = Date.now();
renderUserManagement();
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
const endTime = Date.now();
const loadTime = endTime - startTime;
// 验证加载时间小于2秒
expect(loadTime).toBeLessThan(2000);
});
test('应该正确处理大量用户数据', async () => {
// 生成大量测试数据
const largeUserData = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: i % 2 === 0 ? 'user' : 'admin'
}));
await testRunner.testDatabase.seed({ users: largeUserData });
const startTime = Date.now();
renderUserManagement();
// 等待虚拟滚动加载
await waitFor(() => {
expect(screen.getByText('User 1')).toBeInTheDocument();
}, { timeout: 5000 });
const endTime = Date.now();
const loadTime = endTime - startTime;
// 即使有大量数据,加载时间也应该合理
expect(loadTime).toBeLessThan(3000);
});
});
});
4. E2E测试深度实践
4.1 E2E测试运行器
javascript
// E2E测试运行器
class E2ETestRunner {
constructor(config = {}) {
this.config = {
browser: 'chromium',
headless: true,
viewport: { width: 1280, height: 720 },
baseURL: 'http://localhost:3000',
timeout: 30000,
retries: 2,
video: 'retain-on-failure',
screenshot: 'only-on-failure',
trace: 'retain-on-failure',
...config
};
this.browser = null;
this.context = null;
this.page = null;
this.testResults = [];
this.screenshots = [];
this.videos = [];
this.init();
}
// 初始化
async init() {
const { chromium, firefox, webkit } = require('playwright');
// 选择浏览器
const browsers = { chromium, firefox, webkit };
this.browserType = browsers[this.config.browser] || chromium;
console.log(`🎭 Initializing E2E tests with ${this.config.browser}`);
}
// 启动浏览器
async startBrowser() {
try {
this.browser = await this.browserType.launch({
headless: this.config.headless,
slowMo: this.config.slowMo || 0
});
this.context = await this.browser.newContext({
viewport: this.config.viewport,
baseURL: this.config.baseURL,
recordVideo: this.config.video ? {
dir: './test-results/videos/',
size: this.config.viewport
} : undefined
});
// 启用追踪
if (this.config.trace) {
await this.context.tracing.start({
screenshots: true,
snapshots: true,
sources: true
});
}
this.page = await this.context.newPage();
// 设置默认超时
this.page.setDefaultTimeout(this.config.timeout);
// 添加控制台日志监听
this.page.on('console', msg => {
console.log(`🖥️ Console ${msg.type()}: ${msg.text()}`);
});
// 添加页面错误监听
this.page.on('pageerror', error => {
console.error('🚨 Page error:', error.message);
});
// 添加请求失败监听
this.page.on('requestfailed', request => {
console.error(`🌐 Request failed: ${request.url()} - ${request.failure()?.errorText}`);
});
console.log('🚀 Browser started successfully');
} catch (error) {
console.error('❌ Failed to start browser:', error);
throw error;
}
}
// 停止浏览器
async stopBrowser() {
try {
if (this.config.trace && this.context) {
await this.context.tracing.stop({
path: `./test-results/traces/trace-${Date.now()}.zip`
});
}
if (this.context) {
await this.context.close();
}
if (this.browser) {
await this.browser.close();
}
console.log('🛑 Browser stopped');
} catch (error) {
console.error('❌ Error stopping browser:', error);
}
}
// 运行E2E测试
async run(testSuites = []) {
const startTime = Date.now();
try {
console.log('🎬 Starting E2E tests...');
await this.startBrowser();
const results = {
total: 0,
passed: 0,
failed: 0,
skipped: 0,
tests: [],
duration: 0
};
for (const suite of testSuites) {
console.log(`📋 Running test suite: ${suite.name}`);
const suiteResult = await this.runTestSuite(suite);
results.total += suiteResult.total;
results.passed += suiteResult.passed;
results.failed += suiteResult.failed;
results.skipped += suiteResult.skipped;
results.tests.push(...suiteResult.tests);
}
await this.stopBrowser();
const endTime = Date.now();
results.duration = endTime - startTime;
console.log(`✅ E2E tests completed in ${results.duration}ms`);
console.log(`📊 Results: ${results.passed} passed, ${results.failed} failed, ${results.skipped} skipped`);
return results;
} catch (error) {
console.error('❌ E2E test execution failed:', error);
await this.stopBrowser();
throw error;
}
}
// 运行测试套件
async runTestSuite(suite) {
const results = {
name: suite.name,
total: 0,
passed: 0,
failed: 0,
skipped: 0,
tests: []
};
// 运行套件前置操作
if (suite.beforeAll) {
await suite.beforeAll(this.page, this.context);
}
for (const test of suite.tests) {
const testResult = await this.runTest(test, suite);
results.total++;
results.tests.push(testResult);
if (testResult.status === 'passed') {
results.passed++;
} else if (testResult.status === 'failed') {
results.failed++;
} else {
results.skipped++;
}
}
// 运行套件后置操作
if (suite.afterAll) {
await suite.afterAll(this.page, this.context);
}
return results;
}
// 运行单个测试
async runTest(test, suite) {
const startTime = Date.now();
const testResult = {
name: test.name,
suite: suite.name,
status: 'pending',
duration: 0,
error: null,
screenshots: [],
video: null
};
try {
console.log(` 🧪 Running test: ${test.name}`);
// 运行测试前置操作
if (suite.beforeEach) {
await suite.beforeEach(this.page, this.context);
}
// 运行测试
await test.fn(this.page, this.context, this);
testResult.status = 'passed';
console.log(` ✅ Test passed: ${test.name}`);
} catch (error) {
testResult.status = 'failed';
testResult.error = error.message;
console.error(` ❌ Test failed: ${test.name}`);
console.error(` Error: ${error.message}`);
// 截图
if (this.config.screenshot === 'only-on-failure' || this.config.screenshot === 'always') {
const screenshotPath = `./test-results/screenshots/${suite.name}-${test.name}-${Date.now()}.png`;
await this.page.screenshot({ path: screenshotPath, fullPage: true });
testResult.screenshots.push(screenshotPath);
}
} finally {
// 运行测试后置操作
if (suite.afterEach) {
await suite.afterEach(this.page, this.context);
}
const endTime = Date.now();
testResult.duration = endTime - startTime;
}
return testResult;
}
// 页面操作助手
async navigateTo(url) {
await this.page.goto(url);
await this.page.waitForLoadState('networkidle');
}
async waitForElement(selector, options = {}) {
return await this.page.waitForSelector(selector, {
timeout: this.config.timeout,
...options
});
}
async clickElement(selector) {
await this.page.click(selector);
}
async fillInput(selector, value) {
await this.page.fill(selector, value);
}
async getText(selector) {
return await this.page.textContent(selector);
}
async takeScreenshot(name) {
const screenshotPath = `./test-results/screenshots/${name}-${Date.now()}.png`;
await this.page.screenshot({ path: screenshotPath, fullPage: true });
this.screenshots.push(screenshotPath);
return screenshotPath;
}
async waitForResponse(urlPattern, action) {
const responsePromise = this.page.waitForResponse(urlPattern);
await action();
return await responsePromise;
}
async interceptRequest(urlPattern, handler) {
await this.page.route(urlPattern, handler);
}
// 性能测试助手
async measurePageLoad(url) {
const startTime = Date.now();
await this.page.goto(url);
await this.page.waitForLoadState('networkidle');
const endTime = Date.now();
const loadTime = endTime - startTime;
// 获取性能指标
const metrics = await this.page.evaluate(() => {
const navigation = performance.getEntriesByType('navigation')[0];
return {
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
firstPaint: performance.getEntriesByName('first-paint')[0]?.startTime || 0,
firstContentfulPaint: performance.getEntriesByName('first-contentful-paint')[0]?.startTime || 0
};
});
return {
totalLoadTime: loadTime,
...metrics
};
}
// 可访问性测试助手
async checkAccessibility() {
const { injectAxe, checkA11y } = require('axe-playwright');
await injectAxe(this.page);
try {
await checkA11y(this.page, null, {
detailedReport: true,
detailedReportOptions: { html: true }
});
return { passed: true, violations: [] };
} catch (error) {
return {
passed: false,
violations: error.violations || []
};
}
}
// 获取状态
getStatus() {
return {
config: this.config,
browser: {
connected: !!this.browser,
type: this.config.browser
},
context: {
created: !!this.context
},
page: {
created: !!this.page,
url: this.page?.url() || null
},
results: {
tests: this.testResults.length,
screenshots: this.screenshots.length,
videos: this.videos.length
}
};
}
}
4.2 电商应用E2E测试示例
javascript
// e2e/ecommerce-flow.spec.js
const { E2ETestRunner } = require('../src/utils/testing');
// E2E测试套件
const ecommerceTestSuite = {
name: 'E-commerce Application Flow',
beforeAll: async (page, context) => {
// 设置测试环境
await page.goto('/login');
// 清理测试数据
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
},
afterAll: async (page, context) => {
// 清理测试环境
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
},
beforeEach: async (page, context) => {
// 每个测试前重置状态
await page.goto('/');
},
afterEach: async (page, context) => {
// 每个测试后清理
await page.evaluate(() => {
// 清理购物车
if (window.store) {
window.store.dispatch({ type: 'CLEAR_CART' });
}
});
},
tests: [
{
name: '用户注册流程',
fn: async (page, context, runner) => {
// 导航到注册页面
await runner.navigateTo('/register');
// 填写注册表单
await runner.fillInput('[data-testid="register-name"]', 'Test User');
await runner.fillInput('[data-testid="register-email"]', 'test@example.com');
await runner.fillInput('[data-testid="register-password"]', 'password123');
await runner.fillInput('[data-testid="register-confirm-password"]', 'password123');
// 提交注册
await runner.clickElement('[data-testid="register-submit"]');
// 等待注册成功
await runner.waitForElement('[data-testid="registration-success"]');
// 验证跳转到登录页面
await page.waitForURL('**/login');
// 验证成功消息
const successMessage = await runner.getText('[data-testid="registration-success"]');
expect(successMessage).toContain('注册成功');
}
},
{
name: '用户登录流程',
fn: async (page, context, runner) => {
// 导航到登录页面
await runner.navigateTo('/login');
// 填写登录表单
await runner.fillInput('[data-testid="login-email"]', 'test@example.com');
await runner.fillInput('[data-testid="login-password"]', 'password123');
// 提交登录
await runner.clickElement('[data-testid="login-submit"]');
// 等待登录成功
await runner.waitForElement('[data-testid="user-menu"]');
// 验证跳转到首页
await page.waitForURL('**/dashboard');
// 验证用户信息显示
const userMenu = await runner.getText('[data-testid="user-menu"]');
expect(userMenu).toContain('Test User');
}
},
{
name: '商品浏览和搜索',
fn: async (page, context, runner) => {
// 导航到商品页面
await runner.navigateTo('/products');
// 等待商品列表加载
await runner.waitForElement('[data-testid="product-list"]');
// 验证商品显示
const products = await page.$$('[data-testid="product-item"]');
expect(products.length).toBeGreaterThan(0);
// 搜索商品
await runner.fillInput('[data-testid="search-input"]', 'iPhone');
await runner.clickElement('[data-testid="search-button"]');
// 等待搜索结果
await page.waitForSelector('[data-testid="search-results"]');
// 验证搜索结果
const searchResults = await page.$$('[data-testid="product-item"]');
expect(searchResults.length).toBeGreaterThan(0);
// 验证搜索结果包含关键词
const firstProduct = await runner.getText('[data-testid="product-item"]:first-child [data-testid="product-name"]');
expect(firstProduct.toLowerCase()).toContain('iphone');
}
},
{
name: '商品详情查看',
fn: async (page, context, runner) => {
// 导航到商品页面
await runner.navigateTo('/products');
// 点击第一个商品
await runner.clickElement('[data-testid="product-item"]:first-child');
// 等待商品详情页面加载
await runner.waitForElement('[data-testid="product-details"]');
// 验证商品详情信息
await runner.waitForElement('[data-testid="product-name"]');
await runner.waitForElement('[data-testid="product-price"]');
await runner.waitForElement('[data-testid="product-description"]');
await runner.waitForElement('[data-testid="product-images"]');
// 验证添加到购物车按钮存在
await runner.waitForElement('[data-testid="add-to-cart"]');
// 验证商品评价部分
await runner.waitForElement('[data-testid="product-reviews"]');
}
},
{
name: '购物车操作流程',
fn: async (page, context, runner) => {
// 先登录
await runner.navigateTo('/login');
await runner.fillInput('[data-testid="login-email"]', 'test@example.com');
await runner.fillInput('[data-testid="login-password"]', 'password123');
await runner.clickElement('[data-testid="login-submit"]');
await runner.waitForElement('[data-testid="user-menu"]');
// 导航到商品页面
await runner.navigateTo('/products');
// 添加第一个商品到购物车
await runner.clickElement('[data-testid="product-item"]:first-child [data-testid="add-to-cart"]');
// 等待添加成功提示
await runner.waitForElement('[data-testid="cart-notification"]');
// 验证购物车图标显示数量
const cartCount = await runner.getText('[data-testid="cart-count"]');
expect(cartCount).toBe('1');
// 点击购物车图标
await runner.clickElement('[data-testid="cart-icon"]');
// 等待购物车页面加载
await runner.waitForElement('[data-testid="cart-items"]');
// 验证商品在购物车中
const cartItems = await page.$$('[data-testid="cart-item"]');
expect(cartItems.length).toBe(1);
// 修改商品数量
await runner.clickElement('[data-testid="quantity-increase"]');
// 验证数量更新
const quantity = await runner.getText('[data-testid="item-quantity"]');
expect(quantity).toBe('2');
// 验证总价更新
const totalPrice = await runner.getText('[data-testid="cart-total"]');
expect(totalPrice).toMatch(/\$\d+\.\d{2}/);
}
},
{
name: '结账流程',
fn: async (page, context, runner) => {
// 先添加商品到购物车(复用之前的步骤)
await runner.navigateTo('/login');
await runner.fillInput('[data-testid="login-email"]', 'test@example.com');
await runner.fillInput('[data-testid="login-password"]', 'password123');
await runner.clickElement('[data-testid="login-submit"]');
await runner.waitForElement('[data-testid="user-menu"]');
await runner.navigateTo('/products');
await runner.clickElement('[data-testid="product-item"]:first-child [data-testid="add-to-cart"]');
await runner.waitForElement('[data-testid="cart-notification"]');
// 进入购物车
await runner.clickElement('[data-testid="cart-icon"]');
await runner.waitForElement('[data-testid="cart-items"]');
// 点击结账按钮
await runner.clickElement('[data-testid="checkout-button"]');
// 等待结账页面加载
await runner.waitForElement('[data-testid="checkout-form"]');
// 填写配送信息
await runner.fillInput('[data-testid="shipping-name"]', 'Test User');
await runner.fillInput('[data-testid="shipping-address"]', '123 Test Street');
await runner.fillInput('[data-testid="shipping-city"]', 'Test City');
await runner.fillInput('[data-testid="shipping-zip"]', '12345');
// 选择配送方式
await runner.clickElement('[data-testid="shipping-standard"]');
// 填写支付信息
await runner.fillInput('[data-testid="card-number"]', '4111111111111111');
await runner.fillInput('[data-testid="card-expiry"]', '12/25');
await runner.fillInput('[data-testid="card-cvc"]', '123');
await runner.fillInput('[data-testid="card-name"]', 'Test User');
// 提交订单
await runner.clickElement('[data-testid="place-order"]');
// 等待订单确认页面
await runner.waitForElement('[data-testid="order-confirmation"]');
// 验证订单号
const orderNumber = await runner.getText('[data-testid="order-number"]');
expect(orderNumber).toMatch(/^ORD-\d+$/);
// 验证订单详情
await runner.waitForElement('[data-testid="order-items"]');
await runner.waitForElement('[data-testid="order-total"]');
await runner.waitForElement('[data-testid="shipping-info"]');
}
},
{
name: '订单历史查看',
fn: async (page, context, runner) => {
// 登录
await runner.navigateTo('/login');
await runner.fillInput('[data-testid="login-email"]', 'test@example.com');
await runner.fillInput('[data-testid="login-password"]', 'password123');
await runner.clickElement('[data-testid="login-submit"]');
await runner.waitForElement('[data-testid="user-menu"]');
// 导航到订单历史页面
await runner.clickElement('[data-testid="user-menu"]');
await runner.clickElement('[data-testid="order-history"]');
// 等待订单列表加载
await runner.waitForElement('[data-testid="order-list"]');
// 验证订单显示
const orders = await page.$$('[data-testid="order-item"]');
expect(orders.length).toBeGreaterThan(0);
// 点击查看订单详情
await runner.clickElement('[data-testid="order-item"]:first-child [data-testid="view-order"]');
// 等待订单详情加载
await runner.waitForElement('[data-testid="order-details"]');
// 验证订单详情信息
await runner.waitForElement('[data-testid="order-status"]');
await runner.waitForElement('[data-testid="order-date"]');
await runner.waitForElement('[data-testid="order-items-detail"]');
await runner.waitForElement('[data-testid="shipping-address"]');
}
},
{
name: '响应式设计测试',
fn: async (page, context, runner) => {
// 测试桌面视图
await page.setViewportSize({ width: 1280, height: 720 });
await runner.navigateTo('/');
// 验证桌面导航
await runner.waitForElement('[data-testid="desktop-nav"]');
expect(await page.isVisible('[data-testid="mobile-menu-button"]')).toBe(false);
// 测试平板视图
await page.setViewportSize({ width: 768, height: 1024 });
await page.reload();
// 验证平板布局
await runner.waitForElement('[data-testid="tablet-layout"]');
// 测试手机视图
await page.setViewportSize({ width: 375, height: 667 });
await page.reload();
// 验证移动端导航
await runner.waitForElement('[data-testid="mobile-menu-button"]');
expect(await page.isVisible('[data-testid="desktop-nav"]')).toBe(false);
// 测试移动端菜单
await runner.clickElement('[data-testid="mobile-menu-button"]');
await runner.waitForElement('[data-testid="mobile-menu"]');
// 恢复桌面视图
await page.setViewportSize({ width: 1280, height: 720 });
}
},
{
name: '性能测试',
fn: async (page, context, runner) => {
// 测试首页加载性能
const homeMetrics = await runner.measurePageLoad('/');
// 验证加载时间
expect(homeMetrics.totalLoadTime).toBeLessThan(3000);
expect(homeMetrics.firstContentfulPaint).toBeLessThan(1500);
// 测试商品页面加载性能
const productsMetrics = await runner.measurePageLoad('/products');
// 验证商品页面性能
expect(productsMetrics.totalLoadTime).toBeLessThan(5000);
expect(productsMetrics.domContentLoaded).toBeLessThan(2000);
// 测试搜索性能
const searchStart = Date.now();
await runner.fillInput('[data-testid="search-input"]', 'test');
await runner.clickElement('[data-testid="search-button"]');
await runner.waitForElement('[data-testid="search-results"]');
const searchTime = Date.now() - searchStart;
expect(searchTime).toBeLessThan(2000);
}
},
{
name: '可访问性测试',
fn: async (page, context, runner) => {
// 测试首页可访问性
await runner.navigateTo('/');
const homeA11y = await runner.checkAccessibility();
if (!homeA11y.passed) {
console.warn('首页可访问性问题:', homeA11y.violations);
}
// 测试登录页面可访问性
await runner.navigateTo('/login');
const loginA11y = await runner.checkAccessibility();
if (!loginA11y.passed) {
console.warn('登录页面可访问性问题:', loginA11y.violations);
}
// 测试键盘导航
await page.keyboard.press('Tab');
const focusedElement = await page.evaluate(() => document.activeElement.tagName);
expect(['INPUT', 'BUTTON', 'A']).toContain(focusedElement);
// 测试屏幕阅读器支持
const loginButton = await page.$('[data-testid="login-submit"]');
const ariaLabel = await loginButton.getAttribute('aria-label');
expect(ariaLabel).toBeTruthy();
}
}
]
};
// 运行E2E测试
async function runE2ETests() {
const runner = new E2ETestRunner({
browser: 'chromium',
headless: false, // 开发时可以设置为false观察测试过程
baseURL: 'http://localhost:3000',
timeout: 30000,
video: 'retain-on-failure',
screenshot: 'only-on-failure'
});
try {
const results = await runner.run([ecommerceTestSuite]);
console.log('\n📊 E2E测试结果:');
console.log(`总计: ${results.total}`);
console.log(`通过: ${results.passed}`);
console.log(`失败: ${results.failed}`);
console.log(`跳过: ${results.skipped}`);
console.log(`耗时: ${results.duration}ms`);
if (results.failed > 0) {
console.log('\n❌ 失败的测试:');
results.tests
.filter(test => test.status === 'failed')
.forEach(test => {
console.log(` - ${test.name}: ${test.error}`);
});
}
return results;
} catch (error) {
console.error('E2E测试执行失败:', error);
throw error;
}
}
// 导出测试套件和运行函数
module.exports = {
ecommerceTestSuite,
runE2ETests
};
5. 最佳实践与总结
5.1 测试策略设计原则
测试金字塔实践
javascript
// 测试策略管理器
class TestStrategyManager {
constructor() {
this.strategies = {
unit: {
ratio: 0.7, // 70%的测试应该是单元测试
focus: ['函数逻辑', '组件行为', '工具函数', '状态管理'],
tools: ['Jest', 'React Testing Library', 'Enzyme'],
coverage: { minimum: 80, target: 90 }
},
integration: {
ratio: 0.2, // 20%的测试应该是集成测试
focus: ['组件集成', 'API集成', '数据流', '用户交互'],
tools: ['Jest', 'MSW', 'Testing Library'],
coverage: { minimum: 60, target: 75 }
},
e2e: {
ratio: 0.1, // 10%的测试应该是E2E测试
focus: ['关键用户流程', '跨浏览器兼容性', '性能', '可访问性'],
tools: ['Playwright', 'Cypress', 'Puppeteer'],
coverage: { minimum: 40, target: 60 }
}
};
this.qualityGates = {
coverage: {
statements: 80,
branches: 75,
functions: 80,
lines: 80
},
performance: {
unitTestSpeed: 1000, // ms per test
integrationTestSpeed: 5000,
e2eTestSpeed: 30000
},
reliability: {
flakyTestThreshold: 0.05, // 5%
testStability: 0.95 // 95%
}
};
}
// 评估测试策略
evaluateStrategy(testResults) {
const evaluation = {
distribution: this.analyzeTestDistribution(testResults),
coverage: this.analyzeCoverage(testResults),
performance: this.analyzePerformance(testResults),
quality: this.analyzeQuality(testResults),
recommendations: []
};
// 生成改进建议
evaluation.recommendations = this.generateRecommendations(evaluation);
return evaluation;
}
// 分析测试分布
analyzeTestDistribution(testResults) {
const total = testResults.unit.count + testResults.integration.count + testResults.e2e.count;
return {
unit: {
actual: testResults.unit.count / total,
expected: this.strategies.unit.ratio,
status: this.getDistributionStatus('unit', testResults.unit.count / total)
},
integration: {
actual: testResults.integration.count / total,
expected: this.strategies.integration.ratio,
status: this.getDistributionStatus('integration', testResults.integration.count / total)
},
e2e: {
actual: testResults.e2e.count / total,
expected: this.strategies.e2e.ratio,
status: this.getDistributionStatus('e2e', testResults.e2e.count / total)
}
};
}
// 获取分布状态
getDistributionStatus(type, actual) {
const expected = this.strategies[type].ratio;
const tolerance = 0.1; // 10%容差
if (Math.abs(actual - expected) <= tolerance) {
return 'optimal';
} else if (actual > expected) {
return 'over';
} else {
return 'under';
}
}
// 生成改进建议
generateRecommendations(evaluation) {
const recommendations = [];
// 测试分布建议
Object.keys(evaluation.distribution).forEach(type => {
const dist = evaluation.distribution[type];
if (dist.status === 'under') {
recommendations.push({
type: 'distribution',
priority: 'high',
message: `增加${type}测试数量,当前比例${(dist.actual * 100).toFixed(1)}%,建议${(dist.expected * 100).toFixed(1)}%`
});
} else if (dist.status === 'over') {
recommendations.push({
type: 'distribution',
priority: 'medium',
message: `${type}测试比例过高,考虑重构为更低层次的测试`
});
}
});
// 覆盖率建议
Object.keys(evaluation.coverage).forEach(metric => {
const coverage = evaluation.coverage[metric];
if (coverage < this.qualityGates.coverage[metric]) {
recommendations.push({
type: 'coverage',
priority: 'high',
message: `${metric}覆盖率不足,当前${coverage}%,要求${this.qualityGates.coverage[metric]}%`
});
}
});
// 性能建议
if (evaluation.performance.averageTestTime > this.qualityGates.performance.unitTestSpeed) {
recommendations.push({
type: 'performance',
priority: 'medium',
message: '测试执行时间过长,考虑优化测试代码或并行执行'
});
}
return recommendations;
}
}
5.2 性能优化策略
测试执行优化
javascript
// 测试性能优化器
class TestPerformanceOptimizer {
constructor() {
this.optimizations = {
parallel: true,
cache: true,
incremental: true,
smartSelection: true
};
this.metrics = {
executionTime: [],
memoryUsage: [],
cacheHitRate: 0,
parallelEfficiency: 0
};
}
// 优化测试执行
async optimizeExecution(testSuites) {
const optimizedSuites = [];
for (const suite of testSuites) {
const optimizedSuite = await this.optimizeSuite(suite);
optimizedSuites.push(optimizedSuite);
}
return {
suites: optimizedSuites,
config: this.generateOptimizedConfig(),
recommendations: this.generateOptimizationRecommendations()
};
}
// 优化测试套件
async optimizeSuite(suite) {
const optimized = { ...suite };
// 智能测试选择
if (this.optimizations.smartSelection) {
optimized.tests = await this.selectRelevantTests(suite.tests);
}
// 测试分组优化
optimized.groups = this.optimizeTestGroups(optimized.tests);
// 资源预加载
optimized.preload = this.identifyPreloadResources(optimized.tests);
return optimized;
}
// 智能测试选择
async selectRelevantTests(tests) {
const changedFiles = await this.getChangedFiles();
const relevantTests = [];
for (const test of tests) {
const dependencies = await this.getTestDependencies(test);
const isRelevant = dependencies.some(dep =>
changedFiles.some(file => dep.includes(file))
);
if (isRelevant || test.critical) {
relevantTests.push(test);
}
}
return relevantTests.length > 0 ? relevantTests : tests;
}
// 获取变更文件
async getChangedFiles() {
try {
const { execSync } = require('child_process');
const output = execSync('git diff --name-only HEAD~1', { encoding: 'utf8' });
return output.trim().split('\n').filter(Boolean);
} catch (error) {
console.warn('无法获取变更文件,运行所有测试');
return [];
}
}
// 获取测试依赖
async getTestDependencies(test) {
// 简化的依赖分析
const dependencies = [];
if (test.file) {
const fs = require('fs');
const content = fs.readFileSync(test.file, 'utf8');
// 提取import语句
const imports = content.match(/import.*from\s+['"]([^'"]+)['"]/g) || [];
imports.forEach(imp => {
const match = imp.match(/from\s+['"]([^'"]+)['"]/);;
if (match) {
dependencies.push(match[1]);
}
});
}
return dependencies;
}
// 优化测试分组
optimizeTestGroups(tests) {
const groups = {
fast: [], // 快速测试(<100ms)
medium: [], // 中等测试(100ms-1s)
slow: [], // 慢速测试(>1s)
isolated: [] // 需要隔离的测试
};
tests.forEach(test => {
if (test.isolated) {
groups.isolated.push(test);
} else if (test.estimatedTime < 100) {
groups.fast.push(test);
} else if (test.estimatedTime < 1000) {
groups.medium.push(test);
} else {
groups.slow.push(test);
}
});
return groups;
}
// 生成优化配置
generateOptimizedConfig() {
return {
maxWorkers: this.optimizations.parallel ? '50%' : 1,
cache: this.optimizations.cache,
cacheDirectory: '.jest-cache',
testTimeout: 10000,
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.js',
'!src/serviceWorker.js'
],
coverageThreshold: {
global: {
branches: 75,
functions: 80,
lines: 80,
statements: 80
}
},
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname'
]
};
}
// 生成优化建议
generateOptimizationRecommendations() {
const recommendations = [];
// 并行执行建议
if (!this.optimizations.parallel) {
recommendations.push({
type: 'parallel',
impact: 'high',
message: '启用并行测试执行可以显著提升速度'
});
}
// 缓存建议
if (!this.optimizations.cache) {
recommendations.push({
type: 'cache',
impact: 'medium',
message: '启用测试缓存可以避免重复执行未变更的测试'
});
}
// 增量测试建议
if (!this.optimizations.incremental) {
recommendations.push({
type: 'incremental',
impact: 'medium',
message: '启用增量测试可以只运行相关的测试'
});
}
return recommendations;
}
}
5.3 开发体验提升
测试开发工具
javascript
// 测试开发助手
class TestDevelopmentHelper {
constructor() {
this.templates = new Map();
this.snippets = new Map();
this.generators = new Map();
this.initializeTemplates();
this.initializeSnippets();
this.initializeGenerators();
}
// 初始化模板
initializeTemplates() {
// React组件测试模板
this.templates.set('react-component', `
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { {{componentName}} } from './{{componentName}}';
describe('{{componentName}}', () => {
test('应该正确渲染', () => {
render(<{{componentName}} />);
// 添加断言
});
test('应该处理用户交互', () => {
render(<{{componentName}} />);
// 添加交互测试
});
test('应该处理props变化', () => {
const { rerender } = render(<{{componentName}} prop="value1" />);
// 测试props变化
rerender(<{{componentName}} prop="value2" />);
// 添加断言
});
});
`);
// Hook测试模板
this.templates.set('react-hook', `
import { renderHook, act } from '@testing-library/react';
import { {{hookName}} } from './{{hookName}}';
describe('{{hookName}}', () => {
test('应该返回初始状态', () => {
const { result } = renderHook(() => {{hookName}}());
// 添加断言
});
test('应该正确更新状态', () => {
const { result } = renderHook(() => {{hookName}}());
act(() => {
// 执行状态更新
});
// 添加断言
});
});
`);
// 工具函数测试模板
this.templates.set('utility-function', `
import { {{functionName}} } from './{{functionName}}';
describe('{{functionName}}', () => {
test('应该处理正常输入', () => {
const result = {{functionName}}(/* 正常输入 */);
expect(result).toBe(/* 期望结果 */);
});
test('应该处理边界情况', () => {
// 测试边界情况
});
test('应该处理错误输入', () => {
expect(() => {
{{functionName}}(/* 错误输入 */);
}).toThrow();
});
});
`);
}
// 初始化代码片段
initializeSnippets() {
this.snippets.set('mock-api', `
// Mock API响应
const mockApiResponse = {
data: {
// 模拟数据
},
status: 200,
statusText: 'OK'
};
jest.mock('axios', () => ({
get: jest.fn(() => Promise.resolve(mockApiResponse)),
post: jest.fn(() => Promise.resolve(mockApiResponse)),
put: jest.fn(() => Promise.resolve(mockApiResponse)),
delete: jest.fn(() => Promise.resolve(mockApiResponse))
}));
`);
this.snippets.set('mock-localStorage', `
// Mock localStorage
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn()
};
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
});
`);
this.snippets.set('async-test', `
test('应该处理异步操作', async () => {
// 等待异步操作完成
await waitFor(() => {
expect(screen.getByText('加载完成')).toBeInTheDocument();
});
// 或者使用findBy查询
const element = await screen.findByText('异步内容');
expect(element).toBeInTheDocument();
});
`);
}
// 初始化生成器
initializeGenerators() {
this.generators.set('component-test', this.generateComponentTest.bind(this));
this.generators.set('hook-test', this.generateHookTest.bind(this));
this.generators.set('integration-test', this.generateIntegrationTest.bind(this));
}
// 生成组件测试
generateComponentTest(componentPath) {
const fs = require('fs');
const path = require('path');
// 读取组件文件
const componentContent = fs.readFileSync(componentPath, 'utf8');
// 提取组件信息
const componentInfo = this.analyzeComponent(componentContent);
// 生成测试代码
const testCode = this.generateTestFromTemplate('react-component', {
componentName: componentInfo.name,
props: componentInfo.props,
events: componentInfo.events,
states: componentInfo.states
});
// 写入测试文件
const testPath = componentPath.replace(/\.(jsx?|tsx?)$/, '.test.$1');
fs.writeFileSync(testPath, testCode);
return testPath;
}
// 分析组件
analyzeComponent(content) {
const info = {
name: '',
props: [],
events: [],
states: []
};
// 提取组件名称
const nameMatch = content.match(/(?:function|const)\s+(\w+)|class\s+(\w+)/);
if (nameMatch) {
info.name = nameMatch[1] || nameMatch[2];
}
// 提取props
const propsMatch = content.match(/\{([^}]+)\}\s*=\s*props/);
if (propsMatch) {
info.props = propsMatch[1].split(',').map(prop => prop.trim());
}
// 提取事件处理器
const eventMatches = content.match(/on\w+\s*=/g) || [];
info.events = eventMatches.map(event => event.replace(/\s*=$/, ''));
// 提取状态
const stateMatches = content.match(/useState\(([^)]+)\)/g) || [];
info.states = stateMatches.map((state, index) => `state${index}`);
return info;
}
// 从模板生成测试
generateTestFromTemplate(templateName, data) {
let template = this.templates.get(templateName);
// 替换模板变量
Object.keys(data).forEach(key => {
const regex = new RegExp(`\{\{${key}\}\}`, 'g');
template = template.replace(regex, data[key]);
});
return template;
}
// 生成测试报告
generateTestReport(results) {
const report = {
summary: {
total: results.numTotalTests,
passed: results.numPassedTests,
failed: results.numFailedTests,
skipped: results.numPendingTests,
coverage: results.coverageMap
},
details: results.testResults.map(testFile => ({
file: testFile.testFilePath,
tests: testFile.testResults.map(test => ({
name: test.title,
status: test.status,
duration: test.duration,
error: test.failureMessages[0] || null
}))
})),
recommendations: this.generateTestRecommendations(results)
};
return report;
}
// 生成测试建议
generateTestRecommendations(results) {
const recommendations = [];
// 覆盖率建议
if (results.coverageMap) {
const coverage = results.coverageMap.getCoverageSummary();
if (coverage.statements.pct < 80) {
recommendations.push({
type: 'coverage',
priority: 'high',
message: `语句覆盖率${coverage.statements.pct}%,建议提升至80%以上`
});
}
if (coverage.branches.pct < 75) {
recommendations.push({
type: 'coverage',
priority: 'medium',
message: `分支覆盖率${coverage.branches.pct}%,建议提升至75%以上`
});
}
}
// 测试质量建议
const failedTests = results.testResults.filter(test => test.numFailingTests > 0);
if (failedTests.length > 0) {
recommendations.push({
type: 'quality',
priority: 'high',
message: `${failedTests.length}个测试文件包含失败的测试,需要修复`
});
}
// 性能建议
const slowTests = results.testResults.filter(test =>
test.perfStats.end - test.perfStats.start > 5000
);
if (slowTests.length > 0) {
recommendations.push({
type: 'performance',
priority: 'medium',
message: `${slowTests.length}个测试文件执行时间过长,考虑优化`
});
}
return recommendations;
}
}
5.4 技术选型建议
测试工具选择矩阵
测试类型 | 推荐工具 | 适用场景 | 优势 | 劣势 |
---|---|---|---|---|
单元测试 | Jest + RTL | React应用 | 生态完善、配置简单 | 学习曲线 |
单元测试 | Vitest | Vite项目 | 速度快、ESM支持 | 生态较新 |
集成测试 | Jest + MSW | API集成 | Mock能力强 | 配置复杂 |
E2E测试 | Playwright | 跨浏览器 | 功能全面、稳定 | 资源消耗大 |
E2E测试 | Cypress | 开发体验 | 调试友好、实时预览 | 浏览器限制 |
性能测试 | Lighthouse CI | Web性能 | 标准化指标 | 配置复杂 |
可访问性 | axe-core | 无障碍测试 | 规则全面 | 需要人工验证 |
5.5 未来发展趋势
AI驱动的测试
javascript
// AI测试助手(概念性实现)
class AITestingAssistant {
constructor() {
this.model = null; // AI模型接口
this.testPatterns = new Map();
this.bugPatterns = new Map();
}
// AI生成测试用例
async generateTestCases(componentCode) {
const analysis = await this.analyzeCode(componentCode);
const testCases = await this.model.generate({
prompt: `为以下React组件生成测试用例:\n${componentCode}`,
context: {
patterns: Array.from(this.testPatterns.values()),
bestPractices: this.getTestingBestPractices()
}
});
return this.validateGeneratedTests(testCases);
}
// 智能Bug检测
async detectPotentialBugs(testResults) {
const patterns = await this.model.analyze({
data: testResults,
patterns: Array.from(this.bugPatterns.values())
});
return patterns.map(pattern => ({
type: pattern.type,
confidence: pattern.confidence,
description: pattern.description,
suggestion: pattern.suggestion
}));
}
// 自动化测试维护
async maintainTests(codeChanges) {
const affectedTests = await this.findAffectedTests(codeChanges);
const updates = [];
for (const test of affectedTests) {
const update = await this.model.updateTest({
originalTest: test.content,
codeChanges: codeChanges,
context: test.context
});
updates.push({
file: test.file,
changes: update.changes,
confidence: update.confidence
});
}
return updates;
}
}
5.6 核心价值与收益
测试投资回报分析
javascript
// 测试ROI计算器
class TestingROICalculator {
constructor() {
this.metrics = {
bugDetectionCost: 100, // 测试中发现bug的成本
productionBugCost: 10000, // 生产环境bug的成本
testMaintenanceCost: 50, // 测试维护成本
developmentSpeedIncrease: 0.2, // 开发速度提升
codeQualityImprovement: 0.3 // 代码质量提升
};
}
// 计算测试ROI
calculateROI(testingData) {
const costs = this.calculateCosts(testingData);
const benefits = this.calculateBenefits(testingData);
const roi = ((benefits - costs) / costs) * 100;
return {
roi: roi,
costs: costs,
benefits: benefits,
paybackPeriod: costs / (benefits / 12), // 月
breakdown: {
bugPrevention: this.calculateBugPreventionValue(testingData),
developmentSpeed: this.calculateSpeedValue(testingData),
codeQuality: this.calculateQualityValue(testingData),
maintenance: this.calculateMaintenanceValue(testingData)
}
};
}
// 计算测试成本
calculateCosts(data) {
return {
development: data.testDevelopmentHours * data.hourlyRate,
maintenance: data.testCount * this.metrics.testMaintenanceCost,
infrastructure: data.ciCdCosts + data.toolingCosts,
total: 0
};
}
// 计算测试收益
calculateBenefits(data) {
const bugsPrevented = data.bugsFoundInTesting;
const productionBugsSaved = bugsPrevented * 0.8; // 80%的bug会到生产环境
return {
bugPrevention: productionBugsSaved * this.metrics.productionBugCost,
speedIncrease: data.developmentHours * data.hourlyRate * this.metrics.developmentSpeedIncrease,
qualityImprovement: data.maintenanceHours * data.hourlyRate * this.metrics.codeQualityImprovement,
total: 0
};
}
}
结语
前端测试是现代Web开发不可或缺的重要环节。通过建立完善的测试体系,我们能够:
- 提升代码质量:通过全面的测试覆盖,确保代码的正确性和稳定性
- 加速开发流程:自动化测试减少手动验证时间,提升开发效率
- 降低维护成本:早期发现问题,避免生产环境的高昂修复成本
- 增强团队信心:完善的测试让重构和新功能开发更加安全
- 改善用户体验:确保应用在各种场景下都能正常工作
测试不仅仅是质量保证的手段,更是现代软件工程实践的基础。投资于测试体系建设,将为项目的长期成功奠定坚实基础。
相关文章推荐:
- 前端工程化深度实践:从脚手架到部署的完整工程化解决方案
- 前端性能优化深度指南:从理论到实践的完整性能优化方案
- 前端状态管理深度实践:从Redux到Zustand的现代化状态管理方案
- 前端安全防护深度实践:从XSS到CSRF的完整安全解决方案
2. 单元测试深度实践
2.1 单元测试运行器
javascript
// 单元测试运行器
class UnitTestRunner {
constructor(config = {}) {
this.config = {
framework: 'jest',
environment: 'jsdom',
setupFiles: [],
testMatch: ['**/__tests__/**/*.test.{js,jsx,ts,tsx}'],
collectCoverageFrom: [],
coverageThreshold: {},
moduleNameMapping: {},
transform: {},
...config
};
this.testFiles = [];
this.testResults = [];
this.mockRegistry = new Map();
this.spyRegistry = new Map();
this.init();
}
// 初始化
init() {
this.setupTestFramework();
this.setupMockSystem();
this.setupTestUtilities();
}
// 设置测试框架
setupTestFramework() {
// Jest配置
this.jestConfig = {
testEnvironment: this.config.environment,
setupFilesAfterEnv: this.config.setupFiles,
testMatch: this.config.testMatch,
collectCoverageFrom: this.config.collectCoverageFrom,
coverageThreshold: this.config.coverageThreshold,
moduleNameMapping: this.config.moduleNameMapping,
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
'^.+\\.css$': 'identity-obj-proxy',
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': 'jest-transform-stub',
...this.config.transform
},
transformIgnorePatterns: [
'[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$',
'^.+\\.module\\.(css|sass|scss)$'
],
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'node'],
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname'
]
};
}
// 设置Mock系统
setupMockSystem() {
this.mockUtils = {
// 创建函数Mock
createFunctionMock: (implementation) => {
const mock = jest.fn(implementation);
this.mockRegistry.set(mock, {
type: 'function',
created: Date.now(),
calls: []
});
return mock;
},
// 创建模块Mock
createModuleMock: (modulePath, mockImplementation) => {
const mock = jest.mock(modulePath, () => mockImplementation);
this.mockRegistry.set(modulePath, {
type: 'module',
implementation: mockImplementation,
created: Date.now()
});
return mock;
},
// 创建对象Mock
createObjectMock: (object, methods = []) => {
const mock = {};
if (methods.length === 0) {
methods = Object.getOwnPropertyNames(object.prototype || object)
.filter(name => typeof object[name] === 'function');
}
methods.forEach(method => {
mock[method] = jest.fn();
});
this.mockRegistry.set(mock, {
type: 'object',
originalObject: object,
mockedMethods: methods,
created: Date.now()
});
return mock;
},
// 创建API Mock
createApiMock: (baseUrl, endpoints = {}) => {
const mock = {
baseUrl,
endpoints: {},
requests: []
};
Object.keys(endpoints).forEach(endpoint => {
mock.endpoints[endpoint] = {
response: endpoints[endpoint],
calls: 0,
lastCall: null
};
});
// 模拟fetch
global.fetch = jest.fn((url, options = {}) => {
const endpoint = url.replace(baseUrl, '');
const mockEndpoint = mock.endpoints[endpoint];
mock.requests.push({
url,
options,
timestamp: Date.now()
});
if (mockEndpoint) {
mockEndpoint.calls++;
mockEndpoint.lastCall = Date.now();
return Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve(mockEndpoint.response),
text: () => Promise.resolve(JSON.stringify(mockEndpoint.response))
});
}
return Promise.reject(new Error(`No mock found for ${endpoint}`));
});
this.mockRegistry.set('api', mock);
return mock;
}
};
}
// 设置测试工具
setupTestUtilities() {
this.testUtils = {
// React组件测试工具
renderComponent: (Component, props = {}, options = {}) => {
const { render } = require('@testing-library/react');
const { Provider } = require('react-redux');
const { BrowserRouter } = require('react-router-dom');
const AllTheProviders = ({ children }) => {
let wrapped = children;
// Redux Provider
if (options.store) {
wrapped = React.createElement(Provider, { store: options.store }, wrapped);
}
// Router Provider
if (options.router !== false) {
wrapped = React.createElement(BrowserRouter, {}, wrapped);
}
return wrapped;
};
return render(
React.createElement(Component, props),
{
wrapper: AllTheProviders,
...options
}
);
},
// 异步测试工具
waitForAsync: async (callback, timeout = 5000) => {
const { waitFor } = require('@testing-library/react');
return waitFor(callback, { timeout });
},
// 用户交互模拟
userInteraction: {
click: async (element) => {
const { fireEvent } = require('@testing-library/react');
fireEvent.click(element);
await this.testUtils.waitForAsync(() => {});
},
type: async (element, text) => {
const { fireEvent } = require('@testing-library/react');
fireEvent.change(element, { target: { value: text } });
await this.testUtils.waitForAsync(() => {});
},
submit: async (form) => {
const { fireEvent } = require('@testing-library/react');
fireEvent.submit(form);
await this.testUtils.waitForAsync(() => {});
}
},
// 快照测试
createSnapshot: (component, props = {}) => {
const renderer = require('react-test-renderer');
const tree = renderer.create(
React.createElement(component, props)
).toJSON();
expect(tree).toMatchSnapshot();
return tree;
},
// 性能测试
measurePerformance: async (callback, iterations = 100) => {
const times = [];
for (let i = 0; i < iterations; i++) {
const start = performance.now();
await callback();
const end = performance.now();
times.push(end - start);
}
return {
average: times.reduce((a, b) => a + b, 0) / times.length,
min: Math.min(...times),
max: Math.max(...times),
median: times.sort((a, b) => a - b)[Math.floor(times.length / 2)]
};
}
};
}
// 运行测试
async run(options = {}) {
const startTime = Date.now();
try {
console.log('🧪 Running unit tests...');
// 发现测试文件
await this.discoverTestFiles();
// 运行Jest
const jestResult = await this.runJest(options);
// 处理结果
const result = this.processJestResult(jestResult);
const endTime = Date.now();
result.duration = endTime - startTime;
console.log(`✅ Unit tests completed in ${result.duration}ms`);
return result;
} catch (error) {
console.error('❌ Unit test execution failed:', error);
throw error;
}
}
// 发现测试文件
async discoverTestFiles() {
const glob = require('glob');
const path = require('path');
this.testFiles = [];
for (const pattern of this.config.testMatch) {
const files = glob.sync(pattern, {
cwd: process.cwd(),
absolute: true
});
this.testFiles.push(...files);
}
console.log(`📁 Found ${this.testFiles.length} test files`);
return this.testFiles;
}
// 运行Jest
async runJest(options = {}) {
const jest = require('jest');
const jestOptions = {
...this.jestConfig,
silent: options.silent || false,
verbose: options.verbose || true,
collectCoverage: options.coverage !== false,
runInBand: !options.parallel,
watchAll: options.watch || false
};
if (options.testNamePattern) {
jestOptions.testNamePattern = options.testNamePattern;
}
if (options.testPathPattern) {
jestOptions.testPathPattern = options.testPathPattern;
}
return new Promise((resolve, reject) => {
jest.runCLI(jestOptions, [process.cwd()])
.then(({ results }) => resolve(results))
.catch(reject);
});
}
// 处理Jest结果
processJestResult(jestResult) {
const tests = [];
const errors = [];
jestResult.testResults.forEach(testFile => {
testFile.testResults.forEach(test => {
tests.push({
name: test.title,
file: testFile.testFilePath,
status: test.status,
duration: test.duration,
error: test.failureMessages.length > 0 ? test.failureMessages[0] : null
});
});
if (testFile.failureMessage) {
errors.push(testFile.failureMessage);
}
});
return {
success: jestResult.success,
tests: tests,
coverage: jestResult.coverageMap ? this.processCoverageData(jestResult.coverageMap) : null,
errors: errors,
numTotalTests: jestResult.numTotalTests,
numPassedTests: jestResult.numPassedTests,
numFailedTests: jestResult.numFailedTests,
numPendingTests: jestResult.numPendingTests
};
}
// 处理覆盖率数据
processCoverageData(coverageMap) {
const coverage = {};
Object.keys(coverageMap).forEach(filePath => {
const fileCoverage = coverageMap[filePath];
coverage[filePath] = {
statements: {
total: fileCoverage.s ? Object.keys(fileCoverage.s).length : 0,
covered: fileCoverage.s ? Object.values(fileCoverage.s).filter(Boolean).length : 0,
percentage: 0
},
branches: {
total: fileCoverage.b ? Object.keys(fileCoverage.b).length : 0,
covered: fileCoverage.b ? Object.values(fileCoverage.b).filter(branch => branch.some(Boolean)).length : 0,
percentage: 0
},
functions: {
total: fileCoverage.f ? Object.keys(fileCoverage.f).length : 0,
covered: fileCoverage.f ? Object.values(fileCoverage.f).filter(Boolean).length : 0,
percentage: 0
},
lines: {
total: fileCoverage.l ? Object.keys(fileCoverage.l).length : 0,
covered: fileCoverage.l ? Object.values(fileCoverage.l).filter(Boolean).length : 0,
percentage: 0
}
};
// 计算百分比
Object.keys(coverage[filePath]).forEach(metric => {
const { total, covered } = coverage[filePath][metric];
coverage[filePath][metric].percentage = total > 0 ? Math.round((covered / total) * 100) : 0;
});
});
return coverage;
}
// 清理Mock
cleanupMocks() {
// 清理Jest mocks
jest.clearAllMocks();
jest.resetAllMocks();
jest.restoreAllMocks();
// 清理自定义mocks
this.mockRegistry.clear();
this.spyRegistry.clear();
// 恢复全局对象
if (global.fetch && global.fetch.mockRestore) {
global.fetch.mockRestore();
}
}
// 获取Mock统计
getMockStats() {
const stats = {
totalMocks: this.mockRegistry.size,
mockTypes: {},
activeMocks: []
};
this.mockRegistry.forEach((mockInfo, mock) => {
if (!stats.mockTypes[mockInfo.type]) {
stats.mockTypes[mockInfo.type] = 0;
}
stats.mockTypes[mockInfo.type]++;
stats.activeMocks.push({
type: mockInfo.type,
created: mockInfo.created,
age: Date.now() - mockInfo.created
});
});
return stats;
}
// 获取状态
getStatus() {
return {
config: this.config,
testFiles: this.testFiles.length,
mockStats: this.getMockStats(),
lastResults: this.testResults.slice(-5)
};
}
// 清理
cleanup() {
this.cleanupMocks();
this.testFiles = [];
this.testResults = [];
}
}