Playwright para testes E2E: configuração, boas práticas e CI

1. Introdução ao Playwright e Testes E2E

Testes End-to-End (E2E) simulam o comportamento real do usuário em uma aplicação, validando fluxos completos desde a interface até o backend. São críticos para garantir que funcionalidades críticas não sejam quebradas após deploys, especialmente em aplicações com múltiplas integrações.

O Playwright, desenvolvido pela Microsoft, destaca-se por oferecer suporte nativo a Chromium, Firefox e WebKit, executando testes em paralelo com alta velocidade. Diferente do Cypress, que opera exclusivamente no navegador Chromium, o Playwright permite testar em múltiplos motores de renderização com uma única API. Comparado ao Selenium, o Playwright elimina a necessidade de WebDrivers manuais, oferece auto-waiting inteligente e melhor performance em testes headless.

2. Configuração Inicial do Projeto

Para iniciar, instale o Playwright via npm:

npm init playwright@latest

O comando gera automaticamente a estrutura de diretórios:

project/
├── tests/
│   └── example.spec.ts
├── playwright.config.ts
├── package.json
└── node_modules/

Configure os navegadores e viewports no arquivo playwright.config.ts:

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  retries: 2,
  use: {
    baseURL: 'http://localhost:3000',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
});

Estrutura de diretórios recomendada:

tests/
├── e2e/
│   ├── login.spec.ts
│   └── checkout.spec.ts
├── fixtures/
│   └── auth.fixture.ts
└── utils/
    └── helpers.ts

3. Escrevendo Testes Robusto e Confiáveis

Priorize localizadores modernos que simulam a interação real do usuário:

// ✅ Recomendado: localizadores acessíveis
await page.getByRole('button', { name: 'Enviar' }).click();
await page.getByText('Bem-vindo').isVisible();
await page.getByTestId('user-profile').click();

// ❌ Evite: seletores frágeis
await page.$('#submit-btn');
await page.$('.container > div:nth-child(2)');

Use asserções inteligentes com polling automático:

await expect(page.getByRole('heading')).toHaveText('Dashboard');
await expect(page.getByTestId('loading-spinner')).not.toBeVisible();
await expect(page.locator('.error-message')).toHaveCount(0);

Evite waitForTimeout fixo. Prefira esperas condicionais:

// ✅ Correto: espera por URL específica
await page.waitForURL('**/dashboard');

// ✅ Correto: espera por elemento visível
await page.waitForSelector('[data-testid="user-table"]', { state: 'visible' });

// ❌ Incorreto: tempo fixo
await page.waitForTimeout(3000);

4. Gerenciamento de Estado e Fixtures

Reutilize sessões de autenticação para evitar login repetido:

// auth.fixture.ts
import { test as base } from '@playwright/test';
import path from 'path';

export const test = base.extend({
  authenticatedPage: async ({ page, context }, use) => {
    const storageState = path.resolve('./auth.json');

    // Realiza login uma vez e salva o estado
    await page.goto('/login');
    await page.fill('[name="email"]', 'user@test.com');
    await page.fill('[name="password"]', 'password123');
    await page.click('button[type="submit"]');
    await context.storageState({ path: storageState });

    await use(page);
  },
});

Mock de APIs para isolar dependências:

await page.route('**/api/users', async route => {
  await route.fulfill({
    status: 200,
    body: JSON.stringify([{ id: 1, name: 'Mock User' }]),
  });
});

5. Integração Contínua (CI) e Automação

Exemplo de pipeline GitHub Actions:

name: Playwright Tests
on: [push, pull_request]

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: 18
    - name: Install dependencies
      run: npm ci
    - name: Install Playwright browsers
      run: npx playwright install --with-deps
    - name: Run tests
      run: npx playwright test
    - uses: actions/upload-artifact@v4
      if: always()
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30

Para paralelismo e sharding:

npx playwright test --shard=1/4
npx playwright test --workers=4

Configure relatórios no playwright.config.ts:

reporter: [
  ['html'],
  ['json', { outputFile: 'test-results.json' }],
  ['allure-playwright'],
],

6. Manutenção e Boas Práticas Avançadas

Implemente Page Object Model simplificado:

// pages/LoginPage.ts
export class LoginPage {
  constructor(private page: Page) {}

  async login(email: string, password: string) {
    await this.page.getByLabel('Email').fill(email);
    await this.page.getByLabel('Senha').fill(password);
    await this.page.getByRole('button', { name: 'Entrar' }).click();
  }

  async getErrorMessage() {
    return this.page.getByTestId('error-message').textContent();
  }
}

Trate flaky tests com retries e logging:

// playwright.config.ts
export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  use: {
    trace: 'retain-on-failure',
  },
});

Para ambientes dinâmicos, use dados gerados automaticamente:

const uniqueEmail = `user_${Date.now()}@test.com`;
const today = new Date().toISOString().split('T')[0];

7. Monitoramento e Evolução Contínua

Integre com dashboards para visibilidade:

npx playwright show-report

Para feedback loop rápido, execute testes críticos localmente e testes completos em CI:

npm run test:critical  # testes rápidos (1-2 min)
npm run test:full      # todos os testes (10-15 min)

Próximos passos importantes:

  • Testes visuais com toMatchSnapshot():
await expect(page).toMatchSnapshot('homepage.png');
  • Testes de acessibilidade com Axe:
import { injectAxe, checkA11y } from 'axe-playwright';

await injectAxe(page);
await checkA11y(page);

Referências