Um test.beforeAll que cria um usuário e compartilha entre vários testes é uma bomba-relógio. Quando o teste de deleção roda primeiro no worker paralelo, os testes de visualização e edição falham com 404. Cada teste precisa dos seus próprios dados, criados antes de rodar e deletados depois.

O problema com dados fixos no código

// Frágil — quebra quando o usuário é deletado ou a senha muda
test('usuário consegue fazer login', async ({ page }) => {
  await page.fill('[data-testid="email"]', 'admin@test.com');
  await page.fill('[data-testid="password"]', 'AdminPass1');
});

Três problemas:

  • Depende de dados específicos existindo no banco
  • Vários testes compartilhando uma conta causam conflitos em execuções paralelas
  • O teste só roda em um ambiente, a menos que você lembre de trocar o email

Padrão 1: Arquivo de constantes

O passo mais simples acima dos valores fixos no código: centralize todos os dados de teste em um arquivo.

// data/users.ts
export const TEST_USERS = {
  admin: {
    email: 'admin@test.com',
    password: 'AdminPass1',
    name: 'Test Admin',
    role: 'admin' as const,
  },
  member: {
    email: 'member@test.com',
    password: 'MemberPass1',
    name: 'Test Member',
    role: 'member' as const,
  },
  viewer: {
    email: 'viewer@test.com',
    password: 'ViewerPass1',
    name: 'Test Viewer',
    role: 'viewer' as const,
  },
} as const;

export const TEST_PRODUCTS = {
  basic: { id: 1, name: 'Basic Plan', price: 9.99 },
  pro: { id: 2, name: 'Pro Plan', price: 29.99 },
  enterprise: { id: 3, name: 'Enterprise', price: 99.99 },
} as const;
// Nos testes
import { TEST_USERS, TEST_PRODUCTS } from '../data/users';

test('admin consegue acessar o dashboard', async ({ loginPage }) => {
  await loginPage.login(TEST_USERS.admin.email, TEST_USERS.admin.password);
});

Melhor do que valores espalhados pelo código, mas ainda depende de esses usuários existirem no banco.

Padrão 2: Funções factory

Funções factory geram dados únicos por teste:

// data/factories.ts
let counter = 0;

export function generateUser(overrides: Partial<User> = {}): CreateUserRequest {
  counter++;
  return {
    email: `test_user_${Date.now()}_${counter}@example.com`,
    password: 'ValidPass1!',
    name: `Test User ${counter}`,
    role: 'member',
    ...overrides,
  };
}

export function generateProduct(overrides: Partial<Product> = {}): CreateProductRequest {
  return {
    name: `Test Product ${Date.now()}`,
    price: Math.floor(Math.random() * 100) + 10,
    category: 'electronics',
    description: 'A test product for automated testing',
    inStock: true,
    ...overrides,
  };
}
// Nos testes
import { generateUser } from '../data/factories';

test('criar um novo usuário', async ({ request }) => {
  const userData = generateUser({ role: 'admin' });
  
  const response = await request.post('/api/users', {
    data: userData,
  });
  
  expect(response.status()).toBe(201);
  const created = await response.json();
  expect(created.email).toBe(userData.email);
});

Cada teste recebe um email único. Sem colisões.

Padrão 3: Setup via API em fixtures

Cria dados frescos via API antes de cada teste e deleta depois:

// fixtures/index.ts
import { test as base } from '@playwright/test';
import { generateUser } from '../data/factories';

interface TestFixtures {
  testUser: { id: number; email: string; password: string; token: string };
  adminToken: string;
}

export const test = base.extend<TestFixtures>({
  // Token de admin — compartilhado, escopo de worker (criado uma vez por worker)
  adminToken: [async ({ request }, use) => {
    const response = await request.post('/api/auth/login', {
      data: { email: 'admin@test.com', password: 'AdminPass1' },
    });
    const { token } = await response.json();
    await use(token);
  }, { scope: 'worker' }],

  // Usuário de teste — único por teste
  testUser: async ({ request, adminToken }, use) => {
    const userData = generateUser();
    
    // CRIAÇÃO: novo usuário antes do teste
    const createResp = await request.post('/api/users', {
      data: userData,
      headers: { Authorization: `Bearer ${adminToken}` },
    });
    const user = await createResp.json();

    // Login para obter o token
    const loginResp = await request.post('/api/auth/login', {
      data: { email: userData.email, password: userData.password },
    });
    const { token } = await loginResp.json();

    // Entrega o que o teste precisa
    await use({ 
      id: user.id, 
      email: userData.email, 
      password: userData.password,
      token 
    });

    // TEARDOWN: deleta após o teste
    await request.delete(`/api/users/${user.id}`, {
      headers: { Authorization: `Bearer ${adminToken}` },
    });
  },
});
// tests/profile.spec.ts
import { test, expect } from '../fixtures';

test('usuário consegue atualizar o perfil', async ({ page, testUser }) => {
  // testUser é um usuário fresco, criado só para este teste
  await page.goto(`/login`);
  await page.fill('[data-testid="email"]', testUser.email);
  await page.fill('[data-testid="password"]', testUser.password);
  await page.click('[data-testid="submit"]');
  
  await page.click('[data-testid="edit-profile"]');
  await page.fill('[data-testid="name"]', 'Updated Name');
  await page.click('[data-testid="save"]');
  
  await expect(page.getByTestId('profile-name')).toHaveText('Updated Name');
  // Após o teste: usuário é deletado automaticamente
});

Padrão 4: Seeding do banco de dados

Para cenários de dados complexos, popule o banco diretamente:

// setup/seed.ts
async function seed() {
  const response = await fetch('http://localhost:3000/api/seed', {
    method: 'POST',
    headers: { 
      'Content-Type': 'application/json',
      'X-Seed-Secret': process.env.SEED_SECRET || 'dev-seed-secret',
    },
    body: JSON.stringify({
      users: [
        { email: 'admin@test.com', password: 'AdminPass1', role: 'admin' },
        { email: 'member@test.com', password: 'MemberPass1', role: 'member' },
      ],
      products: [
        { name: 'Basic Plan', price: 9.99, category: 'subscription' },
        { name: 'Pro Plan', price: 29.99, category: 'subscription' },
      ],
    }),
  });
  
  if (!response.ok) {
    throw new Error(`Seed failed: ${response.status}`);
  }
  
  console.log('Banco populado com sucesso');
}

seed();

Execute antes dos testes: node setup/seed.ts && npx playwright test

Padrão 5: Auth state salvo

Evite fazer login no início de cada teste. Faça login uma vez, salve o estado do browser, reutilize:

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

const authFile = path.join(__dirname, '../playwright/.auth/user.json');

setup('autenticar', async ({ page }) => {
  await page.goto('/login');
  await page.fill('[data-testid="email"]', 'member@test.com');
  await page.fill('[data-testid="password"]', 'MemberPass1');
  await page.click('[data-testid="submit"]');
  await page.waitForURL('/dashboard');
  
  // Salva o storage state (cookies, localStorage)
  await page.context().storageState({ path: authFile });
});
// playwright.config.ts
projects: [
  {
    name: 'setup',
    testMatch: /auth\.setup\.ts/,
  },
  {
    name: 'authenticated',
    use: {
      storageState: 'playwright/.auth/user.json',  // Já autenticado
    },
    dependencies: ['setup'],
  },
],

Os testes no projeto authenticated pulam o fluxo de login e começam com uma sessão já autenticada.

Dados para diferentes ambientes

Use variáveis de ambiente para apontar para os dados certos:

// data/config.ts
export const ENV_USERS = {
  local: {
    admin: { email: 'admin@local.test', password: 'LocalAdmin1' },
  },
  staging: {
    admin: { email: 'admin@staging.test', password: process.env.STAGING_ADMIN_PASS! },
  },
  production: {
    // Usuário somente-leitura para smoke tests em produção
    reader: { email: process.env.PROD_READER_EMAIL!, password: process.env.PROD_READER_PASS! },
  },
};

const env = (process.env.TEST_ENV || 'local') as keyof typeof ENV_USERS;
export const USERS = ENV_USERS[env];
TEST_ENV=staging npx playwright test --project=chromium

Isolamento de testes

Quando os testes rodam em paralelo, não podem compartilhar estado mutável.

Ruim: estado compartilhado

let userId: number;

test.beforeAll(async ({ request }) => {
  const user = await request.post('/api/users', { data: generateUser() });
  userId = (await user.json()).id;
});

// Vários testes usando o mesmo userId — race conditions!
test('visualizar usuário', async ({ page }) => { await page.goto(`/users/${userId}`); });
test('editar usuário', async ({ page }) => { /* edita o mesmo usuário */ });
test('deletar usuário', async ({ page }) => { /* deleta ele! */ });

Bom: estado isolado

// Use fixtures para cada teste receber seu próprio usuário
test('visualizar usuário', async ({ page, testUser }) => { 
  await page.goto(`/users/${testUser.id}`); 
});

test('editar usuário', async ({ page, testUser }) => { 
  // Este testUser é diferente do de cima
});

Limpeza após os testes

Sempre limpe os dados que os testes criam:

test('cria um produto', async ({ request, adminToken }) => {
  let productId: number;
  
  try {
    const response = await request.post('/api/products', {
      data: { name: 'Test Product', price: 19.99 },
      headers: { Authorization: `Bearer ${adminToken}` },
    });
    const product = await response.json();
    productId = product.id;
    
    expect(response.status()).toBe(201);
    expect(product.name).toBe('Test Product');
  } finally {
    // Sempre roda, mesmo que o teste falhe
    if (productId!) {
      await request.delete(`/api/products/${productId}`, {
        headers: { Authorization: `Bearer ${adminToken}` },
      });
    }
  }
});

Melhor ainda: coloque a limpeza no teardown da fixture (após await use(...)) para garantir que sempre rode.

Resumo

PadrãoMelhor para
Arquivo de constantesDados de referência estáveis (roles, categorias)
Funções factoryGerar dados únicos por teste
Setup/teardown via API em fixtureDados isolados e frescos por teste
Seeding do bancoEstado inicial complexo antes da suite
Auth state salvoPular o login em cada teste

A abordagem mais sólida combina os padrões: funções factory para gerar dados únicos, fixture de API para criar e deletar por teste, auth state salvo para a sessão autenticada base. Cada teste fica completamente isolado, sem estado compartilhado, sem race conditions, sem dependência de ordem de execução.

→ Veja também: Dados de Teste Reutilizáveis: Factories, Fixtures e Faker.js no Playwright | Isolamento de Testes: Por que Cada Teste Playwright Deve Ser sem Estado | Autenticação no Playwright com storageState (Sem Login em Cada Teste) | Testes de API com o APIRequestContext do Playwright (Sem Postman)