Создание кастомных fixtures

Fixtures в Playwright — это механизм подготовки и управления состоянием окружения для тестов. Они позволяют централизованно определять объекты, которые могут быть переиспользованы в разных тестах, управлять жизненным циклом этих объектов и обеспечивать изоляцию тестов. Playwright поставляется с набором встроенных fixtures, таких как page, browser, context, но в реальных проектах часто требуется создавать кастомные fixtures под конкретные нужды.


Основы создания кастомного fixture

Кастомный fixture определяется с помощью метода test.extend, который позволяет расширить стандартный объект test собственными объектами. Основные моменты:

  • fixture — это асинхронная функция, которая принимает объект с уже существующими fixtures и функцию use.
  • use — функция, которая предоставляет значение fixture тесту и завершает его жизненный цикл после выполнения теста.
  • scope — опциональный параметр, определяющий область видимости: 'test' (по умолчанию) или 'worker'. test создаёт отдельный экземпляр для каждого теста, worker создаёт один экземпляр на весь воркер.

Пример простого кастомного fixture:

const { test } = require('@playwright/test');

const customTest = test.extend({
  userData: async ({}, use) => {
    const user = { name: 'John Doe', role: 'admin' };
    await use(user); // Передаём объект тесту
  }
});

customTest('проверка имени пользователя', async ({ userData }) => {
  console.log(userData.name); // John Doe
});

Fixture с асинхронной инициализацией

Fixtures часто требуют асинхронной подготовки, например, создание пользователя через API или подключение к базе данных. Playwright поддерживает асинхронные операции внутри fixture:

const customTest = test.extend({
  dbConnection: async ({}, use) => {
    const connection = await createDatabaseConnection(); // асинхронная инициализация
    await use(connection);
    await connection.close(); // автоматическое закрытие после теста
  }
});

customTest('запрос к базе', async ({ dbConnection }) => {
  const result = await dbConnection.query('SELECT * FROM users');
  console.log(result);
});

Ключевой момент: всё, что создаётся внутри fixture, должно корректно очищаться после использования, чтобы избежать утечек памяти и конфликтов между тестами.


Fixture с зависимостями

Fixtures могут зависеть друг от друга. Playwright обеспечивает автоматическое разрешение зависимостей на основе параметров функции:

const customTest = test.extend({
  authToken: async ({ dbConnection }, use) => {
    const token = await generateTokenFromDB(dbConnection, 'admin');
    await use(token);
  }
});

customTest('тест с авторизацией', async ({ authToken }) => {
  console.log(authToken); // Токен, полученный через зависимый fixture
});

Важно: порядок передачи аргументов в fixture имеет значение, Playwright создаёт зависимости автоматически, но нужно избегать циклических ссылок.


Scope fixture

Scope определяет, как часто создаётся объект fixture:

  • test — создаётся новый экземпляр для каждого теста.
  • worker — создаётся один экземпляр на поток выполнения (worker), что экономит ресурсы для тяжёлых объектов.

Пример:

const customTest = test.extend({
  sharedResource: [
    async ({}, use) => {
      const resource = await setupHeavyResource();
      await use(resource);
      await resource.cleanup();
    },
    { scope: 'worker' }
  ]
});

Параметризация fixtures

Fixtures можно параметризовать для тестирования с разными конфигурациями:

const customTest = test.extend({
  apiUrl: async ({}, use, testInfo) => {
    const env = testInfo.project.name; // различие между проектами
    const url = env === 'staging' ? 'https://staging.api' : 'https://prod.api';
    await use(url);
  }
});

customTest('тест API', async ({ apiUrl }) => {
  console.log(`API URL: ${apiUrl}`);
});

Советы по организации кастомных fixtures

  1. Минимизировать побочные эффекты: каждый fixture должен работать изолированно, чтобы тесты были независимыми.
  2. Закрывать ресурсы: все соединения, браузеры и временные файлы должны быть корректно освобождены.
  3. Использовать scope: 'worker' для тяжёлых объектов: базы данных, внешние сервисы, браузеры.
  4. Параметризовать fixtures через конфигурацию или testInfo.project.name для работы с разными окружениями.
  5. Документировать зависимости между fixtures, чтобы команда могла быстро понимать порядок инициализации объектов.

Практический пример: полный пользовательский workflow

const customTest = test.extend({
  db: async ({}, use) => {
    const connection = await createDatabaseConnection();
    await use(connection);
    await connection.close();
  },
  testUser: async ({ db }, use) => {
    const user = await db.createUser({ name: 'Alice' });
    await use(user);
    await db.deleteUser(user.id);
  },
  authToken: async ({ testUser }, use) => {
    const token = await generateToken(testUser);
    await use(token);
  }
});

customTest('полный сценарий', async ({ authToken, testUser }) => {
  console.log(authToken); // Токен для Alice
  console.log(testUser.name); // Alice
});

В этом примере:

  • Fixture db создаёт соединение с базой данных и закрывает его после теста.
  • Fixture testUser создаёт временного пользователя и удаляет его.
  • Fixture authToken зависит от testUser и генерирует токен авторизации.

Создание кастомных fixtures позволяет строить масштабируемую, поддерживаемую инфраструктуру тестов, облегчает управление ресурсами и изоляцию тестов. Их грамотное использование критически важно для проектов с большим количеством автоматизированных сценариев.