test.beforeEach roda o setup antes de cada teste no seu escopo. Código de login escrito uma vez dentro de um bloco describe serve a todos os testes dentro dele sem afetar os de fora. test.beforeAll roda uma vez para o grupo inteiro, não uma vez por teste. Um token de auth que ele cria fica compartilhado entre todos os testes do arquivo, o que causa interferência quando esses testes rodam em paralelo.

O teste básico

Antes de adicionar estrutura, um teste simples tem essa cara:

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

test('usuário consegue fazer login', async ({ page }) => {
  await page.goto('/login');
  await page.fill('[data-testid="email"]', 'user@test.com');
  await page.fill('[data-testid="password"]', 'ValidPass1');
  await page.click('[data-testid="submit"]');
  await expect(page).toHaveURL('/dashboard');
});

Simples. Mas se você tem 10 testes que começam com os mesmos passos de login, são 10 blocos duplicados. Quando o fluxo de login muda, você atualiza 10 lugares em vez de um.

test.beforeEach — rodar antes de cada teste

beforeEach executa código de setup antes de cada teste no seu escopo. O uso mais comum: navegar para uma página ou fazer login.

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

test.beforeEach(async ({ page }) => {
  await page.goto('/login');
  await page.fill('[data-testid="email"]', 'user@test.com');
  await page.fill('[data-testid="password"]', 'ValidPass1');
  await page.click('[data-testid="submit"]');
  await page.waitForURL('/dashboard');
});

test('dashboard mostra o nome do usuário', async ({ page }) => {
  // Já autenticado e no /dashboard
  await expect(page.getByTestId('user-name')).toContainText('Test User');
});

test('usuário consegue acessar as configurações', async ({ page }) => {
  // Também já autenticado
  await page.getByTestId('settings-link').click();
  await expect(page).toHaveURL('/settings');
});

Cada teste começa com o usuário já autenticado e no dashboard. Sem duplicação.

test.afterEach — limpar após cada teste

afterEach roda após cada teste, independente de ter passado ou falhado. Use para limpeza que precisa acontecer depois de cada teste.

test.afterEach(async ({ page }, testInfo) => {
  // Captura screenshot na falha (o Playwright também faz isso via config)
  if (testInfo.status !== testInfo.expectedStatus) {
    await page.screenshot({ path: `screenshots/${testInfo.title}.png` });
  }
});

Ou para limpeza via API:

let createdUserId: number;

test.beforeEach(async ({ request }) => {
  const response = await request.post('/api/users', {
    data: { email: 'temp@test.com', password: 'Pass1' },
  });
  const body = await response.json();
  createdUserId = body.id;
});

test.afterEach(async ({ request }) => {
  // Deleta o usuário criado no setup
  await request.delete(`/api/users/${createdUserId}`);
});

test.describe — agrupar testes relacionados

test.describe cria um grupo nomeado de testes. Útil para:
  • Agrupar testes por feature ou página
  • Aplicar beforeEach/afterEach apenas a um subconjunto de testes
  • Aninhar cenários de teste relacionados

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

test.describe('Página de login', () => {
  test('mostra os campos de email e senha', async ({ page }) => {
    await page.goto('/login');
    await expect(page.getByTestId('email')).toBeVisible();
    await expect(page.getByTestId('password')).toBeVisible();
  });

  test('mostra erro com senha errada', async ({ page }) => {
    await page.goto('/login');
    await page.fill('[data-testid="email"]', 'user@test.com');
    await page.fill('[data-testid="password"]', 'SenhaErrada');
    await page.click('[data-testid="submit"]');
    await expect(page.getByTestId('error-message')).toBeVisible();
  });
});

test.describe('Dashboard', () => {
  test.beforeEach(async ({ page }) => {
    // Login antes de cada teste de dashboard
    await page.goto('/login');
    await page.fill('[data-testid="email"]', 'user@test.com');
    await page.fill('[data-testid="password"]', 'ValidPass1');
    await page.click('[data-testid="submit"]');
    await page.waitForURL('/dashboard');
  });

  test('mostra o nome do usuário', async ({ page }) => {
    await expect(page.getByTestId('user-name')).toBeVisible();
  });

  test('mostra pedidos recentes', async ({ page }) => {
    await expect(page.getByTestId('orders-section')).toBeVisible();
  });
});

O beforeEach de login só se aplica ao bloco describe do Dashboard. Os testes de login não são afetados.

test.beforeAll e test.afterAll

beforeAll roda uma vez antes de todos os testes no seu escopo (não antes de cada um). afterAll roda uma vez depois de todos.

Caso de uso: setup caro que só precisa acontecer uma vez, como criar um usuário de teste via API, popular um banco de dados ou iniciar um servidor.

import { test, expect, request as playwrightRequest } from '@playwright/test';

let authToken: string;

test.beforeAll(async () => {
  // Cria o token de auth uma vez para todos os testes do arquivo
  const ctx = await playwrightRequest.newContext();
  const response = await ctx.post('/api/auth/login', {
    data: { email: 'admin@test.com', password: 'AdminPass1' },
  });
  const body = await response.json();
  authToken = body.token;
  await ctx.dispose();
});

test('admin consegue ver todos os usuários', async ({ request }) => {
  const response = await request.get('/api/admin/users', {
    headers: { Authorization: `Bearer ${authToken}` },
  });
  expect(response.status()).toBe(200);
});

test('admin consegue deletar um usuário', async ({ request }) => {
  // Também usa authToken — compartilhado entre todos os testes
});

beforeAll e afterAll rodam em um contexto compartilhado. Mudanças no estado compartilhado persistem entre testes, o que pode causar testes flaky. Para a maioria dos setups, prefira beforeEach.

Blocos describe aninhados

Você pode aninhar blocos describe para criar uma hierarquia:

test.describe('Fluxo de checkout', () => {
  test.beforeEach(async ({ page }) => {
    await loginAsUser(page);
    await addItemToCart(page, 'product-123');
  });

  test.describe('com cartão válido', () => {
    test.beforeEach(async ({ page }) => {
      await fillShippingAddress(page);
      await fillValidCard(page, '4242 4242 4242 4242');
    });

    test('conclui a compra', async ({ page }) => {
      await page.getByTestId('place-order').click();
      await expect(page.getByTestId('order-confirmation')).toBeVisible();
    });

    test('envia email de confirmação', async ({ page }) => {
      // ...
    });
  });

  test.describe('com cartão inválido', () => {
    test('mostra mensagem de erro', async ({ page }) => {
      await fillValidCard(page, '0000 0000 0000 0000');
      await page.getByTestId('place-order').click();
      await expect(page.getByTestId('payment-error')).toBeVisible();
    });
  });
});

Ordem de execução dos hooks para um teste aninhado:

1. beforeEach externo (login + adicionar ao carrinho)

2. beforeEach interno (preencher endereço + cartão)

3. O próprio teste

4. afterEach interno (se houver)

5. afterEach externo (se houver)

test.skip e test.only

Dois modificadores úteis durante o desenvolvimento:

// Pula este teste (marca como pulado, não falhado)
test.skip('feature ainda não implementada', async ({ page }) => {
  // ...
});

// Roda apenas este teste (ignora todos os outros no arquivo)
test.only('depurando este caso específico', async ({ page }) => {
  // ...
});

Nunca faça commit de test.only na branch principal. Ele faz toda a suite de CI falhar ao rodar apenas um teste.

Skip condicional — útil para testes específicos de ambiente:

test('painel de admin', async ({ page }) => {
  test.skip(process.env.ENV === 'production', 'Pulado em produção');
  // ...
});

Como os testes são nomeados nos relatórios

O nome do teste exibido nos relatórios combina o label do describe com o label do test:

test.describe('Página de login', () => {
  test('mostra erro com senha errada', async ({ page }) => { ... });
});
// Relatório mostra: "Página de login > mostra erro com senha errada"

Escreva nomes descritivos. Quando um CI mostrar 3 falhas, você vai precisar entendê-las sem abrir o código.

Exemplo de estrutura completa de arquivo

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

test.describe('Autenticação de usuário', () => {
  let loginPage: LoginPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    await loginPage.navigate();
  });

  test.describe('Credenciais válidas', () => {
    test('redireciona para o dashboard', async ({ page }) => {
      await loginPage.login('user@test.com', 'ValidPass1');
      await expect(page).toHaveURL('/dashboard');
    });

    test('define o cookie de auth', async ({ page }) => {
      await loginPage.login('user@test.com', 'ValidPass1');
      const cookies = await page.context().cookies();
      expect(cookies.some(c => c.name === 'auth_token')).toBe(true);
    });
  });

  test.describe('Credenciais inválidas', () => {
    test('senha errada mostra erro', async () => {
      await loginPage.login('user@test.com', 'SenhaErrada');
      await expect(loginPage.errorMessage).toBeVisible();
    });

    test('email vazio mostra erro de validação', async () => {
      await loginPage.login('', 'ValidPass1');
      await expect(loginPage.emailError).toContainText('required');
    });
  });
});

Resumo

| Hook | Quando roda | Use para |

|------|-------------|----------|

| test.beforeEach | Antes de cada teste no escopo | Navegação, login, reset de estado |

| test.afterEach | Após cada teste no escopo | Limpeza, screenshots em falhas |

| test.beforeAll | Uma vez antes de todos os testes no escopo | Setup caro que pode ser compartilhado |

| test.afterAll | Uma vez após todos os testes no escopo | Teardown único |

| test.describe | (agrupamento, não é hook) | Organizar testes, escopar hooks |

Comece com test.beforeEach para a maioria dos setups. Adicione test.describe para agrupar testes relacionados. Use test.beforeAll apenas quando o setup for genuinamente caro e seguro de compartilhar entre testes.

→ Veja também: Fixtures do Playwright Explicadas: Das Integradas às Personalizadas | Isolamento de Testes: Por que Cada Teste Playwright Deve Ser sem Estado | Boas Práticas de Automação de Testes que Realmente Importam