Suites de teste que funcionam bem com 50 testes frequentemente colapsam com 200. Testes interdependentes não conseguem rodar em paralelo. Waits hardcoded tornam a suite lenta e flaky. E locators repetidos em vários arquivos fazem com que uma mudança de UI quebre dezenas de testes. As práticas que previnem isso: nomes descritivos, isolamento estrito de estado, esperas baseadas em elementos em vez de waitForTimeout, e Page Object Model no momento certo.
Nomeie testes como frases, não como código
O nome do teste é a primeira coisa que você lê quando algo falha às 2h da manhã no CI. Ele precisa dizer exatamente o que quebrou sem precisar abrir o arquivo.
Ruim:
test('login test', async ({ page }) => { ... });
test('test1', async ({ page }) => { ... });
test('checkTable', async ({ page }) => { ... });Bom:
test('user can log in with valid credentials', async ({ page }) => { ... });
test('login fails with incorrect password', async ({ page }) => { ... });
test('travel items table shows 5 rows after login', async ({ page }) => { ... });O padrão é: [quem] pode/não pode [fazer o quê] [sob qual condição]. Escreva de forma que uma pessoa não técnica lendo o output do CI entenda o que falhou.
Blocos describe funcionam da mesma forma:
test.describe('Login', () => {
test('succeeds with valid credentials', async ({ page }) => { ... });
test('fails with wrong password', async ({ page }) => { ... });
test('fails with empty email', async ({ page }) => { ... });
});Uma assertion por teste: o ideal, não a regra
Você vai ver a recomendação de "uma assertion por teste." A regra real é: um conceito lógico por teste.
Um teste que faz login e verifica o título da página está certo (fazem parte do mesmo fluxo). Um teste que faz login, verifica o título, edita um registro, verifica se o registro atualizou e depois faz logout está fazendo coisas demais. Quando falhar, você não vai saber qual parte quebrou.
Certo. Um conceito, múltiplas assertions relacionadas:
test('login redirects to dashboard with correct header', async ({ page }) => {
await page.goto('https://lab.becomeqa.com');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page).toHaveURL(/dashboard/);
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
await expect(page.getByText('My Travel Items')).toBeVisible();
});Mantenha os testes independentes entre si
Testes que dependem uns dos outros são uma armadilha. Se o teste 3 só funciona depois que o teste 2 rodou, você não consegue rodar em paralelo nem executar um teste isoladamente. Quando o teste 2 quebra, você tem uma cascata de falhas difícil de diagnosticar.
Cada teste precisa configurar seu próprio estado e limpar depois de si mesmo.
Cada teste que precisa de um usuário logado deve fazer o login por conta própria. Ou usar storageState para salvar o cookie de auth e reutilizá-lo sem repetir o fluxo de UI.
// Ruim: depende de teste anterior ter feito login
test('can see travel items', async ({ page }) => {
// assume que já estamos logados — quebra se rodado sozinho
await expect(page.getByText('My Travel Items')).toBeVisible();
});
// Bom: configura seu próprio estado
test('can see travel items', async ({ page }) => {
await page.goto('https://lab.becomeqa.com');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('My Travel Items')).toBeVisible();
});test.only em código commitado. Ele desativa silenciosamente todos os outros testes do arquivo. Se um test.only for para o merge, seu CI passa com 1 teste em vez de 50 e ninguém percebe até algo quebrar em produção.Use Page Object Model quando os arquivos ficarem grandes
Quando o arquivo de teste chega em 200+ linhas e cada teste repete as mesmas chamadas de getByLabel('Username').fill(...), é hora do Page Object Model (POM).
O POM move as interações com a página para uma classe separada. Os testes chamam métodos nessa classe em vez de comandos Playwright diretos. Quando o formulário de login muda, você atualiza uma classe em vez de cada teste que toca o login.
// pages/LoginPage.ts
import { Page } from '@playwright/test';
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('https://lab.becomeqa.com');
await this.page.getByRole('button', { name: 'Login' }).click();
}
async login(username: string, password: string) {
await this.page.getByLabel('Username').fill(username);
await this.page.getByLabel('Password').fill(password);
await this.page.getByRole('button', { name: 'Submit' }).click();
}
}// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('user can log in', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('admin@becomeqa.com', 'testpass123');
await expect(page.getByText('My Travel Items')).toBeVisible();
});Quando a UI de login muda, você corrige LoginPage.ts e todos os testes continuam verdes.
Não corra para o POM. Escreva os testes sem ele primeiro. Quando se pegar copiando e colando as mesmas 5 linhas pela terceira vez, esse é o sinal.
Evite waits hardcoded
page.waitForTimeout(3000) é um code smell. Você está dizendo ao Playwright para esperar 3 segundos independentemente do que está na tela. O teste fica lento em máquinas rápidas e ainda flaky nos runners de CI mais lentos.
O Playwright faz auto-wait em elementos antes de interagir com eles. Quando você realmente precisa esperar algo específico, espere por aquela coisa específica:
// Ruim
await page.waitForTimeout(3000);
await page.getByRole('button', { name: 'Save' }).click();
// Bom — Playwright espera automaticamente antes de clicar
await page.getByRole('button', { name: 'Save' }).click();
// Bom — espera um elemento específico aparecer
await page.waitForSelector('[data-testid="success-toast"]');
// Bom — espera uma requisição de rede completar
await page.waitForResponse(resp => resp.url().includes('/api/items'));O único momento em que waitForTimeout é aceitável é no debug local para desacelerar e ver o que está acontecendo. Ele nunca deveria existir em código de teste commitado.
Use variáveis de ambiente para credenciais e URLs
Hardcodar credenciais e base URLs nos testes cria dois problemas: eles vazam para o histórico do git, e alterá-los significa pesquisar em cada arquivo de teste.
Guarde-os em um arquivo .env e carregue via configuração do Playwright:
// .env (nunca commite este arquivo)
BASE_URL=https://lab.becomeqa.com
TEST_USER=admin@becomeqa.com
TEST_PASS=testpass123// playwright.config.ts
export default defineConfig({
use: {
baseURL: process.env.BASE_URL || 'https://lab.becomeqa.com',
},
});// tests/login.spec.ts
test('user can log in', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill(process.env.TEST_USER!);
await page.getByLabel('Password').fill(process.env.TEST_PASS!);
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('My Travel Items')).toBeVisible();
});Adicione .env ao .gitignore. No CI, defina as variáveis de ambiente na configuração do pipeline.
Estruture a pasta de testes antes que ela cresça
Uma estrutura de pasta que funciona para 10 testes desmorona com 100. Configure cedo:
tests/
auth/
login.spec.ts
logout.spec.ts
items/
items-list.spec.ts
items-crud.spec.ts
api/
items-api.spec.ts
pages/
LoginPage.ts
ItemsPage.ts
fixtures/
auth.fixture.tsAgrupe por feature, não por tipo. tests/auth/ é melhor do que tests/ui/ porque quando algo em auth quebra, você sabe exatamente onde procurar.
npx playwright test tests/auth/ para rodar apenas os testes de uma pasta. Útil quando você está trabalhando em uma feature específica e não quer esperar a suite inteira.Escreva testes que documentam a intenção
Um teste é documentação: nomes de variáveis devem descrever o que contêm, dados de teste devem parecer realistas, e comentários devem aparecer apenas para setup não óbvio.
// Difícil de entender
const u = 'admin@becomeqa.com';
const p = 'testpass123';
await page.getByLabel('Username').fill(u);
// Claro
const adminEmail = 'admin@becomeqa.com';
const adminPassword = 'testpass123';
await page.getByLabel('Username').fill(adminEmail);Diferença pequena no código, diferença grande na legibilidade seis meses depois.
Rode a suite completa antes de fazer merge
Testes que só rodam localmente são sugestões, não testes. Conecte seus testes Playwright ao CI para que rodem em cada pull request automaticamente.
No mínimo, seu CI deve instalar dependências, instalar navegadores, rodar a suite e fazer upload do HTML report se os testes falharem:
# .github/workflows/tests.yml (simplificado)
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright testSe os testes passam no CI, você faz merge. Se falham, corrige antes de fazer merge. Esse é o contrato.
FAQ
Quantos testes são muitos para um arquivo?Por volta de 300 a 400 linhas, quando rolar para encontrar um teste começa a incomodar. Divida por feature nesse ponto.
Devo testar todos os edge cases?Não. Teste o caminho feliz, o caminho de erro mais comum, e quaisquer edge cases que causaram bugs reais. O objetivo é confiança, não 100% de cobertura pelo próprio bem da cobertura.
Meus testes passam localmente mas falham no CI. O que geralmente está errado?Três causas mais comuns: uma URL localhost hardcoded, um await faltando, ou uma race condition escondida pelo fato de sua máquina ser mais rápida que o runner de CI. Verifique o output do trace viewer do CI: ele mostra exatamente onde quebrou.
beforeEach vs uma fixture?
beforeEach para setup simples específico de um arquivo de teste. Fixtures para setup reutilizado em múltiplos arquivos (como uma página logada ou dados de teste pré-populados).
→ Veja também: Page Object Model no Playwright: Do Caos à Manutenibilidade | Fixtures do Playwright Explicadas: Das Integradas às Personalizadas | Depurando Testes Instáveis: Um Guia Prático | Isolamento de Testes: Por que Cada Teste Playwright Deve Ser sem Estado