test.beforeAll создающий одного пользователя для нескольких тестов: гонка условий. Когда тест удаления запустится первым в параллельном воркере, тесты просмотра и редактирования упадут с 404. Каждому тесту нужны собственные данные, созданные до его запуска и удалённые после. Эта статья разбирает пять паттернов управления тестовыми данными в Playwright: от файла констант для стабильных справочных данных до API-фикстур которые создают и удаляют записи на каждый тест, включая ситуации когда сидирование базы данных правильный выбор и как работать с учётными данными для разных окружений не коммитя их в репозиторий.

Проблема захардкоженных данных

// Хрупко — ломается когда пользователя удалят или сменится пароль
test('user can log in', async ({ page }) => {
  await page.fill('[data-testid="email"]', 'admin@test.com');
  await page.fill('[data-testid="password"]', 'AdminPass1');
});

Проблемы: зависимость от конкретных данных в базе, несколько тестов делящих один аккаунт конфликтуют при параллельном запуске, и тест работает только в одном окружении если не помнить менять email.

Паттерн 1: файл констант

Простейший шаг от захардкоженных значений: централизовать все тестовые данные в одном файле.

// 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;

// В тестах
import { TEST_USERS, TEST_PRODUCTS } from '../data/users';

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

Лучше чем разрозненные захардкоженные значения, но всё равно зависит от существования конкретных пользователей.

Паттерн 2: фабричные функции

Фабричные функции генерируют уникальные тестовые данные для каждого теста:

// 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,
  };
}

// В тестах
import { generateUser } from '../data/factories';

test('create a new user', 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);
});

Каждый тест получает уникальный email, коллизий больше нет.

Паттерн 3: API-настройка в фикстурах

Создаём свежие данные через API перед каждым тестом, удаляем после:

// 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>({
  // Admin token — общий, worker scope (создаётся один раз на воркер)
  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' }],

  // Test user — уникальный на каждый тест
  testUser: async ({ request, adminToken }, use) => {
    const userData = generateUser();
    
    // СОЗДАНИЕ: новый пользователь перед тестом
    const createResp = await request.post('/api/users', {
      data: userData,
      headers: { Authorization: `Bearer ${adminToken}` },
    });
    const user = await createResp.json();

    // Логин для получения токена
    const loginResp = await request.post('/api/auth/login', {
      data: { email: userData.email, password: userData.password },
    });
    const { token } = await loginResp.json();

    // Передаём тесту что ему нужно
    await use({ 
      id: user.id, 
      email: userData.email, 
      password: userData.password,
      token 
    });

    // TEARDOWN: удаляем после теста
    await request.delete(`/api/users/${user.id}`, {
      headers: { Authorization: `Bearer ${adminToken}` },
    });
  },
});

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

test('user can update their profile', async ({ page, testUser }) => {
  // testUser — свежий пользователь, создан специально для этого теста
  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');
  // После теста: пользователь удаляется автоматически
});

Паттерн 4: сидирование базы данных

Для сложных сценариев данных сидируй базу напрямую:

// setup/seed.ts
import { chromium } from '@playwright/test';

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('Database seeded successfully');
}

seed();

Запускай перед тестами: node setup/seed.ts && npx playwright test

Паттерн 5: сохранённое состояние авторизации

Не логинься в начале каждого теста. Залогинься один раз, сохрани состояние браузера, переиспользуй:

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

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

setup('authenticate', 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');
  
  // Сохраняем storage state (куки, 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',  // Уже залогинен
    },
    dependencies: ['setup'],
  },
],

Тесты в проекте authenticated пропускают флоу логина: они стартуют с уже аутентифицированной сессией.

Данные для разных окружений

Используй переменные окружения чтобы указывать на нужные данные:

// 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: {
    // Пользователь только для чтения для smoke-тестов на проде
    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

Изоляция тестов

При параллельном запуске тесты не должны делить изменяемое состояние.

Плохо: разделяемое состояние

let userId: number;

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

// Несколько тестов используют один userId — гонки условий!
test('view user', async ({ page }) => { await page.goto(`/users/${userId}`); });
test('edit user', async ({ page }) => { /* редактирует того же пользователя */ });
test('delete user', async ({ page }) => { /* удаляет его! */ });

Хорошо: изолированное состояние

// Фикстуры дают каждому тесту своего пользователя
test('view user', async ({ page, testUser }) => { 
  await page.goto(`/users/${testUser.id}`); 
});

test('edit user', async ({ page, testUser }) => { 
  // Этот testUser отличается от того что выше
});

Очистка после тестов

Всегда удаляй данные которые создают твои тесты:

test('creates a product', 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 {
    // Выполняется всегда, даже если тест упал
    if (productId!) {
      await request.delete(`/api/products/${productId}`, {
        headers: { Authorization: `Bearer ${adminToken}` },
      });
    }
  }
});

Лучше: помести очистку в teardown фикстуры (после await use(...)). Тогда она выполняется безусловно.

Шпаргалка

| Паттерн | Лучше всего для |

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

| Файл констант | Стабильные справочные данные (роли, категории) |

| Фабричные функции | Генерация уникальных тестовых данных |

| API-фикстура setup/teardown | Изолированные свежие данные на каждый тест |

| Сидирование базы данных | Сложное начальное состояние перед запуском сьюта |

| Сохранённое состояние авторизации | Пропуск логина в каждом тесте |

Самый надёжный подход сочетает несколько паттернов: фабричные функции для генерации уникальных данных, API-фикстура для создания и удаления на каждый тест, сохранённое состояние авторизации для базовой аутентифицированной сессии. Каждый тест полностью изолирован: никакого разделяемого состояния, никаких гонок, никакой зависимости от порядка выполнения.

→ See also: Переиспользуемые тестовые данные: фабрики, фикстуры и Faker.js в Playwright | Изоляция тестов: почему каждый тест Playwright должен быть stateless | Авторизация в Playwright через storageState (без логина в каждом тесте) | API-тестирование с Playwright APIRequestContext (без Postman)