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

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
-
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. -
No desenvolvimento local, use
reuseExistingServer: true. Caso contrário, o Next.js reinicia a cada execução, e a espera incomoda. -
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.
-
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
-
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.
-
Dê nomes semânticos aos métodos.
async fillLoginForm()comunica melhor do queasync fillForm(). Daqui a seis meses, você agradecerá. -
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. -
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
-
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.
-
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');
});
-
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.
-
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:
- Instala os navegadores em todas as execuções, o que é lento.
- Não inclui um banco de testes, então os testes de API falham.
- 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
-
Contêiner de serviço (
services). Uso PostgreSQL como banco de testes para que os testes de API funcionem. Se você usa MySQL, troque pormysql:8. -
Cache dos navegadores. O
actions/cachearmazena os arquivos dos navegadores do Playwright. A primeira execução é lenta; as próximas ficam mais rápidas. No CI, instalo apenaschromium, pois executar três navegadores demoraria demais. -
Migrações do banco. O comando
npm run db:migratecria as tabelas antes dos testes. Configure o script nopackage.json:
{
"scripts": {
"db:migrate": "prisma migrate deploy"
}
}
- 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:
-
Consultar o trace. Na configuração, usei
trace: 'on-first-retry', que gera um arquivo de trace após uma falha. Baixe-o e executenpx playwright show-trace trace.zippara reproduzir todo o teste. -
Consultar capturas de tela e vídeos. Em caso de falha, esses artefatos mostram diretamente o estado da página.
-
Reproduzir o ambiente de CI localmente. A ferramenta
actexecuta 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
-
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.
-
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.
-
Não configure tentativas demais.
retries: 2ajuda 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
-
Desenvolvimento local: consulto a saída do terminal. Em caso de falha, executo novamente em modo
--debug:npx playwright test --debug -
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.
-
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:
- Comece pelos fluxos essenciais. Não tente cobrir tudo de uma vez; teste primeiro caminhos críticos, como login e pagamento.
- Use Page Object Model. Pode parecer trabalhoso no início, mas economiza tempo no longo prazo.
- Integre ao CI/CD. Se os testes automatizados não são executados automaticamente, boa parte do valor se perde.
- 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?
• 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?
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?
• 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?
• 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?
• 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?
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?
• 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
Guia completo Next.js
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Testes unitários no Next.js: guia completo com Jest e React Testing Library
Configure do zero um ambiente de testes para Next.js 15 com Jest e React Testing Library, incluindo testes de Client e Server Components, Hooks, técnicas de Mock e solução de problemas, com exemplos completos de código.
Parte 20 de 26
Próximo
E-commerce com Next.js: carrinho e pagamentos com Stripe do início ao fim
Implemente um carrinho com Zustand e pagamentos com Stripe no Next.js, incluindo Checkout Session, Webhook, criação de pedidos, controle de estoque e implantação.
Parte 22 de 26



Comentários
Entre com GitHub para comentar