0%

前端测试策略升级——现代测试实践

现代前端测试不仅是质量保证的基础,更是开发效率和信心的重要保障。通过合理的测试策略升级,可以显著提升代码质量和开发体验。

介绍

  随着前端应用的复杂度不断提升,测试已成为现代软件开发流程中不可缺少的一环。传统的测试方法已无法满足当前快速迭代、复杂交互的前端应用需求。本文将深入探讨现代前端测试策略的升级路径,从测试金字塔理论到实际工具应用,为构建高效、可靠的测试体系提供全面指导。

现代测试金字塔

传统测试金字塔的演变

测试金字塔概念的演进反映了现代前端开发的特殊性,需要根据实际情况进行调整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// 测试金字塔策略配置
class TestPyramidStrategy {
constructor() {
this.levels = {
unit: {
ratio: 70, // 70%
description: 'Fast, isolated tests for individual functions and components',
tools: ['Jest', 'Vitest', 'Mocha'],
executionTime: '< 1s per test',
focus: 'Logic correctness, edge cases'
},
integration: {
ratio: 20, // 20%
description: 'Tests for component interactions and API integrations',
tools: ['React Testing Library', 'Testing Library', 'Cypress Component Tests'],
executionTime: '1-10s per test',
focus: 'Component composition, data flow'
},
e2e: {
ratio: 10, // 10%
description: 'End-to-end tests covering critical user flows',
tools: ['Playwright', 'Cypress', 'Selenium'],
executionTime: '10s+ per test',
focus: 'User journeys, production-like environment'
}
};

this.modernAdjustments = {
// 针对前端应用的调整
shiftToLeft: 'Emphasize unit and integration tests over heavy E2E suites',
componentFocus: 'Component-level testing is crucial in modern FE development',
speedPriority: 'Fast feedback is essential for development velocity',
visualTesting: 'Screenshot and visual regression testing',
accessibility: 'Automated accessibility testing',
performance: 'Performance budget and measurement tests'
};
}

// 获取理想的测试分布
getIdealDistribution(projectType) {
switch(projectType) {
case 'spa':
return {
unit: 60,
integration: 25,
e2e: 15,
visual: 5, // 新增视觉测试
performance: 5 // 新增性能测试
};

case 'static-site':
return {
unit: 50,
integration: 20,
e2e: 20, // 更多E2E测试
visual: 10,
accessibility: 10
};

case 'component-library':
return {
unit: 70,
integration: 20,
e2e: 5,
visual: 5 // 组件库需要大量视觉测试
};

default:
return this.levels;
}
}

// 测试优先级策略
prioritizationStrategy = {
smokeTests: {
purpose: 'Quick verification of core functionality',
execution: 'Every commit, every PR',
target: 'Critical paths only',
idealRunTime: '< 2 minutes'
},

regressionTests: {
purpose: 'Ensure new changes don\'t break existing functionality',
execution: 'Before each release',
target: 'Full test suite',
idealRunTime: '< 30 minutes'
},

performanceTests: {
purpose: 'Monitor application performance',
execution: 'Daily, in production',
target: 'Key metrics',
idealRunTime: '5-15 minutes'
}
};
}

// 使用示例
const pyramid = new TestPyramidStrategy();
const distribution = pyramid.getIdealDistribution('spa');
console.log('Recommended test distribution:', distribution);

现代测试工具生态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// 测试工具选择矩阵
class TestToolMatrix {
constructor() {
this.tools = {
// 单元测试工具
unitTesting: {
vitest: {
speed: '⚡ Extremely fast (uses Vite)',
features: ['Instant test startup', 'HMR integration', 'TypeScript support'],
ecosystem: 'Vite ecosystem',
bestFor: ['Vite projects', 'Fast feedback', 'TypeScript projects']
},
jest: {
maturity: 'Highly mature with rich ecosystem',
features: ['Large plugin ecosystem', 'Snapshot testing', 'Mocking utilities'],
ecosystem: 'Broad JavaScript ecosystem',
bestFor: ['Legacy projects', 'Complex mocking', 'Enterprise applications']
},
mocha: {
flexibility: 'Highly configurable',
features: ['Flexible reporters', 'Various assertion libraries', 'Browser testing'],
ecosystem: 'Traditional Node.js ecosystem',
bestFor: ['Node.js applications', 'Custom testing needs', 'Browser automation']
}
},

// 组件测试工具
componentTesting: {
'react-testing-library': {
philosophy: 'Test user behavior, not implementation',
features: ['Queries by text/labels', 'Fire events', 'Wait for async'],
bestFor: ['React applications', 'Behavior-driven tests', 'Accessibility testing']
},
'@testing-library/vue': {
vueNative: true,
features: ['Vue-specific queries', 'Reactivity handling', 'Slot testing'],
bestFor: ['Vue applications', 'Composition API testing', 'Component interactions']
},
'@testing-library/user-event': {
realisticInteraction: true,
features: ['Sequence of events', 'Keyboard navigation', 'Focus management'],
bestFor: ['Accessibility testing', 'Complex user flows', 'Realistic interactions']
}
},

// E2E测试工具
e2eTesting: {
playwright: {
crossBrowser: true,
features: ['All browsers', 'Mobile testing', 'Network interception', 'Visual testing'],
bestFor: ['Modern web apps', 'Cross-browser testing', 'Complex user flows']
},
cypress: {
developerExperience: 'Excellent DX with dashboard',
features: ['Time travel', 'Automatic waiting', 'Screenshot comparisons'],
bestFor: ['Single browser focus', 'Developer productivity', 'CI/CD integration']
},
webdriverio: {
flexibility: 'Multi-framework support',
features: ['App and web testing', 'Cloud services', 'Component testing'],
bestFor: ['Hybrid testing', 'Mobile apps', 'Enterprise testing']
}
}
};
}

// 工具选择建议
getRecommendation(techStack, projectRequirements) {
const recommendations = {
unit: this.recommendUnitTesting(techStack, projectRequirements),
integration: this.recommendIntegrationTesting(techStack, projectRequirements),
e2e: this.recommendE2ETesting(techStack, projectRequirements)
};

return recommendations;
}

recommendUnitTesting(techStack, requirements) {
if (techStack.includes('vite')) {
return {
tool: 'vitest',
reason: 'Vite-native testing with instant startup',
configuration: this.getVitestConfig()
};
} else if (requirements.speed && requirements.modern) {
return {
tool: 'vitest',
reason: 'Fastest test execution',
configuration: this.getVitestConfig()
};
} else {
return {
tool: 'jest',
reason: 'Mature ecosystem with extensive features',
configuration: this.getJestConfig()
};
}
}

getVitestConfig() {
return {
test: {
environment: 'jsdom',
setupFiles: ['./test/setup.js'],
globals: true,
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'test/', 'dist/']
}
}
};
}

getJestConfig() {
return {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/test/setup.js'],
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.js'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
}
}

现代测试实践

Vitest最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// vitest.config.js
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
plugins: [react()],
test: {
// 测试环境配置
environment: 'jsdom',
setupFiles: ['./test/setup.js'],

// 并行执行
threads: true,
maxThreads: 4,
minThreads: 2,

// 隔离
isolate: true,

// 类型检查
globals: true,
includeSource: ['src/**/*.{js,ts}'],

// 覆盖率
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/**',
'test/**',
'dist/**',
'**/*.d.ts',
'vite.config.ts'
],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80
}
},

// Mock配置
mockReset: true,
restoreMocks: true,

// 钩子
onConsoleLog: (log) => {
if (log.includes('fetch failed')) {
return false; // 阻止某些日志
}
}
},
resolve: {
alias: {
'@': new URL('./src', import.meta.url).pathname,
'@test': new URL('./test', import.meta.url).pathname
}
}
});

// 单元测试示例
// src/components/Button/__tests__/Button.test.js
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from '../Button';

describe('Button Component', () => {
it('renders with default props', () => {
render(<Button>Click me</Button>);

const button = screen.getByRole('button', { name: /click me/i });
expect(button).toBeInTheDocument();
expect(button).toHaveClass('btn', 'btn-primary');
expect(button).not.toBeDisabled();
});

it('calls onClick when clicked', async () => {
const mockOnClick = vi.fn();
render(<Button onClick={mockOnClick}>Click me</Button>);

const button = screen.getByRole('button');
await fireEvent.click(button);

expect(mockOnClick).toHaveBeenCalledTimes(1);
});

it('shows loading state', () => {
render(<Button loading>Click me</Button>);

const button = screen.getByRole('button');
expect(button).toBeDisabled();
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});

it('applies correct variant classes', () => {
const { rerender } = render(<Button variant="secondary">Test</Button>);
expect(screen.getByRole('button')).toHaveClass('btn-secondary');

rerender(<Button variant="outline">Test</Button>);
expect(screen.getByRole('button')).toHaveClass('btn-outline');
});
});

React Testing Library实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// 测试工具配置
// test/utils.js
import { render } from '@testing-library/react';
import { ThemeProvider } from 'contexts/ThemeContext';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';

// 创建测试查询客户端
const createTestQueryClient = () => {
return new QueryClient({
defaultOptions: {
queries: {
retry: false, // 测试中禁用重试
cacheTime: 0 // 立即清理缓存
}
}
});
};

// 自定义渲染函数
export const customRender = (ui, options = {}) => {
const {
theme = 'light',
queryClient = createTestQueryClient(),
initialEntries = ['/'],
...renderOptions
} = options;

const Wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>
<ThemeProvider initialTheme={theme}>
<BrowserRouter>
{children}
</BrowserRouter>
</ThemeProvider>
</QueryClientProvider>
);

return render(ui, { wrapper: Wrapper, ...renderOptions });
};

// 重新导出所有内容
export * from '@testing-library/react';
export { customRender as render };

// 测试辅助函数
export const waitForElementToBeRemoved = async (element) => {
const { waitForElementToBeRemoved } = await import('@testing-library/react');
return waitForElementToBeRemoved(element);
};

// 组件测试示例
// src/features/user/__tests__/UserProfile.test.js
import { describe, it, expect, vi } from 'vitest';
import { screen, waitFor, userEvent } from '@test/utils';
import { UserProfile } from '../UserProfile';
import { getUserById } from '../api';

vi.mock('../api', () => ({
getUserById: vi.fn()
}));

describe('UserProfile Component', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('displays loading state initially', () => {
getUserById.mockResolvedValue(null);

customRender(<UserProfile userId="123" />);

expect(screen.getByText(/loading/i)).toBeInTheDocument();
});

it('displays user information when data is loaded', async () => {
const mockUser = {
id: '123',
name: 'John Doe',
email: 'john@example.com',
avatar: 'avatar-url'
};

getUserById.mockResolvedValue(mockUser);

customRender(<UserProfile userId="123" />);

await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
});

it('handles error state gracefully', async () => {
getUserById.mockRejectedValue(new Error('User not found'));

customRender(<UserProfile userId="123" />);

await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});

it('allows editing user profile', async () => {
const mockUser = { id: '123', name: 'John Doe', email: 'john@example.com' };
getUserById.mockResolvedValue(mockUser);

const { container } = customRender(<UserProfile userId="123" />);

await waitFor(() => expect(screen.getByText('John Doe')).toBeInTheDocument());

const editButton = screen.getByRole('button', { name: /edit/i });
await userEvent.click(editButton);

const nameInput = screen.getByLabelText(/name/i);
await userEvent.clear(nameInput);
await userEvent.type(nameInput, 'Jane Doe');

const saveButton = screen.getByRole('button', { name: /save/i });
await userEvent.click(saveButton);

await waitFor(() => {
expect(screen.getByText('Jane Doe')).toBeInTheDocument();
});
});
});

Playwright E2E测试实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
// playwright.config.js
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
// 全局设置
testDir: './e2e',
timeout: 30 * 1000,
expect: {
timeout: 5000
},
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html', { open: 'never' }],
['json', { outputFile: 'test-results.json' }]
],

use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
actionTimeout: 0,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retry-with-video'
},

projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
}
],

webServer: {
command: 'npm run serve',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});

// 页面对象模型示例
// e2e/pages/LoginPage.js
import { expect } from '@playwright/test';

export class LoginPage {
constructor(page) {
this.page = page;
this.usernameInput = page.locator('input[name="username"]');
this.passwordInput = page.locator('input[name="password"]');
this.submitButton = page.locator('button[type="submit"]');
this.errorMessage = page.locator('.error-message');
}

async goto() {
await this.page.goto('/login');
await expect(this.usernameInput).toBeVisible();
}

async login(username, password) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.submitButton.click();
}

async expectErrorMessage(text) {
await expect(this.errorMessage).toContainText(text);
}

async expectSuccessfulLogin() {
await expect(this.page).toHaveURL('/dashboard');
await expect(this.page.locator('text=Welcome')).toBeVisible();
}
}

// API测试示例
// e2e/api/user-api.test.js
import { test, expect } from '@playwright/test';

test.describe('User API Tests', () => {
test('should create user successfully', async ({ request }) => {
const response = await request.post('/api/users', {
data: {
name: 'Test User',
email: 'test@example.com'
}
});

expect(response.status()).toBe(201);
const responseBody = await response.json();
expect(responseBody.name).toBe('Test User');
expect(responseBody.email).toBe('test@example.com');
});

test('should validate user creation', async ({ request }) => {
const response = await request.post('/api/users', {
data: {
name: '', // Invalid name
email: 'invalid-email'
}
});

expect(response.status()).toBe(400);
const responseBody = await response.json();
expect(responseBody.errors).toBeDefined();
});
});

// 端到端测试示例
// e2e/login-flow.test.js
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';

test.describe('Authentication Flow', () => {
test.beforeEach(async ({ page }) => {
// 清理测试数据
await page.goto('/api/cleanup');
});

test('successful login flow', async ({ page }) => {
const loginPage = new LoginPage(page);

await loginPage.goto();
await loginPage.login('testuser@example.com', 'password123');
await loginPage.expectSuccessfulLogin();
});

test('failed login flow', async ({ page }) => {
const loginPage = new LoginPage(page);

await loginPage.goto();
await loginPage.login('invalid@example.com', 'wrongpassword');
await loginPage.expectErrorMessage('Invalid credentials');
});

test('password reset flow', async ({ page }) => {
await page.goto('/forgot-password');

await page.locator('input[name="email"]').fill('user@example.com');
await page.locator('button[type="submit"]').click();

await expect(page.locator('text=Password reset email sent')).toBeVisible();

// 模拟邮箱中的重置链接
const resetToken = await getResetTokenFromEmail(); // 假设的辅助函数
await page.goto(`/reset-password/${resetToken}`);

await page.locator('input[name="newPassword"]').fill('newPassword123');
await page.locator('input[name="confirmPassword"]').fill('newPassword123');
await page.locator('button[type="submit"]').click();

await expect(page.locator('text=Password updated successfully')).toBeVisible();
});
});

测试策略与CI/CD集成

测试运行策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
// 智能测试运行策略
class SmartTestRunner {
constructor() {
this.strategies = {
// 变更检测运行
affected: {
name: 'Affected Tests',
description: 'Run only tests affected by code changes',
implementation: this.runAffectedTests.bind(this),
benefits: ['Faster feedback', 'Reduced CI costs', 'Focused testing']
},

// 分层运行
tiered: {
name: 'Tiered Testing',
description: 'Run tests in priority tiers',
implementation: this.runTieredTests.bind(this),
benefits: ['Quick smoke tests', 'Comprehensive validation', 'Scalable execution']
},

// 並行运行
parallel: {
name: 'Parallel Execution',
description: 'Run tests across multiple machines/threads',
implementation: this.runParallelTests.bind(this),
benefits: ['Reduced execution time', 'Better resource utilization']
},

// 渐进运行
progressive: {
name: 'Progressive Testing',
description: 'Run tests in stages based on risk',
implementation: this.runProgressiveTests.bind(this),
benefits: ['Early failure detection', 'Risk-based prioritization']
}
};
}

// 运行受影响的测试
async runAffectedTests(changedFiles, testFramework) {
const affectedTests = await this.analyzeImpact(changedFiles);

console.log(`Running ${affectedTests.length} affected tests...`);

const results = {
unit: [],
integration: [],
e2e: []
};

for (const test of affectedTests) {
if (test.type === 'unit') {
results.unit.push(await testFramework.run(test.path));
} else if (test.type === 'integration') {
results.integration.push(await testFramework.run(test.path));
} else if (test.type === 'e2e') {
results.e2e.push(await testFramework.run(test.path));
}
}

return results;
}

// 分层测试运行
async runTieredTests(testFramework) {
const tiers = {
smoke: {
tests: await this.getSmokeTests(),
timeout: 5 * 60 * 1000, // 5分钟
critical: true
},
regression: {
tests: await this.getRegressionTests(),
timeout: 30 * 60 * 1000, // 30分钟
critical: false
},
performance: {
tests: await this.getPerformanceTests(),
timeout: 15 * 60 * 1000, // 15分钟
critical: false
}
};

const results = {};

for (const [tierName, tierConfig] of Object.entries(tiers)) {
console.log(`Running ${tierName} tests...`);

const startTime = Date.now();
const tierResult = await Promise.race([
testFramework.runTests(tierConfig.tests),
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`${tierName} tests timed out`)), tierConfig.timeout)
)
]);

results[tierName] = {
...tierResult,
executionTime: Date.now() - startTime
};

// 如果是关键层级失败,停止后续测试
if (tierConfig.critical && !tierResult.success) {
console.log(`${tierName} tests failed, stopping further execution`);
break;
}
}

return results;
}

// 并行测试运行
async runParallelTests(testFramework, concurrency = 4) {
const allTests = await testFramework.discoverTests();
const testGroups = this.partitionTests(allTests, concurrency);

const promises = testGroups.map((group, index) =>
testFramework.runTests(group, { shard: index, totalShards: concurrency })
);

const results = await Promise.all(promises);

return results.reduce((acc, result, index) => {
acc[`shard-${index}`] = result;
return acc;
}, {});
}

// 测试影响分析
async analyzeImpact(changedFiles) {
const impactMap = {
// 定义文件变更对测试的影响
'src/components/Button.js': ['**/__tests__/Button.test.js', '**/__tests__/Form.test.js'],
'src/utils/validation.js': ['**/__tests__/*validation*.test.js', '**/__tests__/*form*.test.js'],
'src/api/users.js': ['**/__tests__/user*.test.js', '**/e2e/user*.test.js']
};

const affectedTests = new Set();

for (const changedFile of changedFiles) {
const relatedTests = impactMap[changedFile] || this.guessAffectedTests(changedFile);
relatedTests.forEach(test => affectedTests.add(test));
}

return Array.from(affectedTests);
}

// 智能测试选择
getSmartTestSelection() {
return {
// 根据代码变更智能选择测试
changeBased: (diff) => {
const patterns = [
{ files: ['**/components/**'], tests: ['**/__tests__/components/**'] },
{ files: ['**/api/**'], tests: ['**/__tests__/api/**', '**/e2e/**'] },
{ files: ['**/utils/**'], tests: ['**/__tests__/utils/**', '**/__tests__/**'] }
];

return patterns.filter(p =>
diff.some(f => new RegExp(p.files).test(f))
).flatMap(p => p.tests);
},

// 根据风险选择测试
riskBased: (commits) => {
const riskyChanges = commits.filter(c =>
c.message.includes('security') ||
c.message.includes('auth') ||
c.message.includes('payment')
);

if (riskyChanges.length > 0) {
return ['**/__tests__/**', '**/e2e/**']; // 运行所有测试
}

return ['**/__tests__/**']; // 只运行单元测试
}
};
}
}

// CI/CD配置示例
// .github/workflows/test.yml
/*
name: Test Suite

on:
push:
branches: [main, develop]
pull_request:
branches: [main]

jobs:
test-affected:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # 获取完整的git历史用于影响分析

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'

- name: Install dependencies
run: npm ci

- name: Detect and run affected tests
run: |
# 使用Nx或类似工具检测受影响的测试
npx nx affected --target=test --parallel=3
*/

质量门和报告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
// 测试质量门控
class TestQualityGate {
constructor() {
this.thresholds = {
coverage: {
lines: 80,
functions: 80,
branches: 80,
statements: 80
},
performance: {
lighthouse: {
accessibility: 90,
bestPractices: 90,
seo: 90,
pwa: 80
},
loading: {
fcp: 1800, // 首次内容绘制 (ms)
lcp: 2500, // 最大内容绘制 (ms)
tti: 3000 // 首次可交互 (ms)
}
},
reliability: {
flakiness: 5, // 脆弱测试比例 (%)
timeoutRate: 2, // 超时率 (%)
errorRate: 1 // 错误率 (%)
}
};
}

// 覆盖率质量检查
async checkCoverageQuality(coverageReport) {
const issues = [];

for (const [metric, threshold] of Object.entries(this.thresholds.coverage)) {
const actual = coverageReport.total[metric]?.pct || 0;

if (actual < threshold) {
issues.push({
type: 'coverage',
metric,
actual,
threshold,
severity: 'high'
});
}
}

return {
passed: issues.length === 0,
issues,
summary: {
...coverageReport.total,
passed: issues.length === 0
}
};
}

// 性能质量检查
async checkPerformanceQuality(performanceReport) {
const issues = [];

// Lighthouse指标检查
if (performanceReport.lighthouse) {
for (const [metric, threshold] of Object.entries(this.thresholds.performance.lighthouse)) {
const actual = performanceReport.lighthouse[metric];
if (actual < threshold) {
issues.push({
type: 'performance',
metric: `lighthouse.${metric}`,
actual,
threshold,
severity: 'medium'
});
}
}
}

// 加载性能检查
if (performanceReport.metrics) {
for (const [metric, threshold] of Object.entries(this.thresholds.performance.loading)) {
const actual = performanceReport.metrics[metric];
if (actual > threshold) {
issues.push({
type: 'performance',
metric: `loading.${metric}`,
actual,
threshold,
severity: 'high'
});
}
}
}

return {
passed: issues.length === 0,
issues,
summary: performanceReport
};
}

// 测试可靠性检查
async checkReliabilityQuality(testReport) {
const totalTests = testReport.summary.total;
const failedTests = testReport.summary.failed;
const skippedTests = testReport.summary.skipped;
const timeOutTests = testReport.summary.timeouts || 0;

const flakinessRate = ((testReport.flaky || 0) / totalTests) * 100;
const timeoutRate = (timeOutTests / totalTests) * 100;
const errorRate = (failedTests / totalTests) * 100;

const issues = [];

if (flakinessRate > this.thresholds.reliability.flakiness) {
issues.push({
type: 'reliability',
metric: 'flakinessRate',
actual: flakinessRate,
threshold: this.thresholds.reliability.flakiness,
severity: 'high'
});
}

if (timeoutRate > this.thresholds.reliability.timeoutRate) {
issues.push({
type: 'reliability',
metric: 'timeoutRate',
actual: timeoutRate,
threshold: this.thresholds.reliability.timeoutRate,
severity: 'medium'
});
}

if (errorRate > this.thresholds.reliability.errorRate) {
issues.push({
type: 'reliability',
metric: 'errorRate',
actual: errorRate,
threshold: this.thresholds.reliability.errorRate,
severity: 'high'
});
}

return {
passed: issues.length === 0,
issues,
summary: {
flakinessRate,
timeoutRate,
errorRate,
passed: issues.length === 0
}
};
}

// 综合质量报告
async generateQualityReport(testResults, coverageReport, performanceReport) {
const coverageCheck = await this.checkCoverageQuality(coverageReport);
const performanceCheck = await this.checkPerformanceQuality(performanceReport);
const reliabilityCheck = await this.checkReliabilityQuality(testResults);

const overallPassed =
coverageCheck.passed &&
performanceCheck.passed &&
reliabilityCheck.passed;

return {
overallPassed,
coverage: coverageCheck,
performance: performanceCheck,
reliability: reliabilityCheck,
summary: {
coverage: coverageCheck.summary,
performance: performanceCheck.summary,
reliability: reliabilityCheck.summary,
timestamp: new Date().toISOString()
}
};
}

// 质量门控执行
async executeQualityGate(reports) {
const qualityReport = await this.generateQualityReport(
reports.test,
reports.coverage,
reports.performance
);

if (!qualityReport.overallPassed) {
console.error('Quality gate failed:');
qualityReport.coverage.issues.forEach(issue =>
console.error(`Coverage: ${issue.metric} (${issue.actual}% < ${issue.threshold}%)`)
);
qualityReport.performance.issues.forEach(issue =>
console.error(`Performance: ${issue.metric} (${issue.actual} > ${issue.threshold})`)
);
qualityReport.reliability.issues.forEach(issue =>
console.error(`Reliability: ${issue.metric} (${issue.actual}% > ${issue.threshold}%)`)
);

throw new Error('Quality gate failed - blocking deployment');
}

console.log('All quality gates passed!');
return qualityReport;
}
}

现代前端测试策略强调自动化、智能化和快速反馈。通过合理的工具选型、分层测试和智能运行策略,可以构建高效可靠的测试体系,为项目质量提供坚实保障。

总结

  现代前端测试策略的升级是一个系统性工程,需要从工具选型、测试分层、运行策略到质量管控等多个维度进行整体设计。通过采用Vitest、React Testing Library、Playwright等现代测试工具,实施智能的测试运行策略,建立完善的质量门控机制,可以显著提升测试效率和应用质量。

  关键要点包括:

  1. 工具现代化:选用速度快、生态好的现代测试工具
  2. 策略智能化:根据变更智能选择测试,提高反馈效率
  3. 分层明确化:清晰定义単元、集成、E2E测试的职责
  4. 质量自动化:建立自动化的质量门控机制
  5. 报告透明化:提供清晰的测试报告和质量指标

  随着前端技术的不断发展,测试策略也需要持续演进。团队应该定期评估测试策略的有效性,及时调整工具和流程,确保测试体系始终为项目发展提供有力支撑。

bulb