Playwright端到端测试框架配置与跨浏览器自动化测试实战

Playwright架构设计与浏览器自动化原理

Playwright是微软开源的端到端测试框架,通过Chrome DevTools Protocol(CDP)和WebSocket直接控制浏览器实例,支持Chromium、Firefox和WebKit三引擎跨浏览器测试。相比Selenium基于WebDriver HTTP协议的通信方式,Playwright的CDP直连模式减少了网络中间层,测试执行速度更快,浏览器控制粒度更细。在前端工程化体系中,Playwright已成为替代Cypress和Selenium的主流E2E测试方案。

Playwright的架构分为三层:上层API(多语言绑定,支持TypeScript/Python/Java/C#)、中层协议层(CDP/WebSocket通信)、底层浏览器实例管理。每个Browser可创建多个BrowserContext(隔离的会话环境),每个Context可创建多个Page(标签页)。BrowserContext之间的Cookie、LocalStorage和缓存完全隔离,这一设计天然支持多用户角色并行测试。

import { test, expect, chromium, firefox, webkit } from '@playwright/test';

// 跨浏览器测试配置
test('跨浏览器登录测试', async ({ browserName, page }) => {
  await page.goto('https://app.example.com/login');
  await page.fill('[data-testid="username"]', 'testuser');
  await page.fill('[data-testid="password"]', 'password123');
  await page.click('[data-testid="login-button"]');
  
  await expect(page.locator('[data-testid="welcome"]')).toBeVisible();
  await expect(page).toHaveURL(/\/dashboard/);
});

项目初始化与playwright.config配置

Playwright通过init命令初始化项目,自动生成配置文件和示例测试。playwright.config.ts是核心配置入口,控制浏览器类型、测试目录、并行策略、报告输出和CI集成。

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,
  reporter: [
    ['html', { outputFolder: 'playwright-report' }],
    ['junit', { outputFile: 'test-results/junit.xml' }],
    ['list']
  ],
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    actionTimeout: 10000,
    navigationTimeout: 30000,
    viewport: { width: 1280, height: 720 },
    extraHTTPHeaders: {
      'X-Test-Mode': 'true'
    }
  },
  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'] },
    },
    {
      name: 'mobile-safari',
      use: { ...devices['iPhone 13'] },
    }
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 30000
  }
});

projects配置实现跨浏览器和多设备测试。每个project继承devices中预置的设备配置(User-Agent、viewport、isMobile等)。webServer配置在测试前自动启动开发服务器,CI环境中每次启动新实例,本地开发复用已有实例。

页面对象模型与测试用例组织

页面对象模型(Page Object Model, POM)将页面元素定位和操作逻辑封装到独立类中,测试用例通过调用页面对象方法完成交互。这种模式降低代码重复,页面变更时只需修改页面对象而非每个测试用例。

// pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly usernameInput: Locator;
  readonly passwordInput: Locator;
  readonly loginButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.usernameInput = page.locator('[data-testid="username"]');
    this.passwordInput = page.locator('[data-testid="password"]');
    this.loginButton = page.locator('[data-testid="login-button"]');
    this.errorMessage = page.locator('[data-testid="error-msg"]');
  }

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

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

  async expectError(message: string) {
    await expect(this.errorMessage).toContainText(message);
  }
}

// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

test.describe('登录功能', () => {
  test('正确凭证登录成功', async ({ page }) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.login('admin', 'admin123');
    
    await expect(page).toHaveURL(/\/dashboard/);
    await expect(page.locator('[data-testid="user-avatar"]')).toBeVisible();
  });

  test('错误密码显示提示', async ({ page }) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.login('admin', 'wrong');
    
    await loginPage.expectError('用户名或密码错误');
  });
});

Locator API是Playwright推荐的元素定位方式。相比于Page.$和Page.$$返回ElementHandle,Locator是动态的——每次操作时重新查询DOM,自动处理元素重新渲染场景。优先使用data-testid属性定位,避免依赖CSS类名或XPath等易变的定位策略。

网络拦截与Mock响应配置

Playwright的page.route()方法拦截和修改网络请求,实现API Mock、请求延迟模拟和响应篡改。这一能力在后端API未就绪或需要测试异常场景时极为实用。

test('商品列表加载失败重试', async ({ page }) => {
  // Mock第一次请求返回500,第二次返回正常数据
  let requestCount = 0;
  await page.route('**/api/products', async (route) => {
    requestCount++;
    if (requestCount === 1) {
      await route.fulfill({
        status: 500,
        contentType: 'application/json',
        body: JSON.stringify({ error: 'Internal Server Error' })
      });
    } else {
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify({
          products: [
            { id: 1, name: 'Product A', price: 99 },
            { id: 2, name: 'Product B', price: 199 }
          ]
        })
      });
    }
  });

  await page.goto('/products');
  
  // 验证错误提示出现后自动重试
  await expect(page.locator('[data-testid="error-toast"]')).toBeVisible();
  await expect(page.locator('[data-testid="product-card"]')).toHaveCount(2);
});

test('性能测试:API响应延迟', async ({ page }) => {
  await page.route('**/api/dashboard', async (route) => {
    await new Promise(resolve => setTimeout(resolve, 2000));
    await route.continue();
  });
  
  const start = Date.now();
  await page.goto('/dashboard');
  const loadTime = Date.now() - start;
  
  expect(loadTime).toBeGreaterThan(2000);
  expect(loadTime).toBeLessThan(5000);
});

并行测试与测试隔离策略

Playwright默认按测试文件级别并行执行,每个测试文件运行在独立的Worker进程中。同一文件内的测试用例串行执行,共享BrowserContext。通过test.describe.parallel可以标记需要并行的测试组。

// 文件级并行(默认)
test.describe('订单流程', () => {
  test('创建订单', async ({ page }) => { ... });
  test('取消订单', async ({ page }) => { ... });
});

// 强制并行
test.describe.parallel('多用户并发', () => {
  test('用户A下单', async ({ browser }) => {
    const ctx = await browser.newContext();
    const page = await ctx.newPage();
    // 独立BrowserContext
    await ctx.close();
  });
  
  test('用户B下单', async ({ browser }) => {
    const ctx = await browser.newContext();
    const page = await ctx.newPage();
    await ctx.close();
  });
});

测试隔离通过BrowserContext实现。每个测试默认获得全新的BrowserContext,无Cookie和LocalStorage残留。对于需要保持登录状态的测试场景,可以通过storageState保存和恢复认证状态。

// global-setup.ts - 预登录保存状态
import { chromium, FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://app.example.com/login');
  await page.fill('[data-testid="username"]', 'admin');
  await page.fill('[data-testid="password"]', 'admin123');
  await page.click('[data-testid="login-button"]');
  await page.waitForURL('**/dashboard');
  
  await page.context().storageState({ path: 'tests/.auth/admin.json' });
  await browser.close();
}

export default globalSetup;

// playwright.config.ts
use: {
  storageState: 'tests/.auth/admin.json'
}

全局Setup在所有测试前执行一次,保存登录状态到JSON文件。后续测试自动加载该状态,跳过登录流程,减少重复操作并提升测试执行效率。

CI/CD集成与测试报告

Playwright在CI环境中通过Docker容器执行,确保跨平台一致性。GitHub Actions集成配置:

# .github/workflows/e2e-test.yml
name: E2E Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: '20'
    - run: npm ci
    - run: npx playwright install --with-deps
    - run: npx playwright test
    - uses: actions/upload-artifact@v4
      if: always()
      with:
        name: playwright-report
        path: playwright-report/

Playwright的HTML报告包含测试用例详情、失败截图、录屏视频和Trace Viewer。Trace Viewer是Playwright独有的调试工具,以时间轴形式回放测试执行全过程,展示每一步操作的DOM快照、网络请求和控制台日志,定位间歇性失败问题效率极高。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/playwright-duan-dao-duan-ce-shi-kuang-jia-pei-zhi-yu-kua/

(0)
小编小编
上一篇 1小时前
下一篇 1小时前

相关推荐