Alternar tema

Testes E2E no Next.js com Playwright: guia prático de automação

Easton editorial illustration: rendering-mode selector

Na lista de bugs, mais uma etiqueta vermelha de “urgente”: o fluxo de pagamento havia quebrado outra vez. No ambiente de testes tudo funcionava; em produção, não.

Na semana anterior, durante a publicação, eu tinha aberto manualmente mais de 30 páginas, preenchido uma dezena de formulários e alternado entre três navegadores. Ainda assim, deixei passar um botão que só aparecia no celular depois de rolar até o fim da tela. Então chegou a mensagem da gerente de produto: “Os usuários dizem que não conseguem usar o cupom”.

Não dava mais para continuar testando tudo à mão. Mais cedo ou mais tarde, o teste manual acabaria desgastando tanto a mim quanto a equipe.

Na escolha da ferramenta, avaliei Cypress, Selenium, Puppeteer e Playwright. Acabei optando pelo Playwright por oferecer testes em vários navegadores e uma configuração mais simples do que a do Cypress. Na primeira semana, ele encontrou cinco bugs que nunca tinham aparecido nos testes manuais: alguns eram problemas de estilo exclusivos do Firefox; outros vinham de condições de corrida em APIs assíncronas.

Também enfrentei algumas armadilhas no começo. Reescrevi o arquivo de configuração várias vezes e refiz os casos de teste duas vezes. Agora, porém, o fluxo está estável: o CI/CD é automático, todos os commits passam pela suíte e a quantidade de bugs em produção caiu pela metade.

Por que escolher Playwright em vez de Cypress

Quem já usou Cypress sabe que a configuração é simples, a documentação é clara e a comunidade é ativa. Então por que não fiquei com ele?

Três pontos pesaram na decisão.

Suporte fraco a vários navegadores. O suporte do Cypress ao Firefox e ao Safari nunca foi tão sólido quanto o suporte ao Chromium. Pode parecer pouco importante, mas eu já enfrentei um problema sério: a página de pagamento funcionava perfeitamente no Chrome e ficava em branco no Safari por usar uma propriedade CSS não compatível. O Playwright oferece suporte nativo aos três mecanismos Chromium, Firefox e WebKit, então uma única suíte cobre os principais navegadores.

Velocidade dos testes. O paralelismo do Playwright é muito mais eficiente. No Cypress, 50 casos executados em série podem levar mais de dez minutos; com 8 workers em paralelo, o Playwright conclui os mesmos casos em cerca de cinco minutos. No CI/CD, cada minuto custa dinheiro, e essa diferença pesa.

Design da API. Para ser sincero, estranhei a mudança do Cypress para o Playwright. O encadeamento de chamadas do Cypress é agradável de escrever. Depois de algum tempo com o async/await do Playwright, percebi que essa forma combina melhor com o JavaScript moderno e também com o estilo dos Server Components do Next.js.

Isso não significa que Cypress seja uma ferramenta ruim. Se o projeto só precisa testar o Chrome e a equipe ainda tem pouca experiência com testes, ele realmente é mais fácil de adotar. As ferramentas de depuração são excelentes, e o recurso de viagem no tempo permite visualizar diretamente cada etapa, o que ajuda bastante quem está começando.

Para as minhas necessidades, porém, o Playwright era mais adequado:

  • testes entre navegadores;
  • experiência prévia com Next.js e React, sem dificuldade com async/await;
  • retorno rápido no ambiente de CI;
  • testes de API Routes e páginas com SSR.

Não existe uma ferramenta absolutamente melhor; depende do cenário. Minha recomendação é começar com Cypress se o projeto ainda é pequeno e a equipe tem pouca prática com testes. Se o projeto já cresceu e o objetivo é investir em automação no longo prazo, o Playwright tende a ser a melhor escolha.

Configuração prática de Next.js com Playwright

Instalar o Playwright é simples:

npm init playwright@latest
# Ou use pnpm
pnpm create playwright

Durante a instalação, algumas perguntas serão exibidas. Estas são as minhas escolhas:

  • TypeScript? Yes — recomendo muito, pois as dicas de tipo evitam vários erros.
  • Diretório de testes? tests — o padrão funciona bem.
  • GitHub Actions? Yes — será usado mais adiante no CI/CD.

Após a instalação, o projeto ganha estes arquivos:

your-nextjs-project/
├── tests/               # Diretório dos casos de teste
│   └── example.spec.ts
├── playwright.config.ts # Configuração do Playwright
└── .github/
    └── workflows/
        └── playwright.yml  # Configuração de CI

Armadilhas do arquivo de configuração

O playwright.config.ts inicial serve para projetos web genéricos. Em um projeto Next.js, alguns ajustes são necessários. Depois de seis meses de uso, esta foi a configuração mais estável para mim:

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

export default defineConfig({
  // Diretório de testes
  testDir: './tests',

  // Timeout global: 30 segundos por teste
  timeout: 30 * 1000,

  // Timeout global de expectativa: 5 segundos para localizar elementos
  expect: {
    timeout: 5000,
  },

  // Tentativas após falha (recomendadas no CI)
  retries: process.env.CI ? 2 : 0,

  // Número de workers paralelos (minha máquina tem 8 núcleos, então uso 4)
  workers: process.env.CI ? 2 : 4,

  // Relatórios de teste
  reporter: [
    ['html'],                    // Gera relatório HTML
    ['list'],                    // Mostra a lista no terminal
    process.env.CI ? ['github'] : ['list'], // Usa o formato do GitHub no CI
  ],

  // Inicia o servidor de desenvolvimento do Next.js
  webServer: {
    command: 'npm run dev',
    port: 3000,
    timeout: 120 * 1000,         // A primeira inicialização do Next.js pode compilar
    reuseExistingServer: !process.env.CI, // Reutiliza o servidor local e economiza tempo
  },

  // Projetos de teste em vários navegadores
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    // Teste em dispositivo móvel (opcional)
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
  ],

  // Configuração global
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',      // Registra trace na falha para facilitar a depuração
    screenshot: 'only-on-failure', // Captura a tela em caso de falha
    video: 'retain-on-failure',   // Mantém a gravação em caso de falha
  },
});

Alguns problemas fáceis de encontrar

  1. Dê tempo suficiente ao webServer.timeout. No começo, usei 30 segundos, mas a primeira inicialização do Next.js precisa compilar e expirava com frequência. Com 120 segundos, ficou estável.

  2. No desenvolvimento local, use reuseExistingServer: true. Caso contrário, o Next.js reinicia a cada execução, e a espera incomoda.

  3. Não configure workers demais. Quando usei um worker por núcleo da CPU, o computador travava durante a execução. Metade dos núcleos equilibra velocidade e estabilidade.

  4. O teste em dispositivos móveis é opcional. Em um projeto Next.js responsivo, adicionar Mobile Chrome pode revelar bugs específicos do celular, mas também pode dobrar o tempo da suíte. Decida conforme a necessidade.

Com a configuração pronta, execute o exemplo oficial:

npx playwright test

Se aparecer passed em verde, o ambiente está funcionando. Agora já é possível escrever casos reais.

Boas práticas para interação com páginas usando Page Object Model

Quando comecei, coloquei todo o código de teste em um único arquivo. O teste da página de login passou de 100 linhas, com page.locator, page.fill e page.click por toda parte. Quando o seletor de um botão mudou, precisei alterar mais de dez arquivos de teste.

Depois adotei o Page Object Model (POM). Em resumo, ele encapsula as ações de uma página em uma classe; os casos de teste chamam métodos em vez de manipular elementos diretamente.

Teste sem POM: o que evitar

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

test('用户登录', async ({ page }) => {
  await page.goto('/login');

  // 直接操作元素,代码重复
  await page.locator('input[name="email"]').fill('[email protected]');
  await page.locator('input[name="password"]').fill('password123');
  await page.locator('button[type="submit"]').click();

  await expect(page.locator('h1')).toContainText('Dashboard');
});

test('登录失败提示', async ({ page }) => {
  await page.goto('/login');

  // 又来一遍同样的操作...
  await page.locator('input[name="email"]').fill('[email protected]');
  await page.locator('input[name="password"]').fill('wrongpass');
  await page.locator('button[type="submit"]').click();

  await expect(page.locator('.error')).toBeVisible();
});

O problema fica evidente: se o designer trocar input[name="email"] por input[id="email"], todos os testes precisarão mudar.

Refatoração com POM: abordagem recomendada

Primeiro, crie o Page Object:

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

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;
  readonly errorMessage: Locator;
  readonly dashboardTitle: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.locator('input[name="email"]');
    this.passwordInput = page.locator('input[name="password"]');
    this.submitButton = page.locator('button[type="submit"]');
    this.errorMessage = page.locator('.error');
    this.dashboardTitle = page.locator('h1');
  }

  // Encapsula a operação de login
  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }

  // Encapsula a navegação
  async goto() {
    await this.page.goto('/login');
  }

  // Encapsula as verificações
  async expectLoginSuccess() {
    await this.dashboardTitle.waitFor();
    await expect(this.dashboardTitle).toContainText('Dashboard');
  }

  async expectLoginError() {
    await expect(this.errorMessage).toBeVisible();
  }
}

Os casos de teste ficam bem mais simples:

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

test('用户登录', async ({ page }) => {
  const loginPage = new LoginPage(page);

  await loginPage.goto();
  await loginPage.login('[email protected]', 'password123');
  await loginPage.expectLoginSuccess();
});

test('登录失败提示', async ({ page }) => {
  const loginPage = new LoginPage(page);

  await loginPage.goto();
  await loginPage.login('[email protected]', 'wrongpass');
  await loginPage.expectLoginError();
});

Agora, uma mudança de seletor exige alteração apenas em LoginPage.ts. Além disso, o teste se lê quase como linguagem natural, o que facilita a entrada de alguém novo na equipe.

Estrutura de diretórios em um projeto real

tests/
├── pages/                  # Page Objects
│   ├── LoginPage.ts
│   ├── DashboardPage.ts
│   └── CheckoutPage.ts
├── fixtures/               # Dados de teste e utilitários
│   └── testData.ts
├── auth.spec.ts           # Testes de autenticação
├── checkout.spec.ts       # Testes do fluxo de pagamento
└── dashboard.spec.ts      # Testes do Dashboard

Armadilhas que encontrei e minhas recomendações

  1. Não encapsule demais. Nem toda página precisa de um Page Object. Se uma página será testada uma única vez, escreva diretamente no teste; não adote POM apenas por formalidade.

  2. Dê nomes semânticos aos métodos. async fillLoginForm() comunica melhor do que async fillForm(). Daqui a seis meses, você agradecerá.

  3. Encapsule a lógica de espera no Page Object. A espera automática do Playwright é inteligente, mas às vezes ainda é preciso chamar waitFor(). Esconder essa lógica no Page Object mantém o caso de teste limpo.

  4. Gerencie os dados de teste separadamente. Eu centralizo usuários e senhas em fixtures/testData.ts:

// tests/fixtures/testData.ts
export const testUsers = {
  validUser: {
    email: '[email protected]',
    password: 'password123'
  },
  invalidUser: {
    email: '[email protected]',
    password: 'wrongpass'
  }
};

Depois, basta importar esses dados:

import { testUsers } from './fixtures/testData';

await loginPage.login(testUsers.validUser.email, testUsers.validUser.password);

Com esse padrão, o custo de manutenção caiu bastante. Hoje meu processo é basicamente: definir o Page Object, escrever algumas linhas de teste e concluir.

Testes E2E de API Routes

As API Routes do Next.js também fazem parte da aplicação e precisam de testes. Antes, eu verificava cada endpoint manualmente no Postman, o que consumia muito tempo. Agora faço isso diretamente no Playwright, sem sequer abrir um navegador.

O Playwright fornece o objeto request, capaz de enviar requisições HTTP e adequado para testar API Routes do Next.js.

Teste básico de API

Este exemplo testa um endpoint que retorna uma lista de usuários:

// tests/api/users.spec.ts
import { test, expect } from '@playwright/test';

test.describe('用户 API 测试', () => {
  test('GET /api/users - 获取用户列表', async ({ request }) => {
    const response = await request.get('/api/users');

    // 验证状态码
    expect(response.status()).toBe(200);

    // 验证响应格式
    const users = await response.json();
    expect(Array.isArray(users)).toBeTruthy();
    expect(users.length).toBeGreaterThan(0);

    // 验证数据结构
    expect(users[0]).toHaveProperty('id');
    expect(users[0]).toHaveProperty('email');
    expect(users[0]).toHaveProperty('name');
  });

  test('POST /api/users - 创建用户', async ({ request }) => {
    const newUser = {
      email: '[email protected]',
      name: 'Test User',
      password: 'password123'
    };

    const response = await request.post('/api/users', {
      data: newUser
    });

    expect(response.status()).toBe(201);

    const createdUser = await response.json();
    expect(createdUser.email).toBe(newUser.email);
    expect(createdUser).not.toHaveProperty('password'); // 密码不应该返回
  });

  test('POST /api/users - 邮箱重复应返回错误', async ({ request }) => {
    const duplicateUser = {
      email: '[email protected]',
      name: 'Duplicate User',
      password: 'password123'
    };

    const response = await request.post('/api/users', {
      data: duplicateUser
    });

    expect(response.status()).toBe(400);

    const error = await response.json();
    expect(error.message).toContain('邮箱已存在');
  });
});

Teste de API com autenticação

Em projetos reais, muitos endpoints só podem ser acessados após o login. Nesse caso, obtenha primeiro o token e envie-o no cabeçalho:

// tests/api/auth.spec.ts
import { test, expect } from '@playwright/test';

let authToken: string;

test.describe('需要认证的 API', () => {
  // 所有测试前先登录获取 token
  test.beforeAll(async ({ request }) => {
    const response = await request.post('/api/auth/login', {
      data: {
        email: '[email protected]',
        password: 'password123'
      }
    });

    const { token } = await response.json();
    authToken = token;
  });

  test('GET /api/profile - 获取用户资料', async ({ request }) => {
    const response = await request.get('/api/profile', {
      headers: {
        'Authorization': `Bearer ${authToken}`
      }
    });

    expect(response.status()).toBe(200);

    const profile = await response.json();
    expect(profile.email).toBe('[email protected]');
  });

  test('未登录访问应返回 401', async ({ request }) => {
    const response = await request.get('/api/profile');
    expect(response.status()).toBe(401);
  });
});

Teste combinado: página e API

O recurso mais poderoso é combinar testes de página e de API. Para testar a criação de um artigo, por exemplo, faço assim:

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

test('发布文章完整流程', async ({ page, request }) => {
  // 1. 先通过页面登录
  await page.goto('/login');
  await page.fill('input[name="email"]', '[email protected]');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');

  // 2. 进入文章编辑页
  await page.goto('/posts/new');
  await page.fill('input[name="title"]', '测试文章标题');
  await page.fill('textarea[name="content"]', '这是测试内容');
  await page.click('button:has-text("发布")');

  // 3. 等待跳转到文章详情页
  await page.waitForURL(/\/posts\/\d+/);

  // 4. 通过 API 验证文章确实创建成功
  const url = page.url();
  const postId = url.split('/').pop();

  const response = await request.get(`/api/posts/${postId}`);
  expect(response.status()).toBe(200);

  const post = await response.json();
  expect(post.title).toBe('测试文章标题');
  expect(post.content).toBe('这是测试内容');
  expect(post.status).toBe('published');
});

Essa abordagem valida tanto a interação no frontend quanto os dados no backend. Foi assim que encontrei um bug discreto: a página dizia que a publicação havia sido concluída, mas o estado do artigo no banco ainda era draft, porque a lógica de atualização estava errada.

Recomendações baseadas na prática

  1. Cubra os casos-limite nas APIs. O fluxo normal costuma ser testado, mas também é preciso verificar parâmetros ausentes, tipos incorretos e permissões insuficientes.

  2. Limpe os dados de teste. Testes de API gravam no banco. Faça a limpeza em afterAll; costumo usar um banco exclusivo para testes e esvaziá-lo periodicamente:

test.afterAll(async ({ request }) => {
  await request.delete('/api/test/cleanup');
});
  1. Simule serviços externos. Se a API chama um serviço de pagamento ou SMS, use um mock durante o teste. Caso contrário, cada execução pode gerar uma cobrança real.

  2. Observe o tempo de resposta. O Playwright permite medir a duração da requisição. Eu adiciono uma asserção para garantir que o endpoint continue rápido:

const start = Date.now();
await request.get('/api/users');
const duration = Date.now() - start;

expect(duration).toBeLessThan(1000); // A API deve responder em menos de 1 segundo

Quando testes de API e de página funcionam juntos, é possível cobrir cerca de 90% dos cenários. Os 10% restantes ficam para os testes unitários.

Integração com GitHub Actions e CI/CD

Depois de escrever os testes, o próximo passo é integrá-los ao CI/CD. Executar a suíte automaticamente a cada commit evita muitas surpresas: várias vezes eu tinha certeza de que uma mudança estava correta, mas o CI revelou que outra funcionalidade havia quebrado.

O npm init playwright já gera um arquivo de configuração para GitHub Actions. A versão padrão é básica, então faço alguns ajustes conforme o projeto.

Configuração básica de CI

Veja o .github/workflows/playwright.yml inicial:

name: Playwright Tests

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

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - uses: actions/setup-node@v3
      with:
        node-version: 18

    - name: Install dependencies
      run: npm ci

    - name: Install Playwright Browsers
      run: npx playwright install --with-deps

    - name: Run Playwright tests
      run: npx playwright test

    - uses: actions/upload-artifact@v3
      if: always()
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30

Essa configuração funciona, mas tem três problemas:

  1. Instala os navegadores em todas as execuções, o que é lento.
  2. Não inclui um banco de testes, então os testes de API falham.
  3. O relatório precisa ser baixado para consulta, o que é pouco prático.

Minha configuração para produção

Esta é a configuração que uso na prática, com cache, banco de dados e publicação dos relatórios:

name: E2E Tests

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

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest

    services:
      # Banco de testes (PostgreSQL)
      postgres:
        image: postgres:15
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    env:
      DATABASE_URL: postgresql://test:test@localhost:5432/testdb
      NODE_ENV: test

    steps:
    - uses: actions/checkout@v4

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '20'
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Cache Playwright browsers
      uses: actions/cache@v3
      with:
        path: ~/.cache/ms-playwright
        key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }}

    - name: Install Playwright Browsers
      run: npx playwright install --with-deps chromium

    - name: Run database migrations
      run: npm run db:migrate

    - name: Run Playwright tests
      run: npx playwright test

    - name: Upload test results
      if: always()
      uses: actions/upload-artifact@v4
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30

    # Na branch principal, publica o relatório no GitHub Pages
    - name: Deploy report to GitHub Pages
      if: always() && github.ref == 'refs/heads/main'
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./playwright-report

Explicação dos principais pontos

  1. Contêiner de serviço (services). Uso PostgreSQL como banco de testes para que os testes de API funcionem. Se você usa MySQL, troque por mysql:8.

  2. Cache dos navegadores. O actions/cache armazena os arquivos dos navegadores do Playwright. A primeira execução é lenta; as próximas ficam mais rápidas. No CI, instalo apenas chromium, pois executar três navegadores demoraria demais.

  3. Migrações do banco. O comando npm run db:migrate cria as tabelas antes dos testes. Configure o script no package.json:

{
  "scripts": {
    "db:migrate": "prisma migrate deploy"
  }
}
  1. Publicação do relatório. Na branch principal, o relatório é implantado automaticamente no GitHub Pages. Assim, a equipe pode consultá-lo online sem fazer download.

Configuração de variáveis de ambiente

O ambiente de testes pode precisar de chaves de API ou segredos. Eu os configuro em Settings → Secrets no repositório do GitHub:

env:
  DATABASE_URL: ${{ secrets.DATABASE_URL }}
  NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
  STRIPE_SECRET_KEY: ${{ secrets.STRIPE_TEST_KEY }}

Depuração de falhas

O que fazer quando o CI falha? O Playwright oferece alguns recursos muito úteis:

  1. Consultar o trace. Na configuração, usei trace: 'on-first-retry', que gera um arquivo de trace após uma falha. Baixe-o e execute npx playwright show-trace trace.zip para reproduzir todo o teste.

  2. Consultar capturas de tela e vídeos. Em caso de falha, esses artefatos mostram diretamente o estado da página.

  3. Reproduzir o ambiente de CI localmente. A ferramenta act executa GitHub Actions no computador e acelera a depuração:

# Instalar act
brew install act  # macOS
# ou
choco install act  # Windows

# Executar o workflow
act -j test

Armadilhas que encontrei

  1. Defina um timeout razoável. No começo usei 30 minutos; um teste travou e desperdiçou muito tempo de CI. Hoje mantenho 60 minutos, mas monitoro quais testes estão demorando demais.

  2. Não exagere no paralelismo. As máquinas de CI têm recursos limitados, e workers demais podem deixar tudo mais lento. Dois workers são suficientes no meu caso.

  3. Não configure tentativas demais. retries: 2 ajuda com oscilações ocasionais de rede. Se o teste tem um problema real, repetir várias vezes só aumenta o tempo do CI.

Depois da integração com CI/CD, a qualidade do código melhorou muito. Agora todo PR precisa receber o indicador verde antes de ser mesclado, o que obriga a equipe a levar os testes a sério.

Cobertura de testes e geração de relatórios

Depois de executar a suíte, é importante interpretar o resultado. O relatório do Playwright é completo e fácil de consultar.

Relatório HTML: o mais usado

Após a execução, rode:

npx playwright show-report

Uma página local é aberta com todos os resultados. O relatório inclui:

  • estado de aprovação ou falha de cada teste;
  • duração da execução;
  • capturas de tela e vídeos dos testes que falharam;
  • arquivos de trace para reproduzir todo o processo.

Meu recurso preferido é o Trace Viewer. Ao abrir um teste que falhou, você vê cada etapa da execução, incluindo requisições de rede, snapshots do DOM e logs do console. Funciona como uma máquina do tempo e mostra com precisão onde surgiu o problema.

Cobertura de testes

Em E2E, cobertura não significa exatamente cobertura de código, mas cobertura funcional. Eu mantenho uma lista das funções já testadas:

## Lista de cobertura de testes

### Autenticação de usuários
- [x] Login: fluxo normal
- [x] Falha no login: senha incorreta
- [x] Cadastro
- [x] Recuperação de senha
- [ ] Login com terceiros: Google

### Gerenciamento de produtos
- [x] Adicionar produto
- [x] Editar produto
- [x] Excluir produto
- [ ] Importação em lote

### Fluxo de pedidos
- [x] Adicionar ao carrinho
- [x] Finalizar compra
- [x] Pagamento em ambiente simulado
- [ ] Fluxo de reembolso

Essa lista fica em tests/README.md e é atualizada sempre que uma função nova é adicionada. Assim, fica claro o que ainda não está coberto.

Cobertura de código: opcional

Se você realmente quiser medir cobertura de código, também é possível fazer isso com o Playwright. É preciso configurar Istanbul ou v8 coverage:

// playwright.config.ts
export default defineConfig({
  use: {
    // Habilita a cobertura de código
    trace: 'on',
    // Injeta o código de coleta de cobertura
    contextOptions: {
      recordVideo: {
        dir: 'test-results/videos'
      }
    }
  }
});

Sinceramente, quase nunca consulto cobertura de código em testes E2E. Os testes unitários já cobrem a lógica central; no E2E, importa mais saber se o fluxo funcional realmente termina com sucesso.

Relatórios personalizados

Às vezes, é útil enviar os resultados ao Slack ou ao DingTalk para avisar a equipe. O Playwright aceita reporters personalizados:

// my-reporter.ts
import { Reporter } from '@playwright/test/reporter';

class SlackReporter implements Reporter {
  onEnd(result) {
    const passed = result.suites.filter(s => s.ok).length;
    const failed = result.suites.length - passed;

    // Envia ao Slack
    fetch('https://hooks.slack.com/services/YOUR_WEBHOOK', {
      method: 'POST',
      body: JSON.stringify({
        text: `测试完成:${passed} 通过,${failed} 失败`
      })
    });
  }
}

export default SlackReporter;

Ative-o no arquivo de configuração:

// playwright.config.ts
export default defineConfig({
  reporter: [
    ['html'],
    ['./my-reporter.ts']
  ]
});

Acompanhar a evolução dos testes

Para acompanhar o histórico — como taxa de aprovação e tempo de execução — você pode usar o Playwright Test Runner. Envie o arquivo de trace gerado pelo CI para visualizar a qualidade dos testes.

Eu prefiro uma alternativa mais simples: após cada execução no CI, registro a taxa de aprovação e a duração em um arquivo CSV e monto o gráfico no Google Sheets.

Como costumo usar os relatórios

  1. Desenvolvimento local: consulto a saída do terminal. Em caso de falha, executo novamente em modo --debug:

    npx playwright test --debug
  2. Revisão de PR: verifico o relatório HTML do CI, com atenção especial aos testes que falharam e ao tempo de execução. Se um teste sempre expira, pode haver um problema no código.

  3. Revisão periódica: uma vez por semana, consulto a lista de cobertura e adiciono os casos que faltam.

O relatório não é apenas um conjunto de números; ele serve para encontrar problemas e melhorar o processo.

Conclusão

Quando lembro da noite, seis meses atrás, em que fiquei testando manualmente até as três da manhã, percebo como o trabalho ficou mais leve.

Na prática, a maior vantagem da combinação Playwright e Next.js é a tranquilidade. Você configura uma vez e quase não precisa mais intervir. A cada commit, o CI executa os testes; a cada lançamento, existe mais segurança. Os bugs em produção realmente diminuíram, e também caíram as ligações urgentes da gerente de produto.

Se o seu projeto Next.js ainda depende de testes manuais, recomendo:

  1. Comece pelos fluxos essenciais. Não tente cobrir tudo de uma vez; teste primeiro caminhos críticos, como login e pagamento.
  2. Use Page Object Model. Pode parecer trabalhoso no início, mas economiza tempo no longo prazo.
  3. Integre ao CI/CD. Se os testes automatizados não são executados automaticamente, boa parte do valor se perde.
  4. Não persiga 100% de cobertura. Priorize o que importa e teste bem as funções centrais.

Uma última observação: testes E2E não são apenas uma ferramenta técnica, mas também uma forma de colaboração. Eles fazem a equipe valorizar a qualidade do código, reduzem o custo de comunicação — porque os testes funcionam como documentação — e tornam os lançamentos previsíveis.

Agora consigo encerrar o trabalho às cinco e meia. Com o tempo que economizei, finalmente voltei à academia antes que o plano anual expirasse.

Comece a escrever testes. Você no futuro vai agradecer.

FAQ

Afinal, devo escolher Playwright ou Cypress?
Depende das necessidades do projeto:

• Playwright: testes entre navegadores (Chromium/Firefox/WebKit), paralelismo rápido (8 workers podem ser três vezes mais rápidos do que o Cypress), sintaxe async/await e boa adequação a projetos médios e grandes
• Cypress: foco no Chrome, excelente depuração com viagem no tempo, comunidade ampla e curva de aprendizado amigável

Se o projeto só precisa testar o Chrome e a equipe tem pouca experiência com testes, escolha Cypress. Se você precisa testar vários navegadores, quer rapidez no CI e a equipe já trabalha com React/Next.js, escolha Playwright.
É obrigatório usar Page Object Model?
Não, mas é altamente recomendável.

Se o projeto é pequeno, com menos de 10 casos de teste, ou se uma página será testada apenas uma vez, você pode escrever tudo diretamente no teste. Porém, o POM evita problemas quando:
• vários casos de teste operam a mesma página
• várias pessoas mantêm o código de testes
• o projeto continuará evoluindo por bastante tempo

O valor fica claro na primeira mudança de seletor: sem POM, você altera mais de 10 arquivos; com POM, apenas um.
O que fazer quando os testes sempre expiram no ambiente de CI?
Causas e soluções comuns:

• webServer.timeout curto demais: aumente para 120 segundos, pois a inicialização a frio do Next.js precisa compilar
• workers em excesso: máquinas de CI costumam ter recursos limitados; 2–4 são suficientes
• problema no próprio teste: investigue o arquivo de trace para saber se a demora está na rede ou na espera de um elemento
• instalação lenta dos navegadores: use actions/cache para armazenar os arquivos em cache

Outra opção é executar apenas o Chromium no CI e deixar os testes em vários navegadores para o ambiente local.
Como gerenciar os dados de teste? Preciso limpar o banco manualmente a cada execução?
Há três alternativas:

• banco de testes independente: dedicado aos testes, limpo periodicamente e isolado do ambiente de desenvolvimento
• limpeza após cada execução: chamar uma API de limpeza em test.afterAll(), embora algo possa ficar para trás
• contêiner Docker: iniciar um banco novo para cada execução e destruí-lo ao final; é a opção mais limpa, mas também a mais lenta

Eu combino as duas primeiras abordagens: no CI, uso um banco em contêiner Docker; no desenvolvimento local, um banco de testes separado com limpeza em afterAll.
É preciso simular serviços de terceiros nos testes de API?
Sim, por três motivos:

• custo: chamadas reais a APIs de pagamento ou SMS podem gerar cobrança a cada teste
• velocidade: respostas lentas de terceiros prejudicam o tempo dos testes
• estabilidade: uma indisponibilidade externa não deve derrubar a sua suíte

O Playwright permite interceptar a rede e simular respostas:
await page.route('**/api/payment', route => route.fulfill({ status: 200, body: '{"success": true}' }));

Outra opção é verificar uma variável de ambiente nas API Routes do Next.js e retornar dados simulados no ambiente de teste.
Qual cobertura de testes é suficiente?
Em testes E2E, a referência não é cobertura de código, mas cobertura funcional.

Ordem de prioridade:
• funções essenciais, como login, pagamento e pedido: cobertura de 100%
• funções frequentes, como navegar por produtos e adicionar ao carrinho: mais de 80%
• funções menos frequentes, como recuperação de senha e reembolso: mais de 50%
• funções periféricas, como troca de tema ou idioma: opcionais

Não persiga 100% de tudo. No meu projeto, os fluxos essenciais têm cobertura total e a cobertura funcional geral é de 60%, o bastante para interceptar 90% dos bugs.
Depois de escrever testes com Playwright, ainda preciso de testes unitários?
Sim. Eles se complementam:

• testes E2E com Playwright: validam fluxos funcionais, interação do usuário e integração entre frontend e backend; são lentos, mas abrangentes
• testes unitários com Jest ou Vitest: validam lógica de funções, casos-limite e tratamento de erros; são rápidos, mas locais

Uma proporção razoável é 70% de testes unitários e 30% de testes E2E. Use testes unitários para funções utilitárias, hooks e lógica de componentes; use E2E para fluxos completos do usuário.

Os testes unitários ajudam a localizar o problema rapidamente, enquanto os testes E2E confirmam que a função realmente pode ser usada.

20 min de leitura · Publicado em: 7 jan 2026 · Atualizado em: 4 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog