Введение в фикстуры Playwright

от автора

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


ссылка на оригинал статьи https://habr.com/ru/articles/877292/