
Привет! На связи Даня, разработчик на Angular в T-Банке. Поделюсь с вами опытом использования фикстур в Playwright. Я решил поговорить об этом, потому что вместе с ростом функциональности проектов растут и сложности при тестировании, а фикстуры предоставляют удобный способ избавиться от дублирующегося кода и сложных моков.
Эта статья посвящена основам: зачем нужны фикстуры, чем они отличаются друг от друга и какую пользу приносят при тестировании веб-приложений. Мы подробно разберем устройство фикстур, посмотрим, как их создавать и грамотно внедрять в тесты. А еще рассмотрим практические примеры, которые помогут с легкостью применить полученные знания на реальном проекте. Поехали!
Погружение в проблему
Современные веб-приложения становятся все сложнее и объемнее. Они обрастают новыми функциями, страницами, вкладками и разделами словно снежный ком, катящийся с горы. Пользователи требуют больше возможностей — и желательно «еще вчера». Стремясь удовлетворить эти потребности, разработчики ускоряют процесс создания и доставки новых фич.
Вместе с увеличением скорости разработки растет и риск возникновения ошибок. Интеграционное тестирование становится неотъемлемой частью разработки приложений, помогая обеспечить качество и надежность продукта.
Тестирование сложных приложений — задача не из легких. Чтобы изолировать тестируемые функции и правильно настроить окружение для сценария, необходимо прибегать к мокированию API-запросов, сторонних сервисов и других данных. Этот процесс может стать сложным и трудоемким, особенно когда тесты переполнены моками, код дублируется и поддержка и чтение таких тестов превращаются в кошмар.
Вот пример мокирования двух небольших запросов в начале теста:
// Внутри теста
await page.route('**/api/users', async (route) => {
  await route.fulfill({
  status: 200,
  contentType: 'application/json',
  body: JSON.stringify({
    users: [
      { id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' },
      { id: 2, name: 'Bob', email: 'bob@example.com', role: 'user' },
      // ... еще 18 пользователей
    ],
    total: 20,
    page: 1,
    pageSize: 20,
  }),
  });
});
 
// Мокирование других API-запросов
await page.route('**/api/orders', async (route) => {
  await route.fulfill({
  status: 200,
  contentType: 'application/json',
  body: JSON.stringify({
    orders: [
      { id: 101, user_id: 1, total: 99.99, status: 'shipped' },
      // ... много данных
    ],
  }),
  });
});
 
// И так далее...Даже с двумя запросами код становится громоздким, его трудно читать и поддерживать. Дублирование моков в разных тестах увеличивает объем кода и усложняет его изменение, если нужно обновить данные.
В идеале интеграционные тесты хочется видеть как понятную последовательность шагов, где каждый шаг имеет ясное описание и приводит к конечному результату. Если добавлять в тесты большое количество сложных конструкций по подготовке данных, это ухудшает читаемость и усложняет поддержку тестов.
Как настроить тестовую среду правильно — базовые варианты решения
Часто между страницами и логическими блоками любого приложения есть большое количество одинаковых API-запросов и данных. Это значит, что во время мокирования и подготовки тестовой среды нужно обобщать эти данные, выносить их в отдельные места, вызывать как простую функцию и переиспользовать многократно.
Выносим в отдельную функцию. К примеру, у нас есть мокирование запроса к API, который необходим в каждом тесте:
await page.route('**/api/users', async (route) => {
  await route.fulfill({
  status: 200,
  contentType: 'application/json',
  body: JSON.stringify([{ id: 1, name: 'Alice' }]),
  });
});Мы можем поступить с ним так: вынести в отдельный файл, поместить в функцию — и из файла возвращать только функцию, чтобы в тесте мы могли вызывать только ее.
/ В отдельном модуле mocks.js
export async function mockUsersApi(page) {
    // Код нашего запроса из примера выше
}
 
// В тесте
import { mockUsersApi } from './mocks'
 
await mockUsersApi(page);Используем класс-билдер. Для более сложных моделей, требующих сложных комбинаций из большого количества данных, можно использовать классы-билдеры. Они позволяют вызывать методы цепочкой и поэтапно собирать сложную модель данных для теста.
Например, класс-билдер для сборки модели данных аккаунта пользователя accountsBuilder.ts:
export class AccountsBuilder {
    constructor() {
      this.accounts = [];
      this.withAdminRules = false;
    }
 
    addAccount(account) {
      this.accounts.push(account);
      return this;
    }
 
    setWithAdminRules () {
      this.withAdminRules = true;
      return this;
    }
 
    build() {
      return {
        accounts: this.accounts,
        withAdminRules: this.withAdminRules,
      };
    }
  }Стоит обратить внимание, что каждый метод класса возвращает this, а значит, можно вызывать все методы по цепочке и дополнять итоговую модель новой информацией.
Вот пример того, как это можно использовать в тесте:
import { AccountsBuilder } from './accountsBuilder'
 
const accountsData = new AccountsBuilder()
  .addAccount({
    id: ‘1’,
    name: 'User1',
    isOnline: 'true',
    status: 'don’t_worry',
    balance: 100000,
  })
  . setWithAdminRules()
  .build();
 
// Мокирование API
await mockUsersApi(page);Если данных для теста нужно много, то даже с использованием этих инструментов код может быть громоздким.
Можно пойти дальше и выносить повторяющиеся в файле моки в блок beforeEach, тем самым убирая дублирование кода. А очищать данные, возвращая хранилище к исходному состоянию, можно в блоке afterEach. Но это тоже не идеальное решение. 
Если мы вынесем повторяющийся код в beforeEach, то в самих тестах не будет понятно, что там используется и какие данные нужны. Возможна мутация данных от теста к тесту, что повлечет за собой нежелательное поведение. Поэтому не до конца понятно, где стоит размещать логику по подготовке данных для теста.
Используем фикстуры. Фикстуры — это специальные функции или объекты, которые помогают подготовить необходимое окружение для тестов и упростить повторное использование кода между ними. Они позволяют:
- Избежать дублирования кода: общие настройки можно вынести в фикстуры. 
- Упростить инициализацию и очистку: фикстуры могут автоматически запускать код до и после тестов. 
- Улучшить читаемость: тесты становятся короче и фокусируются на проверках, а не на настройках. 
Playwright предоставляет ряд встроенных фикстур, которые облегчают работу с тестами. Рассмотрим основные из них.
browser — предоставляет экземпляр браузера (Chromium, Firefox, WebKit). Позволяет контролировать браузер на уровне теста.
test('Использование browser фикстуры', async ({ browser }) => {
  const context = await browser.newContext();
  const page = await context.newPage();
  await page.goto('https://example.com');
  // Ваши проверки...
});context — предоставляет новый контекст браузера для каждого теста. Полезно для разделения состояния между тестами.
test('Использование context фикстуры', async ({ context }) => {
  const page = await context.newPage();
  await page.goto('https://example.com');
  // Ваши проверки...
});page — предоставляет новую страницу (вкладку) в браузере для каждого теста. Наиболее часто используемая фикстура для взаимодействия с веб-приложением.
test('Использование page фикстуры', async ({ page }) => {
  await page.goto('https://example.com');
  // Ваши проверки...
});request — предоставляет API для выполнения HTTP-запросов вне контекста браузера. Полезно для тестирования API или предварительной настройки данных.
test('Использование request фикстуры', async ({ request }) => {
  const response = await request.get('https://api.example.com/data');
  expect(response.status()).toBe(200);
  // Ваши проверки...
});browserName — содержит название браузера, в котором выполняется тест ('chromium', 'firefox', 'webkit'). Позволяет писать условный код в зависимости от браузера.
test('Использование browserName фикстуры', async ({ page, browserName }) => {
  await page.goto('https://example.com');
  if (browserName === 'webkit') {
  // Специфичные проверки для WebKit
  }
});testInfo — предоставляет информацию о текущем тесте, такую как название, статус, вложенность и так далее. Можно использовать для логирования или изменения поведения теста в зависимости от контекста.
test('Использование testInfo фикстуры', async ({ page }, testInfo) => {
  await page.goto('https://example.com');
  console.log(`Запуск теста: ${testInfo.title}`);
  // Ваши проверки...
});trace — управляет записью трассировки для отладки тестов. Позволяет включать и выключать запись трассировки.
test('Использование trace фикстуры', async ({ page, trace }) => {
  await trace.start({ screenshots: true, snapshots: true });
  await page.goto('https://example.com');
  // Ваши проверки...
  await trace.stop();
});parallelIndex — предоставляет индекс текущего параллельного процесса тестирования. Полезно для разделения ресурсов между параллельными тестами.
test('Использование parallelIndex фикстуры', async ({ parallelIndex }) => {
  const dbName = `test_db_${parallelIndex}`;
  // Инициализация базы данных для текущего теста
});Создание собственных фикстур
Создание пользовательских фикстур позволяет расширить возможности тестов и адаптировать их под ваши потребности. Рассмотрим процесс создания своих кастомных фикстур.
Шаг 1. Расширим базовый класс тестов. Нужно импортировать базовый test из @playwright/test и расширить его с помощью метода test.extend(). fixtures.ts:
import base from '@playwright/test'
 
export const test = base.test.extend({
  // Здесь будут ваши фикстуры
});import base from '@playwright/test' — импорт базового объекта test.
const test = base.test.extend({... }) — создаем новый объект test, расширяя базовый с помощью метода extend(). Это позволяет добавить новые фикстуры.
Шаг 2. Определим фикстуру. Фикстура определяется как свойство объекта, переданного в extend(). Ключ — имя фикстуры, значение — функция или массив, содержащий функцию и опции.
Пример простой фикстуры:
export const test = base.test.extend({
    myFixture: async ({}, use) => {
      // Инициализация фикстуры
      const data = await fetchData();
      // Передаем фикстуру в тест
      await use(data);
      // Очистка после теста
      await cleanUpData();
    },
  });myFixture — имя фикстуры.
async ({}, use) => { ... } — функция фикстуры.
{}, первый аргумент, — это объект с доступными фикстурами. Если фикстура зависит от других фикстур, можно их деструктурировать здесь.
use — функция, которую нужно вызвать, передав в нее значение фикстуры. До вызова use тестовая функция не начнет выполняться.
await use(data) — передача значения фикстуры в тест. После этого вызова начинается выполнение теста.
Код после use — выполняется после завершения теста. Здесь можно выполнять очистку, закрытие соединений и так далее.
Шаг 3. Используем фикстуру в тесте. Вы можете получить доступ к фикстуре через параметры функции. test.spec.ts:
mport { test } from './fixtures';
 
test('Тест с myFixture', async ({ myFixture }) => {
  // Используем myFixture в тесте
  console.log(myFixture);
  // Ваши проверки...
});({ myFixture }) — деструктурируем нашу фикстуру из параметров тестовой функции.
Если фикстура имеет auto: true, ее можно не указывать, она все равно будет инициализирована.
Логика после вызова use(): код, написанный после await use(...), выполняется после завершения теста. Это позволяет выполнять операции очистки, закрывать соединения, освобождать ресурсы и так далее.
Фикстуры сами по себе заменяют beforeEach и afterEach. Инициализация происходит до вызова use(), а очистка — после. Это упрощает структуру тестов и делает код более понятным.
При определении фикстуры можно указать дополнительные опции.
scope: определяет область действия фикстуры.
'test' (по умолчанию):
- Фикстура инициализируется для каждого теста отдельно. 
- Обеспечивает изоляцию между тестами. 
- Полезно, когда фикстура использует данные, которые могут изменяться от теста к тесту. 
'worker':
- Фикстура инициализируется один раз для каждого воркера. 
- Может повысить производительность за счет уменьшения числа инициализаций. 
- Нужно быть осторожным с изменяемым состоянием, так как оно будет общим для всех тестов в воркере. 
Пример фикстуры с scope: 'worker':
export const test = base.test.extend({
    sharedResource: [async ({}, use) => {
      const resource = await createResource();
      await use(resource);
      await resource.cleanup();
    }, { scope: 'worker' }],
  });В примере стоит обратить внимание, что в качестве значения для нашей фикстуры передаем не просто функцию, а массив, где первый элемент — это функция со значением фикстуры, а второй аргумент — объект с дополнительными опциями для работы фикстуры.
auto:
- true: фикстура инициализируется автоматически, даже если она не указана в параметрах теста. Полезно для фикстур, которые всегда должны быть активны (например, настройки окружения). 
- false (по умолчанию): фикстура инициализируется, только если она указана в параметрах теста. 
Пример фикстуры с auto: true. В этом случае environmentSetup будет выполняться для каждого теста автоматически:
export const test = base.test.extend({
    environmentSetup: [async ({}, use) => {
      await setupEnvironment();
      await use();
      await teardownEnvironment();
    }, { auto: true }],
  });timeout: устанавливает максимальное время выполнения для фикстуры. Если фикстура не завершится в указанный срок, тест будет прерван с ошибкой.
Полезно, если фикстура выполняет длительные операции, такие как подключение к внешним сервисам или сложные сетевые запросы, и вам нужно ограничить время их выполнения.
Значение timeout указывается в миллисекундах. Пример фикстуры с timeout:
  export const test = base.test.extend({
    dbConnection: [async ({}, use) => {
      const connection = await createDatabaseConnection();
      await use(connection);
      await connection.close();
    }, { timeout: 5000 }], // Таймаут 5000 мс
  });В этом примере, если фикстура dbConnection не завершится за 5 000 мс, тест завершится с ошибкой. Это полезно для случаев, когда инициализация может зависнуть или работать медленнее, чем обычно.
Изменения в работе фикстуры с добавлением опций
С scope: 'worker':
— Уменьшает количество инициализаций ресурса.
— Может повысить производительность.
— Нужно следить за тем, чтобы состояние фикстуры не изменялось в разных тестах.
С auto: true:
— Упрощает использование фикстуры.
— Может привести к ненужной инициализации, если фикстура не требуется в каждом тесте.
С timeout:
— Защищает тесты от зависания в случае, если фикстура выполняется слишком долго.
— Полезно для долгих операций, таких как сетевые запросы или мокация внешних API, особенно если они могут зависнуть или замедлиться.
— Устанавливает максимальное время ожидания, после которого тест будет завершен с ошибкой, если фикстура не завершится.
Применение фикстур в разных сценариях
Использование фикстур для замены базовых моков вызовов API. Мы уже разобрали пример с выносом мокирования запроса API в отдельный файл и созданием дополнительных классов для упрощения создания данных. Теперь посмотрим, как это выглядит с использованием фикстур.
Без фикстур:
test('Тест без фикстур', async ({ page }) => {
  await page.route('**/api/data', async (route) => {
  await route.fulfill({
    status: 200,
    body: JSON.stringify({ data: 'value' }),
  });
  });
 
  await page.goto('https://example.com');
  // Ваши проверки...
});С фикстурой:
// fixtures.ts
export const test = base.test.extend({
    apiMock: async ({ page }, use) => {
      await page.route('**/api/data', async (route) => {
        await route.fulfill({
          status: 200,
          body: JSON.stringify({ data: 'value' }),
        });
      });
      await use();
    },
  });
 
  // test.spec.ts
  import { test } from './fixtures'
 
  test('Тест с фикстурой apiMock', async ({ page, apiMock }) => {
    await page.goto('https://example.com');
    // Ваши проверки...
  });Разница и преимущества:
- Чистота кода: тест становится короче и понятнее. 
- Повторное использование: фикстуру - apiMockможно использовать в нескольких тестах.
- Управление: изменение мока в фикстуре влияет на все тесты, которые ее используют. 
- Читаемость: мы видим, какие данные в тесте используются. 
Замена билдеров. Билдеры можно использовать вместе с фикстурами, чтобы улучшить итоговый результат.
Без фикстур:
test('Тест без фикстур', async ({ page }) => {
  const dataBuilder = new DataBuilder()
  .setFieldA('valueA')
  .setFieldB('valueB')
  .build();
 
  await page.route('**/api/data', async (route) => {
  await route.fulfill({
    status: 200,
    body: JSON.stringify(dataBuilder),
  });
  });
 
  await page.goto('https://example.com');
  // Тестовая логика...
});С фикстурой:
// fixtures.ts
export const test = base.test.extend({
    testData: async ({}, use) => {
      const data = new DataBuilder()
        .setFieldA('valueA')
        .setFieldB('valueB')
        .build();
      await use(data);
    },
 
    apiMock: async ({ page, testData }, use) => {
      await page.route('**/api/data', async (route) => {
        await route.fulfill({
          status: 200,
          body: JSON.stringify(testData),
        });
      });
      await use();
    },
  });
 
  // test.spec.ts
  import { test } from './fixtures'
 
  test('Тест с фикстурой testData', async ({ page, apiMock }) => {
    await page.goto('https://example.com');
    // Тестовая логика...
  });Разница и преимущества:
- Единое место для данных: изменение в билдере отражается во всех тестах. 
- Уменьшение дублирования: нет необходимости создавать билдер в каждом тесте. 
- Чистота кода: тесты становятся короче и понятнее. 
Замена Page Object. Неотъемлемый элемент тестирования — паттерн PageObject — можно использовать совместно с фикстурами для написания более оптимизированных и стабильных тестов.
Без фикстур:
test('Тест без фикстур', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('user', 'pass');
 
  // Тестовая логика...
});С фикстурой:
// fixtures.ts
export const test = base.test.extend({
    loginPage: async ({ page }, use) => {
      const loginPage = new LoginPage(page);
      await use(loginPage);
    },
  });
 
  // test.spec.ts
  import { test } from './fixtures'
 
  test('Тест с фикстурой loginPage', async ({ loginPage }) => {
    await loginPage.goto();
    await loginPage.login('user', 'pass');
 
    // Тестовая логика...
  });Разница и преимущества:
- Упрощение тестов: нет необходимости создавать объект PageObject в каждом тесте. 
- Единообразие: все тесты используют одну и ту же фикстуру. 
- Легкость изменения: изменения в фикстуре отражаются во всех тестах. 
Переопределение базовых фикстур. Можно переопределять встроенные фикстуры, чтобы изменить их поведение или добавить дополнительную функциональность.
Пример переопределения фикстуры page:
  // fixtures.ts
export const test = base.test.extend({
    page: async ({ page }, use) => {
      // Настраиваем страницу перед использованием
      await page.setViewportSize({ width: 1280, height: 720 });
      await page.addInitScript(() => {
        // Дополнительные настройки или полифилы
      });
      await use(page);
      // Можно добавить логику после использования страницы
    },
  });
 
  // test.spec.ts
  import { test } from './fixtures'
 
  test('Тест с переопределенной page фикстурой', async ({ page }) => {
    await page.goto('https://example.com');
    // Страница уже настроена
    // Тестовая логика...
  });Пример переопределения фикстуры context:
// fixtures.ts
export const test = base.test.extend({
    context: async ({ browser }, use) => {
      const context = await browser.newContext({
        locale: 'ru-RU',
        geolocation: { longitude: 37.618423, latitude: 55.751244 },
        permissions: ['geolocation'],
      });
      await use(context);
      await context.close();
    },
  });
 
  // test.spec.ts
  import { test } from './fixtures'
 
  test('Тест с переопределенной context фикстурой', async ({ page }) => {
    await page.goto('https://example.com');
    // Контекст браузера настроен с нужной локалью и геолокацией
    // Тестовая логика...
  });Использование цепочки фикстур. Фикстуры могут зависеть друг от друга, и вы можете использовать одну фикстуру внутри другой.
Пример использования встроенной фикстуры внутри пользовательской:
// fixtures.ts
export const test = base.test.extend({
    customPage: async ({ page }, use) => {
      // Дополнительная настройка страницы
      await page.setExtraHTTPHeaders({ 'X-Custom-Header': 'value' });
      await use(page);
    },
  });
 
  // test.spec.ts
  import { test } from './fixtures'
 
  test('Тест с кастомной customPage фикстурой', async ({ customPage }) => {
    await customPage.goto('https://example.com');
    // Ваши проверки...
  });Пример использования одной пользовательской фикстуры внутри другой:
// fixtures.ts
export const test = base.test.extend({
    testData: async ({}, use) => {
      const data = { value: 42 };
      await use(data);
    },
 
    apiMock: async ({ page, testData }, use) => {
      await page.route('**/api/value', async (route) => {
        await route.fulfill({
          status: 200,
          body: JSON.stringify({ result: testData.value }),
        });
      });
      await use();
    },
  });
 
  // test.spec.ts
  import { test } from './fixtures'
 
  test('Тест с цепочкой фикстур', async ({ page }) => {
    await page.goto('https://example.com');
    // Тестовая логика...
  });Рассмотрим подробнее этот пример:
- Фикстура - apiMockзависит от- testDataи- page.
- Playwright автоматически инициализирует фикстуры в правильном порядке. 
Итоги
Использование фикстур в Playwright позволяет значительно улучшить структуру и читаемость ваших тестов:
- Тесты становятся короче и фокусируются на проверках, а не на настройках. 
- Снижается дублирование кода: общие настройки выносятся в фикстуры и могут быть переиспользованы. 
- Упрощается поддержка: изменения в фикстурах автоматически применяются ко всем тестам, которые их используют. 
- Повышается гибкость и расширяемость: вы можете создавать сложные цепочки фикстур, переопределять встроенные и адаптировать их под свои нужды. 
В следующей части мы рассмотрим продвинутые темы, такие как область действия фикстур (scope), параллелизация тестов, организация фикстур в больших проектах и многое другое. Оставайтесь с нами, будет интересно!
P. S. Фикстуры — это как невидимые супергерои ваших тестов: они работают за кулисами, чтобы мы могли сосредоточиться на самом важном.
Комментарии (7)
 - Noah129.01.2025 10:43- Пользователи требуют больше возможностей — и желательно «еще вчера». - Нет. Этого требуют менеджеры. Пользователям 95% изменений не нужны, более того - неприятны. 
 
           
 
oracle_schwerpunkte
К сожалению, на практике вещь досататочно бесполезная, так как в fixture нельзя передать параметры.
Вот здесь хотелось бы задавать разное имя пользователя для каждого теста , но это невозможно сделать адекватным способом.
test('Тест с фикстурой loginPage', async ({ loginPage })
Хак с test.use({ login: 'aloha' })
не не будет работать с несколькими тестами.
Хак с передачей парамертов через tag больше похож на злую шутку.
jQwery Автор
В этом моменте согласен
Однако что насчет создания фикстур через фабрику, куда в свою очередь можно прокидывать параметры
Я собираюсь подробнее описать этот кейс во второй части статьи
А если рассматривать глобально фикстуры, то не соглашусь что это бесполезно
В большинстве кейсов они полезны и существенно уменьшают время написания тестов, а если рассмотреть их возможности настройки, в частности настройки для запуска с каждым тестом отдельно, или один раз для всего скопа тестов - тут уже фикстуры могут быть мощной штукой
oracle_schwerpunkte
Реальная задача: измерять время выполнения тестов. Не должно включаться время потраченное на SSO login и на сохранение playwright скриншотов/видео. Хотелось бы сделать с помощью fixture но login не дает.
QALiberum
Можно сделать столько фикстур, сколько пользователей.
Юзать POM и метод логина в нем + фикстуры, как в примере автора.
LionMuzzle
Не совсем понял, что значит нельзя передать параметры? При вызове фикстуры это делается легко. Так же как и при регистрации фикстуры. Вот пример, очень упрощенный:
oracle_schwerpunkte
В Вашем примере логин происходит не внутри fixture, а внутри теста. И в логе будет отображаться внутри теста и засорять его.
Также я не могу использовать loginUser в другой fixture, todoPage , если пользователь будет определен только на уровне теста.