Playwright в картинках — серия статей, где я использую playwright-timeline-reporter, чтобы показывать разные концепции Playwright на простых графиках.
Фикстуры — одна из центральных частей Playwright. Они позволяют вынести подготовку и сброс состояния за пределы теста, чтобы сам тест был сосредоточен на проверяемом поведении. Если коротко, фикстуры это before/after хуки на стероидах.
Но есть и обратная сторона: с фикстурами выполнение теста становится менее очевидным. Код фикстуры может запускаться до теста, после теста, один раз на воркер или даже без явного упоминания в тесте. В этой статье я пройдусь по основным вариантам использования фикстур и покажу, как каждый из них выглядит на таймлайне.
Простая фикстура
Фикстуры объявляются через test.extend(). Вот пример фикстуры myTestFixture:
fixtures.ts
import { test as base } from '@playwright/test'; export const test = base.extend<{ myTestFixture: string }>({ myTestFixture: async ({}, use) => { // fixture setup... await use('fixture value'); // fixture teardown... }, });
По умолчанию фикстуры имеют скоуп тест. Это значит, что такая фикстура запускается для каждого теста, который её использует. Давайте используем myTestFixture в тесте:
example.spec.ts
import { test } from './fixtures'; test('test 1', async ({ myTestFixture }) => { // uses 'myTestFixture' // ... });
Запускаем тест:
npx playwright test
Теперь Playwright создаёт myTestFixture перед тестом и удаляет её после теста. На графике это синие полосы:

Теперь добавим второй тест, который использует ту же фикстуру myTestFixture:
example.spec.ts
test('test 1', async ({ myTestFixture }) => { // uses 'myTestFixture' // ... }); test('test 2', async ({ myTestFixture }) => { // uses 'myTestFixture' // ... });
Так как myTestFixture имеет скоуп тест, Playwright создаёт новый экземпляр для каждого теста. Первый экземпляр удаляется до того, как будет создан второй:

Ленивые фикстуры
Фикстура не запускается просто потому, что она объявлена. Playwright создаёт её только тогда, когда тест или другая фикстура её запрашивает.
Теперь уберём myTestFixture из аргументов test 2:
example.spec.ts
test('test 1', async ({ myTestFixture }) => { // uses 'myTestFixture' // ... }); test('test 2', async () => { // does not use 'myTestFixture' // ... });
test 2 больше не запрашивает myTestFixture, поэтому Playwright не создаёт её для этого теста. На таймлайне синяя полоса фикстуры остаётся только вокруг test 1:

Автоматические фикстуры
Иногда фикстура должна запускаться для каждого теста. Для этого не нужно добавлять её в сигнатуру каждого теста. Достаточно пометить её как { auto: true }:
fixtures.ts
import { test as base } from '@playwright/test'; export const test = base.extend<{ myTestFixture: string }>({ myTestFixture: [async ({}, use) => { // fixture setup... await use('fixture value'); // fixture teardown... }, { auto: true }], // <-- make 'auto' fixture });
Теперь тест вообще не ссылается на myTestFixture:
example.spec.ts
import { test } from './fixtures'; test('test 1', async () => { // ... });
Playwright всё равно запускает фикстуру, потому что она автоматическая:

Встроенные фикстуры
Playwright также предоставляет несколько фикстур из коробки. Самая частая — page, которая даёт доступ к странице браузера:
example.spec.ts
import { test } from '@playwright/test'; test('test 1', async ({ page }) => { // ... }); test('test 2', async ({ page }) => { // ... });
После запуска этих тестов таймлайн показывает инициализацию фикстуры page перед каждым тестом. Это синие полосы:

На этом таймлайне бросаются в глаза две вещи:
Кроме фикстуры
page(синяя полоса), также есть фикстураbrowser(жёлтая полоса) со скоупом воркер. Причина в том, что фикстураpageзависит от фикстурыbrowser, поэтому Playwright сначала создаёт эту зависимость. Про зависимости фикстур я расскажу ниже.Вторая инициализация
pageзаметно короче первой. Playwright использует внутреннюю оптимизацию для следующих вызововpage.
Переопределение фикстур
Любую фикстуру можно переопределить ещё одним вызовом test.extend(). Например, этот код переопределяет встроенную фикстуру page и добавляет свою инициализацию и очистку вокруг неё:
fixtures.ts
import { test as base } from '@playwright/test'; export const test = base.extend({ page: async ({ page }, use) => { // custom page setup... await use(page); // custom page teardown... }, });
Обратите внимание на зависимость { page } внутри переопределения. Это оригинальная фикстура page из Playwright. Сначала Playwright создаёт эту оригинальную страницу, а затем запускает вашу обёртку вокруг неё.
Сам тест при этом выглядит так же. Он запрашивает page, а Playwright отдаёт ему переопределённую версию:
example.spec.ts
import { test } from './fixtures'; test('test 1', async ({ page }) => { // ... }); test('test 2', async ({ page }) => { // ... });
На таймлайне видно, что фикстура page теперь выполняется дольше за счёт дополнительной обёртки:

Зависимости
Фикстуры могут зависеть от других фикстур. В этом примере fixtureC зависит от fixtureA и fixtureB:
fixtures.ts
import { test as base } from '@playwright/test'; export const test = base.extend<{ fixtureA: string; fixtureB: string; fixtureC: string; }>({ fixtureA: async ({}, use) => { // fixture A setup... await use('A'); // fixture A teardown... }, fixtureB: async ({}, use) => { // fixture B setup... await use('B'); // fixture B teardown... }, fixtureC: async ({ fixtureA, fixtureB }, use) => { // fixture C setup... await use(`${fixtureA} - ${fixtureB} - C`); // fixture C teardown... }, });
Тест ссылается только на fixtureC:
example.spec.ts
import { test } from './fixtures'; test('test 1', async ({ fixtureC }) => { // ... });
Playwright видит, что fixtureC нужны fixtureA и fixtureB, поэтому сначала создаёт обе родительские фикстуры. Очистка выполняется в обратном порядке:

Фикстуры для воркеров
Когда инициализация тяжёлая и её можно переиспользовать между несколькими тестами, используйте фикстуру со скоупом воркер вместо обычной фикстуры на уровне тестов. Она объявляется в том же base.extend(), но функция фикстуры оборачивается в массив с опцией { scope: 'worker' }:
fixtures.ts
import { test as base } from '@playwright/test'; export const test = base.extend<{}, { myWorkerFixture: string }>({ myWorkerFixture: [async ({}, use) => { // worker fixture setup... await use('worker fixture value'); // worker fixture teardown... }, { scope: 'worker' }], });
Теперь используем myWorkerFixture в двух тестах:
example.spec.ts
import { test } from './fixtures'; test('test 1', async ({ }) => { // ... }); test('test 2', async ({ myWorkerFixture }) => { throw new Error('Intentional error'); });
Оба теста запускаются в одном воркере, поэтому Playwright создаёт один экземпляр myWorkerFixture и переиспользует его. Между телами двух тестов нет отдельного вызова этой фикстуры:

Небольшая деталь про репортер: в этом демо
test 2специально падает, чтобы отчёт мог показать очистку фикстуры (правая жёлтая полоса). При успешном выполнении теста Playwright сейчас не отдаёт тайминги очистки воркер-фикстур в API репортеров (см. playwright#38350).Это ограничение влияет только на отчёт. Инициализация и очистка фикстуры работают одинаково и для успешных, и для упавших тестов.
Скоуп воркер значит один раз на воркер, а не один раз на весь запуск тестов. Чтобы увидеть разницу, разнесём два теста по разным файлам:
spec1.test.ts
test('test 1', async ({ myWorkerFixture }) => { // ... });
spec2.test.ts
test('test 2', async ({ myWorkerFixture }) => { // ... });
Запускаем файлы с двумя воркерами:
npx playwright test --workers 2
С двумя воркерами каждый файл запускается в отдельном воркере. Каждый воркер получает свой myWorkerFixture, поэтому инициализация выполняется дважды:

Собираем всё вместе
В одном тесте можно комбинировать несколько фикстур с разным скоупом. Playwright инициализирует все нужные фикстуры и их зависимости в правильном порядке.
Вот пример объявления нескольких фикстур:
fixtures.ts
import { test as base } from '@playwright/test'; export const test = base.extend< { myTestFixture: string }, { myWorkerFixture: string } >({ myTestFixture: async ({}, use) => { // test fixture setup... await use('fixture value'); // test fixture teardown... }, myWorkerFixture: [async ({}, use) => { // worker fixture setup... await use('worker fixture value'); // worker fixture teardown... }, { scope: 'worker' }], });
Используем обе фикстуры в тестах:
example.spec.ts
import { test } from './fixtures'; test('test 1', async ({ myTestFixture, myWorkerFixture }) => { // ... }); test('test 2', async ({ myTestFixture, myWorkerFixture }) => { // ... });
На таймлайне видны обе фикстуры. myTestFixture (синяя полоса) создаётся и удаляется для каждого теста, а myWorkerFixture (жёлтая полоса) создаётся один раз для воркера и переиспользуется двумя тестами:

Заключение
Фикстуры это очень мощный инструмент для ваших тестов, особенно если держать в голове несколько принципов:
Скоуп: на каждый тест или на каждый воркер?
Проверяйте зависимости и переопределения фикстур
Следите за временем выполнения фикстур: даже одна медленная фикстура может заметно ухудшить производительность тестов
Удачного тестирования!
