
Привет! На связи Даня, разработчик на 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. Фикстуры — это как невидимые супергерои ваших тестов: они работают за кулисами, чтобы мы могли сосредоточиться на самом важном.
