e2e-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseE2E Testing Patterns
端到端测试模式
Build reliable, fast, and maintainable end-to-end test suites with Playwright and Cypress.
使用Playwright和Cypress构建可靠、快速且可维护的端到端测试套件。
What to Test with E2E
端到端测试的适用场景
Good for:
- Critical user journeys (login, checkout, signup)
- Complex interactions (drag-and-drop, multi-step forms)
- Cross-browser compatibility
- Real API integration
Not for:
- Unit-level logic (use unit tests)
- API contracts (use integration tests)
- Edge cases (too slow)
适用场景:
- 关键用户流程(登录、结账、注册)
- 复杂交互(拖拽、多步骤表单)
- 跨浏览器兼容性
- 真实API集成
不适用场景:
- 单元级逻辑(使用单元测试)
- API契约(使用集成测试)
- 边缘情况(速度过慢)
Playwright Configuration
Playwright配置
typescript
// playwright.config.ts
export default defineConfig({
testDir: './e2e',
timeout: 30000,
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile', use: { ...devices['iPhone 13'] } },
],
});typescript
// playwright.config.ts
export default defineConfig({
testDir: './e2e',
timeout: 30000,
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile', use: { ...devices['iPhone 13'] } },
],
});Page Object Model
页面对象模型
typescript
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly loginButton: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.loginButton = page.getByRole('button', { name: 'Login' });
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.page.getByLabel('Password').fill(password);
await this.loginButton.click();
}
}typescript
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly loginButton: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.loginButton = page.getByRole('button', { name: 'Login' });
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.page.getByLabel('Password').fill(password);
await this.loginButton.click();
}
}Waiting Strategies
等待策略
typescript
// Bad: Fixed timeouts
await page.waitForTimeout(3000); // Flaky!
// Good: Wait for conditions
await expect(page.getByText('Welcome')).toBeVisible();
await page.waitForURL('/dashboard');
// Wait for API response
const responsePromise = page.waitForResponse(
r => r.url().includes('/api/users') && r.status() === 200
);
await page.click('button');
await responsePromise;typescript
// Bad: Fixed timeouts
await page.waitForTimeout(3000); // Flaky!
// Good: Wait for conditions
await expect(page.getByText('Welcome')).toBeVisible();
await page.waitForURL('/dashboard');
// Wait for API response
const responsePromise = page.waitForResponse(
r => r.url().includes('/api/users') && r.status() === 200
);
await page.click('button');
await responsePromise;Network Mocking
网络模拟
typescript
test('displays error when API fails', async ({ page }) => {
await page.route('**/api/users', route => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Server Error' }),
});
});
await page.goto('/users');
await expect(page.getByText('Failed to load')).toBeVisible();
});typescript
test('displays error when API fails', async ({ page }) => {
await page.route('**/api/users', route => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Server Error' }),
});
});
await page.goto('/users');
await expect(page.getByText('Failed to load')).toBeVisible();
});Visual Regression
视觉回归测试
typescript
test('homepage looks correct', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
maxDiffPixels: 100,
});
});typescript
test('homepage looks correct', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
maxDiffPixels: 100,
});
});Accessibility Testing
可访问性测试
typescript
import AxeBuilder from '@axe-core/playwright';
test('no accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});typescript
import AxeBuilder from '@axe-core/playwright';
test('no accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});Best Practices
最佳实践
- Use Data Attributes: for stable selectors
data-testid - Test User Behavior: Click, type, see - not implementation
- Keep Tests Independent: Each test runs in isolation
- Clean Up Test Data: Create and destroy per test
- Use Page Objects: Encapsulate page logic
- Optimize for Speed: Mock when possible, parallel execution
- 使用数据属性:作为稳定选择器
data-testid - 测试用户行为:模拟点击、输入、查看,而非实现细节
- 保持测试独立性:每个测试独立运行
- 清理测试数据:每个测试创建并销毁测试数据
- 使用页面对象:封装页面逻辑
- 优化速度:尽可能使用模拟,并行执行
Bad vs Good Selectors
不良选择器 vs 良好选择器
typescript
// Bad
cy.get('.btn.btn-primary.submit-button').click();
cy.get('div > form > div:nth-child(2) > input').type('text');
// Good
cy.getByRole('button', { name: 'Submit' }).click();
cy.get('[data-testid="email-input"]').type('user@example.com');typescript
// Bad
cy.get('.btn.btn-primary.submit-button').click();
cy.get('div > form > div:nth-child(2) > input').type('text');
// Good
cy.getByRole('button', { name: 'Submit' }).click();
cy.get('[data-testid="email-input"]').type('user@example.com');Debugging
调试
bash
undefinedbash
undefinedHeaded mode
Headed mode
npx playwright test --headed
npx playwright test --headed
Debug mode (step through)
Debug mode (step through)
npx playwright test --debug
npx playwright test --debug
Trace viewer
Trace viewer
npx playwright show-trace trace.zip
undefinednpx playwright show-trace trace.zip
undefined