Playwright в картинках: как работают фикстуры
- четверг, 2 июля 2026 г. в 00:00:06
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 (жёлтая полоса) создаётся один раз для воркера и переиспользуется двумя тестами:

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