Um teste que usa alice@example.com hardcoded cria um conflito de unicidade na segunda execução se a limpeza do teste anterior falhou. O Faker.js resolve isso com dados gerados, mas chamar faker.internet.email() duas vezes (uma para preencher o formulário, outra para fazer assertion de que aparece) produz dois valores diferentes. Gere uma vez, armazene em uma variável, use em todo lugar.

O problema com dados hardcoded

Dados hardcoded quebram de três formas, e cada uma agrava as outras.

A primeira é conflito de unicidade. A maioria das aplicações reais impõe emails únicos, usernames únicos, números de pedido únicos. Se seu teste usa alice@example.com e o teste rodou ontem e deixou essa linha no banco, a execução de hoje falha no momento da criação. Não porque a feature está quebrada, mas porque a limpeza nunca aconteceu.

A segunda é estado compartilhado. Cinco testes que operam todos em item-id-42 são cinco testes esperando para interferir entre si. Em execuções paralelas, as colisões acontecem constantemente. Em execuções sequenciais, acontecem com frequência suficiente para parecer aleatório.

A terceira é fragilidade dos próprios dados. Uma data hardcoded de 2024-01-15 que era "no futuro" quando você escreveu o teste está agora dois anos no passado. Um status "pending" que fazia sentido para um fluxo foi renomeado para "awaiting_approval". Todo valor hardcoded é um fardo de manutenção futuro.

// O que você quer parar de fazer
test('usuário pode atualizar o perfil', async ({ page }) => {
  // Esses vão eventualmente conflitar ou ficar desatualizados
  await loginAs(page, 'alice@example.com', 'password123');
  await page.goto('/profile');
  await page.getByLabel('Display name').fill('Alice Smith');
  await page.getByRole('button', { name: 'Save' }).click();
  await expect(page.getByText('Profile updated')).toBeVisible();
});

A solução não é ser mais cuidadoso com valores hardcoded. É parar de usá-los.

Faker.js: instalação e o básico

O Faker.js é uma biblioteca para gerar dados falsos realistas. Nomes, emails, endereços, UUIDs, datas, telefones, nomes de produtos: todos aleatórios, todos plausíveis.

npm install --save-dev @faker-js/faker

A API agrupa geradores por categoria. Estes são os que você vai usar mais em suites de testes:

import { faker } from '@faker-js/faker';

// Identidade
faker.person.firstName()           // 'Marcus'
faker.person.lastName()            // 'Holloway'
faker.internet.email()             // 'marcus.holloway@gmail.com'
faker.internet.username()          // 'marcus_holloway42'
faker.internet.password({ length: 12 }) // 'Kd9$mXp2vLqR'

// IDs e referências
faker.string.uuid()                // 'e2d4f6a8-...'
faker.number.int({ min: 1, max: 9999 }) // 7342

// Datas
faker.date.future()                // objeto Date no futuro
faker.date.past({ years: 2 })      // objeto Date nos últimos 2 anos
faker.date.between({ from: '2025-01-01', to: '2025-12-31' }) // Date no intervalo

// Conteúdo
faker.lorem.sentence()             // 'Voluptas et dolorem rerum.'
faker.commerce.productName()       // 'Sleek Rubber Shoes'
faker.commerce.price()             // '42.99'

Inicialize o Faker com faker.seed(12345) no topo de um arquivo de teste para tornar a geração determinística. O mesmo seed produz a mesma sequência de valores toda vez. Útil para debugar um teste flaky que depende de valores gerados específicos: rode com um seed, capture os dados, reproduza a falha de forma confiável.

Um hábito importante: gere o valor uma vez e armazene em uma variável. Não chame faker.internet.email() em dois lugares esperando o mesmo resultado.

// Errado — dois emails diferentes
await page.getByLabel('Email').fill(faker.internet.email());
await expect(page.getByText(faker.internet.email())).toBeVisible(); // valor diferente!

// Correto — gera uma vez, usa em todo lugar
const email = faker.internet.email();
await page.getByLabel('Email').fill(email);
await expect(page.getByText(email)).toBeVisible();

Funções factory: buildUser() e buildOrder()

Uma função factory é uma função TypeScript comum que retorna um objeto de dados completo. Usa o Faker para defaults mas aceita overrides para que os testes especifiquem apenas o que realmente importa para eles.

// factories/user.factory.ts
import { faker } from '@faker-js/faker';

export interface User {
  firstName: string;
  lastName: string;
  email: string;
  password: string;
  role: 'admin' | 'editor' | 'viewer';
  createdAt: Date;
}

export function buildUser(overrides: Partial<User> = {}): User {
  return {
    firstName: faker.person.firstName(),
    lastName: faker.person.lastName(),
    email: faker.internet.email(),
    password: faker.internet.password({ length: 12 }),
    role: 'viewer',
    createdAt: faker.date.past({ years: 1 }),
    ...overrides,
  };
}

// factories/order.factory.ts
import { faker } from '@faker-js/faker';

export interface Order {
  id: string;
  customerId: string;
  items: { productId: string; quantity: number; price: number }[];
  status: 'pending' | 'processing' | 'shipped' | 'delivered';
  total: number;
  placedAt: Date;
}

export function buildOrder(overrides: Partial<Order> = {}): Order {
  const items = Array.from({ length: faker.number.int({ min: 1, max: 4 }) }, () => ({
    productId: faker.string.uuid(),
    quantity: faker.number.int({ min: 1, max: 5 }),
    price: parseFloat(faker.commerce.price({ min: 5, max: 200 })),
  }));

  const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);

  return {
    id: faker.string.uuid(),
    customerId: faker.string.uuid(),
    items,
    status: 'pending',
    total: parseFloat(total.toFixed(2)),
    placedAt: new Date(),
    ...overrides,
  };
}

Nos testes, você especifica apenas o que o teste se importa. Todo o resto preenche automaticamente:

// Um teste que só se importa com o role do usuário
const adminUser = buildUser({ role: 'admin' });

// Um teste que só se importa com o status do pedido
const shippedOrder = buildOrder({ status: 'shipped' });

// Um teste que precisa de um email específico (para login)
const user = buildUser({ email: 'known-test-user@example.com' });

Isso mantém os testes expressivos. Quando você vê buildUser({ role: 'admin' }), você sabe imediatamente que o role é o que importa para esse teste. Os outros 10 campos são ruído irrelevante que o Faker tratou por você.

Padrão builder para objetos de teste complexos

Para objetos com muitos campos interdependentes, onde definir uma propriedade provavelmente deveria mudar outra, use uma classe builder com API fluente. É mais legível que uma função factory com um grande objeto de overrides.

// builders/UserBuilder.ts
import { faker } from '@faker-js/faker';
import { User } from '../factories/user.factory';

export class UserBuilder {
  private user: User;

  constructor() {
    this.user = {
      firstName: faker.person.firstName(),
      lastName: faker.person.lastName(),
      email: faker.internet.email(),
      password: faker.internet.password({ length: 12 }),
      role: 'viewer',
      createdAt: new Date(),
    };
  }

  withRole(role: User['role']): UserBuilder {
    this.user.role = role;
    return this;
  }

  withEmail(email: string): UserBuilder {
    this.user.email = email;
    return this;
  }

  withName(firstName: string, lastName: string): UserBuilder {
    this.user.firstName = firstName;
    this.user.lastName = lastName;
    return this;
  }

  asAdmin(): UserBuilder {
    this.user.role = 'admin';
    this.user.email = `admin-${faker.string.uuid().slice(0, 8)}@company.com`;
    return this;
  }

  createdDaysAgo(days: number): UserBuilder {
    const date = new Date();
    date.setDate(date.getDate() - days);
    this.user.createdAt = date;
    return this;
  }

  build(): User {
    return { ...this.user };
  }
}

O ponto de chamada lê quase como inglês puro:

const admin = new UserBuilder().asAdmin().build();

const recentUser = new UserBuilder()
  .withRole('editor')
  .createdDaysAgo(3)
  .build();

const namedUser = new UserBuilder()
  .withName('Jordan', 'Reeves')
  .withRole('viewer')
  .build();

O builder brilha quando asAdmin() precisa definir múltiplos campos juntos (role, domínio de email, e talvez uma flag isVerified). Você não quer espalhar essa lógica por cada teste que cria um usuário admin.

Use funções factory para objetos simples e classes builder para qualquer coisa onde combinações predefinidas de campos sejam comuns. Eles também se compõem bem: uma função factory pode usar internamente um builder se o objeto for complexo.

Seed de dados via API

Criar dados de teste pela UI é lento e frágil. Um fluxo de cadastro que leva 8 segundos no navegador leva 80 milissegundos pela API. Mais importante, o setup via UI acopla seu teste a duas features de uma vez. Se o formulário de cadastro quebrar, cada teste que o usa como setup também quebra, mesmo que esses testes sejam sobre algo completamente diferente.

O fixture request do Playwright dá um contexto de API que você pode usar diretamente no código de setup.

// helpers/api.helpers.ts
import { APIRequestContext } from '@playwright/test';
import { buildUser, User } from '../factories/user.factory';

const BASE_URL = process.env.API_BASE_URL ?? 'https://lab.becomeqa.com/api';

export async function createUserViaApi(
  request: APIRequestContext,
  overrides: Partial<User> = {}
): Promise<User & { id: string }> {
  const userData = buildUser(overrides);

  const response = await request.post(`${BASE_URL}/users`, {
    data: userData,
    headers: {
      Authorization: `Bearer ${process.env.API_SEED_TOKEN}`,
      'Content-Type': 'application/json',
    },
  });

  if (!response.ok()) {
    throw new Error(`Failed to create user: ${response.status()} ${await response.text()}`);
  }

  const created = await response.json();
  return { ...userData, id: created.id };
}

export async function deleteUserViaApi(
  request: APIRequestContext,
  userId: string
): Promise<void> {
  await request.delete(`${BASE_URL}/users/${userId}`, {
    headers: { Authorization: `Bearer ${process.env.API_SEED_TOKEN}` },
  });
}

Testes que precisam de um usuário só chamam o helper e recebem um registro real do banco instantaneamente:

test('admin pode desativar uma conta de usuário', async ({ page, request }) => {
  const user = await createUserViaApi(request, { role: 'editor' });

  await page.goto('/admin/users');
  await page.getByTestId(`user-row-${user.id}`).getByRole('button', { name: 'Deactivate' }).click();
  await page.getByRole('button', { name: 'Confirm' }).click();

  await expect(page.getByTestId(`user-row-${user.id}`).getByText('Inactive')).toBeVisible();

  await deleteUserViaApi(request, user.id); // limpeza explícita
});

Esse teste verifica a UI de desativação do admin sem depender da UI de cadastro ou de login. Outros caminhos que poderiam falhar por razões não relacionadas ficam fora da equação.

Nunca use credenciais de produção ou um token de API de produção em testes, nem mesmo para uma "verificação rápida". Use um ambiente de teste dedicado com suas próprias credenciais. Tokens de seed pertencem a variáveis de ambiente, não a código commitado.

Fixture de dados com limpeza automática

O teste acima trata a limpeza manualmente. Funciona, mas se o teste lançar uma exceção antes de chegar ao deleteUserViaApi, a limpeza nunca roda e o usuário fica no banco. Uma fixture do Playwright resolve isso tornando a limpeza incondicional.

// fixtures/data.fixture.ts
import { test as base, APIRequestContext } from '@playwright/test';
import { buildUser, User } from '../factories/user.factory';
import { createUserViaApi, deleteUserViaApi } from '../helpers/api.helpers';

type DataFixtures = {
  testUser: User & { id: string };
  testAdminUser: User & { id: string };
};

export const test = base.extend<DataFixtures>({
  testUser: async ({ request }, use) => {
    const user = await createUserViaApi(request);

    await use(user);

    // Roda após o teste — passe ou falhe
    try {
      await deleteUserViaApi(request, user.id);
    } catch (error) {
      console.warn(`Cleanup failed for user ${user.id}:`, error);
    }
  },

  testAdminUser: async ({ request }, use) => {
    const user = await createUserViaApi(request, { role: 'admin' });

    await use(user);

    try {
      await deleteUserViaApi(request, user.id);
    } catch (error) {
      console.warn(`Cleanup failed for admin user ${user.id}:`, error);
    }
  },
});

export { expect } from '@playwright/test';

Os testes agora recebem um usuário completamente criado e nunca tocam na limpeza:

// tests/profile.spec.ts
import { test, expect } from '../fixtures/data.fixture';

test('usuário pode atualizar o nome de exibição', async ({ page, testUser }) => {
  await loginAs(page, testUser.email, testUser.password);
  await page.goto('/profile');

  await page.getByLabel('Display name').fill('New Display Name');
  await page.getByRole('button', { name: 'Save' }).click();

  await expect(page.getByText('Profile updated')).toBeVisible();
  // testUser é deletado após essa linha — automaticamente
});

test('admin pode ver detalhes do usuário', async ({ page, testAdminUser, testUser }) => {
  await loginAs(page, testAdminUser.email, testAdminUser.password);
  await page.goto(`/admin/users/${testUser.id}`);

  await expect(page.getByText(testUser.email)).toBeVisible();
  // Ambos os usuários são deletados após esse teste
});

O try/catch no bloco de teardown é intencional. Se a limpeza lançar e você não capturar, o Playwright pode exibir uma falha secundária confusa que obscurece o que realmente deu errado no teste. Logue o aviso, mas não relance.

Dados com escopo de worker para dados de referência somente-leitura

Alguns dados não mudam entre testes: um catálogo de categorias de produtos, uma lista de países, um conjunto de definições de permissões. Criar e deletar esses dados para cada teste é dispendioso quando os dados nunca são modificados.

Fixtures com escopo de worker criam os dados uma vez por processo worker e os compartilham entre todos os testes daquele worker. O tipo de fixture vai para o segundo parâmetro genérico de extend().

// fixtures/worker-data.fixture.ts
import { test as base } from '@playwright/test';

interface Category {
  id: string;
  name: string;
  slug: string;
}

type WorkerFixtures = {
  productCategories: Category[];
};

export const test = base.extend<{}, WorkerFixtures>({
  productCategories: [
    async ({ request }, use) => {
      // Seed das categorias uma vez por worker
      const created: Category[] = [];

      for (const name of ['Electronics', 'Books', 'Clothing']) {
        const response = await request.post('/api/categories', {
          data: { name, slug: name.toLowerCase() },
          headers: { Authorization: `Bearer ${process.env.API_SEED_TOKEN}` },
        });
        const category = await response.json();
        created.push(category);
      }

      await use(created);

      // Limpeza após todos os testes do worker terminarem
      for (const category of created) {
        try {
          await request.delete(`/api/categories/${category.id}`, {
            headers: { Authorization: `Bearer ${process.env.API_SEED_TOKEN}` },
          });
        } catch (error) {
          console.warn(`Category cleanup failed for ${category.id}:`, error);
        }
      }
    },
    { scope: 'worker' },
  ],
});

export { expect } from '@playwright/test';

Testes no mesmo worker recebem o mesmo array productCategories. Como só leem dele, não há interferência entre testes. Se os testes rodaram em paralelo em múltiplos workers, cada worker cria seu próprio conjunto, o que é adequado para dados de referência.

Nunca use escopo de worker para dados que os testes modificam. Se um teste muda um estado compartilhado, os testes que rodam depois no mesmo worker vão ver a versão modificada. Isso gera falhas sutis dependentes de ordem que são difíceis de debugar.

Garantindo unicidade: emails com UUID e IDs à prova de colisão

Mesmo com o Faker, colisões de unicidade podem acontecer. O internet.email() do Faker usa um pool de nomes e domínios comuns, então marcus.holloway@gmail.com poderia aparecer duas vezes em uma execução longa. Para qualquer campo que o banco aplica como único, você precisa de uma estratégia que garanta zero repetições.

A abordagem mais confiável é embutir um UUID no valor:

// helpers/unique.ts
import { faker } from '@faker-js/faker';

export function uniqueEmail(prefix = 'test'): string {
  const id = faker.string.uuid().slice(0, 8);
  return `${prefix}+${id}@test-suite.local`;
}

export function uniqueUsername(): string {
  const id = faker.string.uuid().slice(0, 8);
  return `user_${id}`;
}

export function uniqueSlug(base: string): string {
  const id = faker.string.uuid().slice(0, 6);
  return `${base}-${id}`.toLowerCase().replace(/\s+/g, '-');
}

Isso produz valores como test+a3f9c2b1@test-suite.local e user_a3f9c2b1. Oito caracteres de UUID equivalem a 16^8 = 4,3 bilhões de valores possíveis, então a probabilidade de colisão em uma suite de testes é efetivamente zero. Usar o domínio test-suite.local também torna trivial identificar e deletar em massa dados de teste se a limpeza ficar para trás.

Atualize sua factory para usar esses helpers:

// factories/user.factory.ts (atualizado)
import { faker } from '@faker-js/faker';
import { uniqueEmail, uniqueUsername } from '../helpers/unique';

export function buildUser(overrides: Partial<User> = {}): User {
  return {
    firstName: faker.person.firstName(),
    lastName: faker.person.lastName(),
    email: uniqueEmail(),          // garantidamente único
    username: uniqueUsername(),    // garantidamente único
    password: faker.internet.password({ length: 12 }),
    role: 'viewer',
    createdAt: faker.date.past({ years: 1 }),
    ...overrides,
  };
}

Se sua aplicação valida domínios de email estritamente, substitua test-suite.local por um domínio real que você controla, ou configure um domínio de teste especial na lista de permitidos do seu ambiente de staging. Algumas apps também rejeitam + em endereços de email. Nesse caso, use um formato de UUID como prefixo: a3f9c2b1.test@yourdomain.com.

Para IDs numéricos gerados por uma sequência de banco, a sequência cuida da unicidade por você, então você não precisa pré-gerar IDs. Deixe a API retornar o ID criado e use-o no seu teste. Só gere IDs no cliente quando estiver testando sistemas que aceitam IDs fornecidos pelo cliente, como UUIDs armazenados diretamente no banco.

FAQ

Devo usar uma função factory ou uma classe builder?

Funções factory são mais simples e funcionam para a maioria dos casos. Use builders quando você tem vários presets significativos (como asAdmin(), asUnverified(), asSuspended()) que combinam múltiplos valores de campo. Se você se encontra passando o mesmo objeto de override repetidamente, isso é sinal de que um método builder nomeado seria mais limpo.

E se minha API exigir autenticação para criar dados de teste?

Armazene o token de seed em uma variável de ambiente e carregue com process.env. Para CI, injete a variável pelos secrets do pipeline (GitHub Actions: secrets.API_SEED_TOKEN). Nunca hardcode credenciais em arquivos de código-fonte.

Posso compor múltiplas fixtures de dados em um teste?

Sim. Solicite tantas fixtures quanto o teste precisar: async ({ testUser, testAdminUser, productCategories }). O Playwright resolve e cria todas elas antes do teste rodar, depois limpa todas depois, em ordem inversa de criação.

Como lidar com limpeza de dados se o teste deixa o app em estado quebrado?

O teardown da fixture roda independente do resultado do teste. O padrão try/catch no bloco de teardown garante que, se a deleção falhar, o erro é logado mas não produz uma falha falsa. Isso acontece, por exemplo, quando o teste já deletou o recurso como parte do fluxo sendo testado. Se um recurso foi intencionalmente deletado pelo teste, verifique antes de deletar: if (response.status() !== 404) await deleteViaApi(id).

É seguro ter o Faker.js apenas como devDependency?

Sim. Instale com --save-dev e importe apenas em arquivos de teste e de factory/helper. Ele nunca vai para produção. Se você usar uma factory em código de produção, as fronteiras de módulo do TypeScript e o tree-shaking vão detectar isso, ou você pode forçar com uma regra do ESLint.

→ Veja também: Gerenciamento de Dados de Teste no Playwright: Estratégias e Padrões | Fixtures Personalizados no Playwright: O Padrão que Torna os Testes Legíveis | Isolamento de Testes: Por que Cada Teste Playwright Deve Ser sem Estado