e2e-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

E2E 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

最佳实践

  1. Use Data Attributes:
    data-testid
    for stable selectors
  2. Test User Behavior: Click, type, see - not implementation
  3. Keep Tests Independent: Each test runs in isolation
  4. Clean Up Test Data: Create and destroy per test
  5. Use Page Objects: Encapsulate page logic
  6. Optimize for Speed: Mock when possible, parallel execution
  1. 使用数据属性
    data-testid
    作为稳定选择器
  2. 测试用户行为:模拟点击、输入、查看,而非实现细节
  3. 保持测试独立性:每个测试独立运行
  4. 清理测试数据:每个测试创建并销毁测试数据
  5. 使用页面对象:封装页面逻辑
  6. 优化速度:尽可能使用模拟,并行执行

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
undefined
bash
undefined

Headed 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
undefined
npx playwright show-trace trace.zip
undefined