Привет! На связи Даня, разработчик на 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. Фикстуры — это как невидимые супергерои ваших тестов: они работают за кулисами, чтобы мы могли сосредоточиться на самом важном.
Комментарии (5)
Noah1
29.01.2025 10:43Пользователи требуют больше возможностей — и желательно «еще вчера».
Нет. Этого требуют менеджеры. Пользователям 95% изменений не нужны, более того - неприятны.
oracle_schwerpunkte
К сожалению, на практике вещь досататочно бесполезная, так как в fixture нельзя передать параметры.
Вот здесь хотелось бы задавать разное имя пользователя для каждого теста , но это невозможно сделать адекватным способом.
test('Тест с фикстурой loginPage', async ({ loginPage })
Хак с test.use({ login: 'aloha' })
не не будет работать с несколькими тестами.
Хак с передачей парамертов через tag больше похож на злую шутку.
jQwery Автор
В этом моменте согласен
Однако что насчет создания фикстур через фабрику, куда в свою очередь можно прокидывать параметры
Я собираюсь подробнее описать этот кейс во второй части статьи
А если рассматривать глобально фикстуры, то не соглашусь что это бесполезно
В большинстве кейсов они полезны и существенно уменьшают время написания тестов, а если рассмотреть их возможности настройки, в частности настройки для запуска с каждым тестом отдельно, или один раз для всего скопа тестов - тут уже фикстуры могут быть мощной штукой
QALiberum
Можно сделать столько фикстур, сколько пользователей.
Юзать POM и метод логина в нем + фикстуры, как в примере автора.
LionMuzzle
Не совсем понял, что значит нельзя передать параметры? При вызове фикстуры это делается легко. Так же как и при регистрации фикстуры. Вот пример, очень упрощенный: