Как только тестов становится много, всплывает вопрос: где готовить зависимости, которые нужны сценариям, - авторизованного пользователя, тестовые данные, объект страницы? Раскладывать создание и уборку по каждому тесту вручную - путь к дублированию и забытой очистке. Playwright решает это фикстурами: fixture - это типизированный жизненный цикл зависимости, которую тест получает готовой через аргумент, не заботясь о том, как она создана и когда убрана.
Механизм строится на test.extend. Базовый test расширяют, описывая набор фикстур, и каждая - это функция вида async ({ ...зависимости }, use) => {}. Всё до вызова use - подготовка, сам use(value) отдаёт значение тесту, а код после use - уборка, которая выполнится, когда тест закончится. Так создание и удаление живут в одном месте, а тест просто просит нужную фикстуру по имени в аргументе.
Ключевое свойство - фикстуры видят друг друга. Фикстура user может зависеть от request, а dashboard - от page, объявив их в своём аргументе; Playwright сам выстроит порядок и передаст готовое. Это dependency injection для тестов: сценарий объявляет, что ему нужно - пользователь и страница дашборда, - а инфраструктура собирает граф зависимостей и отдаёт всё готовым, уже с настроенной уборкой.
Полезно один раз увидеть, как фикстуры собираются в один модуль. Ниже - файл, где base.extend описывает user (создаётся через API, удаляется после теста) и dashboard (объект страницы поверх page), а рядом реэкспортируется expect. Тесты потом импортируют этот расширенный test вместо базового - и получают свои зависимости через аргумент. К этой форме возвращаются, добавляя в проект новую общую зависимость.
У фикстур есть область видимости, и её выбор важен. Test-scoped фикстура создаётся заново для каждого теста - это дефолт и правильный выбор для всего изменяемого: свежий пользователь, свои данные, чистое состояние. Worker-scoped создаётся один раз на worker и живёт весь его прогон - для дорогого и неизменяемого, что не жалко разделить между тестами одного процесса, вроде тяжёлого клиента или соединения.
Граница между областями проходит по изменяемости. В worker-scoped фикстуру нельзя класть mutable test data: если тесты в одном worker'е делят изменяемого пользователя или заказ, они начнут влиять друг на друга и падать в зависимости от порядка - тот самый флак, который изоляция и должна была убрать. В worker-scope - только дорогое и общее для чтения; всё, что тест меняет, - в test-scope, свежим на каждый запуск.
Отдельно полезны автоматические фикстуры. Automatic fixture подключается к каждому тесту без явного запроса в аргументе - это удобно для сквозной инфраструктуры: включить сбор логов, приложить диагностические attachments при падении, настроить окружение. Тест о ней не знает и не должен, а она делает свою работу вокруг каждого сценария - идеальное место для того, что нужно всем тестам одинаково.
Типичные провалы фикстур предсказуемы. Mutable-данные в worker-scoped фикстуре - гонки и зависимость от порядка внутри worker'а. Уборка, размазанная по телу теста вместо кода после use, - забытая очистка при падении в середине. И ручное создание одних и тех же зависимостей в каждом тесте вместо фикстуры - дублирование, которое рассыпается при первом же изменении. Готовьте зависимости фикстурами, изменяемое держите test-scoped, а уборку - после use.
import { test as base } from '@playwright/test'
type Fixtures = {
user: { id: string; email: string }
dashboard: DashboardPage
}
export const test = base.extend<Fixtures>({
user: async ({ request }, use) => {
const user = await createUser(request)
await use(user) // отдаём тесту
await deleteUser(request, user.id) // уборка после теста
},
dashboard: async ({ page }, use) => {
await use(new DashboardPage(page))
},
})
export { expect } from '@playwright/test'