
В первой части мы познакомились с базовыми понятиями фикстур в Playwright. Мы узнали, что такое фикстуры, для чего они нужны, какие бывают и как создавать собственные. Рассмотрели примеры и сценарии использования фикстур, которые способны упростить процесс тестирования и сделать тесты более чистыми и поддерживаемыми.
Но как быть, когда проект растет и количество фикстур увеличивается? Как эффективно организовать фикстуры в большом проекте? Как управлять дополнительными опциями фикстур и как они влияют на параллельное выполнение тестов? В этой части глубже погрузимся в тему фикстур и рассмотрим все эти вопросы.
Как использовать фикстуры из нескольких файлов в одном месте
В большом проекте хранение всех фикстур в одном файле становится непрактичным. Это приводит к низкой читаемости кода, сложности в поддержке, проблемам с переиспользованием. Есть несколько подходов к решению проблемы.
Объединение фикстур из нескольких файлов в одном тесте. Предположим, что у нас есть структура проекта:

Шаг 1. Экспорт фикстур из отдельных файлов.
apiFixtures.ts:
export const apiFixtures = {
apiMock: async ({ page }, use) => {
// код фикстуры
},
};
pageObjectsFixtures.ts:
import { HomePage } from '../pageObjects/homePage'
import { LoginPage } from '../pageObjects/loginPage'
export const pageObjectsFixtures = {
homePage: async ({ page }, use) => {
// код фикстуры
},
loginPage: async ({ page }, use) => {
// код фикстуры
},
};
Шаг 2. Объединение фикстур в тесте.
testA.spec.ts:
import base from '@playwright/test'
import { commonFixtures } from '../fixtures/commonFixtures'
import { apiFixtures } from '../fixtures/apiFixtures'
import { pageObjectsFixtures } from '../fixtures/pageObjectsFixtures'
const test = base.test.extend({
...commonFixtures,
...apiFixtures,
...pageObjectsFixtures,
});
test('Тест A', async ({ homePage, loginPage, apiMock }) => {
await loginPage.goto();
await loginPage.login('user', 'pass');
// Дополнительная логика теста...
});
Подход с объединением фикстур позволяет выбрать только те из них, что нужны в тесте, причем в самом тесте будет явно видно, какие фикстуры используются. Подход можно оценить как простой, ведь он не требует использования дополнительных абстракций и других сложных конструкций.
Мы можем вынести конструкцию по объединению фикстур в отдельный файл, расширять базовый test отдельно и сразу импортировать его в файл теста, чтобы не перегружать файл теста расширениями базового test.
Использование фабрики фикстур. Давайте подумаем над вариантом автоматизации использования фикстур из разных файлов в одном тесте. Решим эту задачу с использованием фабричной функции, которая будет объединять фикстуры.
Представим, что у нас есть структура проекта:

Шаг 1. Экспорт фикстур из файлов.
pageObjectsFixtures.ts:
import { test: base } from './commonFixtures'
import { HomePage } from '../pageObjects/homePage'
import { LoginPage } from '../pageObjects/loginPage'
export const test = base.extend({
homePage: async ({ page }, use) => {
// текст фикстуры
},
loginPage: async ({ page }, use) => {
// текст фикстуры
},
});
apiFixtures.ts:
import { test: base } from './commonFixtures'
export const test = base.extend({
apiMock: async ({ page }, use) => {
// текст фикстуры
},
});
Шаг 2. Создание фабрики фикстур. Чтобы объединить фикстуры из разных файлов без дублирования кода, мы можем создать фабрику фикстур.
fixtureFactory.ts:
import { test: base } from '@playwright/test'
/**
* Фабрика для объединения фикстур из разных модулей.
* @param {...object} fixturesModules - Модули с фикстурами для объединения.
* @returns {object} - Объект test с объединенными фикстурами.
*/
export function createTestWithFixtures(...fixturesModules) {
let extendedTest = base;
for (const fixtures of fixturesModules) {
extendedTest = extendedTest.extend(fixtures.testFixtures || fixtures);
}
return extendedTest;
}
Шаг 3. Использование фабрики в тестах.
testA.spec.ts:
import { pageObjectsFixtures } from '../fixtures/pageObjectsFixtures'
import { apiFixtures } from '../fixtures/apiFixtures'
import { testAFixtures } from '../fixtures/specificFixtures/testAFixtures'
import { createTestWithFixtures } from '../fixtures/fixtureFactory'
const test = createTestWithFixtures(pageObjectsFixtures, apiFixtures, testAFixtures);
test('Тест A', async ({ homePage, loginPage, apiMock, testAData }) => {
// Ваш тест, использующий объединенные фикстуры
await loginPage.goto();
await loginPage.login('user', 'pass');
// Дополнительная логика теста...
});
Подход с использованием фабрик обеспечивает гибкость в использовании фикстур, так как можно комбинировать разные фикстуры по мере необходимости для каждого теста. Благодаря тому, что логика объединения находится в одном месте, уменьшается дублирование кода. Теперь в тестах не нужно расширять базовый test, чтобы использовать фикстуры, а значит, код становится более чистым.
Использование встроенной функции mergeTests. Playwright предлагает нативную реализацию фабричной функции, которую мы рассмотрели в предыдущем примере. Для реализации объединения фикстур используем фикстуры pageObjectsFixtures,
apiFixtures, testAFixtures.
testA.spec.ts:
import { pageObjectsFixtures } = from '../fixtures/pageObjectsFixtures'
import { apiFixtures } = from'../fixtures/apiFixtures'
import { testAFixtures } = from '../fixtures/specificFixtures/testAFixtures'
const test = mergeTests(pageObjectsFixtures, apiFixtures, testAFixtures);
test('Тест A', async ({ homePage, loginPage, apiMock, testAData }) => {
// Ваш тест, использующий объединенные фикстуры
await loginPage.goto();
await loginPage.login('user', 'pass');
// Дополнительная логика теста...
});
Как менять дополнительные опции на ходу
Представим, что вам нужна одна и та же фикстура для разных целей. Например, где-то вы хотите использовать ее с областью действия scope: test,
а где-то — с scope: worker.
Создавать две отдельные фикстуры с одинаковой функциональностью, но разным scope не хотелось бы, так как это приводит к дублированию кода.
Если вспомнить, как создается фикстура, становится понятно, как должна выглядеть фабрика по созданию фикстур:
export function createFixture(fixtureName, fixtureFunction, options = {}) {
return {
[fixtureName]: [fixtureFunction, options],
};
}
В эту функцию можно передать название фикстуры, функцию, где содержится основная функциональность, и, самое главное, дополнительные опции. Теперь мы можем использовать фабрику для создания фикстур на лету и под наши потребности:
export const test = base.test.extend(createFixture(
'dbConnection',
async ({}, use) => {
const db = await createDatabaseConnection(connectionParams);
await use(db);
await closeDatabaseConnection(db);
},
{ auto: true, scope: 'test' }
))
tests/test.spec.ts:
import { test } from '../fixtures/dbFixtures'
test('Тест с кастомным dbConnection', async ({ dbConnection }) => {
// Используйте dbConnection в вашем тесте
});
Решение избавляет нас от дублирования кода, делает фикстуры еще более переиспользуемыми, ведь теперь можно использовать их с различными параметрами. Также фикстуры, созданные таким образом, можно использовать с фикстурами из других файлов и добиться от кода максимума.
Параллельное выполнение тестов с фикстурами и без
При параллельном выполнении тестов возникает риск мутации общих данных, особенно если использовать глобальные переменные или общие ресурсы без должной изоляции.
Вот пример, где данные из sharedData могут быть изменены одновременно несколькими тестами, что приведет к нестабильным результатам:
let sharedData = {};
beforeEach(() => {
sharedData = { value: 0 };
});
test('Тест 1', () => {
sharedData.value += 1;
expect(sharedData.value).toBe(1);
});
test('Тест 2', () => {
sharedData.value += 1;
expect(sharedData.value).toBe(1);
});
В случае нестабильности в тестах можно положиться на фикстуры, которые будут гарантировать сохранность данных от теста к тесту:
import base from '@playwright/test'
const test = base.test.extend({
sharedData: async ({}, use) => {
const data = { value: 0 };
await use(data);
},
});
test('Тест 1', async ({ sharedData }) => {
sharedData.value += 1;
expect(sharedData.value).toBe(1);
});
test('Тест 2', async ({ sharedData }) => {
sharedData.value += 1;
expect(sharedData.value).toBe(1);
});
Использование фикстур решает проблему мутации и нестабильного состояния данных. Каждый тест получает свою копию sharedData. Тесты не влияют на состояние друг друга при параллельном выполнении.
Параллельное выполнение и scope
Мы уже рассматривали применение опции scope в фикстурах. Теперь давайте углубимся в эту тему и рассмотрим особенности работы с точки зрения параллельного выполнения тестов.
Scope: test
:
Фикстура инициализируется для каждого теста отдельно.
Обеспечивает полную изоляцию между тестами.
Подходит для случаев, когда состояние не должно разделяться.
Использование scope: test
для фикстур создает и изолирует каждую фикстуру отдельно для каждого теста, что обеспечивает независимость данных.
Когда использовать scope: test:
Изоляция данных критична: если каждый тест должен работать с независимыми данными. Например, каждый тест создает, изменяет или удаляет данные, которые могут конфликтовать с другими тестами.
Состояние меняется в процессе теста: если фикстура содержит состояние, которое тесты могут менять, и эти изменения не должны влиять на другие тесты.
Тесты требуют полной независимости: это полезно, если тесты проверяют разные аспекты одного и того же ресурса и изменение состояния ресурса в одном тесте может повлиять на другой.
Пример:
export const test = base.extend({
dbConnection: [async ({}, use) => {
const connection = await createDatabaseConnection();
await use(connection);
await connection.close();
}, { scope: 'test' }], // Создается отдельное подключение для каждого теста
});
Если у вас есть тесты, которые проверяют создание, обновление и удаление записей в базе данных, и каждому тесту нужен свой независимый экземпляр базы данных, scope: test
будет оптимальным.
В этом случае каждый тест получит свою изолированную фикстуру dbConnection, что предотвратит любые конфликты.
Наряду с преимуществами использования опции можно выделить ряд недостатков:
Более высокая нагрузка на ресурсы и большее время выполнения, так как фикстуры создаются и удаляются для каждого теста отдельно.
Может не подойти для ресурсов, которые сложно или долго инициализировать многократно.
Scope: worker
:
Фикстура инициализируется один раз для каждого воркера.
Состояние фикстуры разделяется между тестами внутри одного воркера.
Уменьшает количество инициализаций, что может повысить производительность.
Риск мутации общего состояния между тестами.
Использование scope: worker
позволяет создать одну фикстуру для всех тестов в файле, которые выполняются параллельно. Эта фикстура будет инициализирована один раз для каждого рабочего процесса (worker), и все тесты будут ее совместно использовать.
Когда использовать scope: worker
:
Состояние фикстуры не изменяется тестами: если фикстура предоставляет статические данные или подключение, которые не меняются в ходе тестов.
Ресурсы дорого создавать: если инициализация ресурса требует много времени или ресурсов и вы хотите создать его только один раз. Например, подключение к базе данных.
Небольшой риск конфликта данных: если фикстура предоставляет данные, которые не нуждаются в изоляции. Например, если все тесты только читают из базы данных или проверяют доступ к статическим ресурсам.
Пример:
export const test = base.extend({
dbConnection: [async ({}, use) => {
const connection = await createDatabaseConnection();
await use(connection);
await connection.close();
}, { scope: 'worker' }], // Подключение к базе данных создается один раз для всех тестов
});
Если у вас есть тесты, которым нужно только читать данные из базы данных и они не изменяют состояние, scope: worker
оптимален, так как это снижает нагрузку на ресурсы.
Здесь dbConnection будет создан один раз для всех тестов в файле, что уменьшит время на инициализацию.
Преимущества и недостатки scope: | |
+ Меньшая нагрузка на ресурсы и более быстрое выполнение тестов, так как ресурс создается только один раз + Подходит для тестов, которые не изменяют состояние ресурса и могут совместно использовать один экземпляр |
— Отсутствие изоляции между тестами — изменение состояния в одном тесте может повлиять на другие тесты — Может привести к конфликтам данных, если тесты выполняют изменения в общем ресурсе |
Когда тесты в одном файле запускаются параллельно, выбор scope: test
или scope: worker
сильно влияет на производительность и стабильность тестов.
Со scope: test
можно избежать конфликтов данных между тестами. Но чрезмерное и неоправданное использование опции может привести к увеличению нагрузки на систему, потому что будет создаваться свой экземпляр ресурса для каждого теста.
Например, если создание подключения к базе данных занимает 500 мс, а в файле 4 теста, которые выполняются параллельно, общее время на инициализацию фикстур будет 2 секунды (4 × 500 мс).
Со scope: worker
можно существенно ускорить выполнение тестов, так как фикстура создается один раз на worker. Например, если подключение к базе данных занимает 500 мс, оно создается только один раз, независимо от количества тестов, что дает преимущество в производительности. Но все тесты будут делить один и тот же ресурс, что может привести к конфликтам и мутации данных.
В итоге, зная об особенностях каждой опции, можно умело их сочетать, добиваясь высокого результата стабильности и скорости выполнения тестов.
Порядок выполнения фикстур с auto: true
Фикстуры с опцией auto: true инициализируются автоматически перед началом теста, даже если они не указаны в параметрах тестовой функции.
Последовательность выполнения:
Фикстуры с auto: true и scope:
worker
инициализируются перед фикстурами с scope:test
.Если есть несколько фикстур с одинаковым scope, они инициализируются в порядке определения.
При наличии зависимостей между фикстурами порядок инициализации определяется графом зависимостей.
Пример:
export const test = base.test.extend({
fixtureA: [async ({}, use) => {
console.log('Инициализация fixtureA');
await use();
console.log('Очистка fixtureA');
}, { auto: true }],
fixtureB: [async ({ fixtureA }, use) => {
console.log('Инициализация fixtureB');
await use();
console.log('Очистка fixtureB');
}, { auto: true }],
fixtureC: async ({}, use) => {
console.log('Инициализация fixtureC');
await use();
console.log('Очистка fixtureC');
},
});
В тесте:
test('Тест с фикстурами', async ({ fixtureC }) => {
console.log('Выполнение теста');
});
Вывод в консоль при выполнении теста:

Объяснение:
Сначала инициализируются fixtureA и fixtureB, так как у них auto: true.
fixtureB зависит от fixtureA, поэтому fixtureA инициализируется первой.
Затем инициализируется fixtureC, так как она указана в параметрах теста.
После завершения теста фикстуры очищаются в обратном порядке.
Недостатки использования фикстур
Фикстуры — довольно сложный инструмент, и их неосмотрительное использование может привести к нежелательным последствиям. Несмотря на все преимущества, фикстуры имеют недостатки, которые важно учитывать.
Скрытая сложность и неявность. Логика инициализации и очистки может быть скрыта внутри фикстур, что затрудняет понимание полного потока выполнения теста.
Новым членам команды может быть сложно понять, что происходит в тесте, без глубокого изучения фикстур. Чтобы минимизировать эти проблемы, обязательно нужно документировать фикстуры, описывая их предназначение и использование, и использовать осмысленные имена фикстур, отражающие их функциональность. Нужно указывать фикстуры в параметрах теста, даже если у них auto: true, чтобы сделать зависимости явными.
Сложность отладки. При возникновении ошибок может быть сложно определить, в какой именно фикстуре или на каком этапе произошла проблема. Также фикстуры часто используют асинхронный код, что может усложнить отладку.
Чтобы избежать этих проблем, добавляется логирование внутри фикстур, чтобы отслеживать их выполнение и состояние. Нужно писать отдельные тесты для фикстур, особенно если они содержат сложную логику.
Потенциальное дублирование и конфликты. При неправильной организации фикстур может возникнуть дублирование логики. Стоит обращать внимание на то, что при объединении фикстур из разных файлов возможны конфликты имен.
На проекте стоит организовать фикстуры по файлам и модулям, избегая дублирования. Использовать уникальные и понятные имена для фикстур и следить за тем, какие фикстуры импортируются и объединяются в каждом тесте.
Производительность. Фикстуры с auto: true могут инициализироваться, даже если они не нужны в тесте, что может привести к дополнительным затратам времени. Использование scope: 'test' для ресурсоемких фикстур может замедлить выполнение тестов.
Чтобы не допустить падения производительности, стоит анализировать, какие фикстуры действительно нужны в тесте, и избегать лишней инициализации. Важно выбирать область действия фикстур в зависимости от их характера и влияния на производительность. Если возможно, используйте кэширование или переиспользование ресурсов внутри фикстур.
Итоги
Фикстуры в Playwright — мощный инструмент для организации и оптимизации ваших тестов. Они позволяют:
Повысить читаемость и поддерживаемость кода. Тесты становятся более чистыми и фокусируются на логике проверки.
Уменьшить дублирование кода. Общая логика и настройки выносятся в фикстуры.
Управлять жизненным циклом ресурсов. Playwright автоматически инициализирует и очищает фикстуры.
Гибко настраивать поведение тестов. Используя опции фикстур и их комбинации, вы можете адаптировать тесты под разные сценарии.
Обеспечить изоляцию и стабильность. Правильное использование scope и фикстур помогает избежать проблем при параллельном выполнении тестов.
Важно внимательно подходить к организации фикстур, документировать их и следить за тем, чтобы они облегчали, а не усложняли процесс тестирования.
Рекомендации:
Планировать структуру. Продумать организацию фикстур с самого начала, особенно в больших проектах.
Обучать команду. Делиться знаниями о фикстурах с коллегами, проводить код-ревью и обсуждения.
Следить за производительностью. Регулярно анализировать время выполнения тестов и оптимизировать фикстуры при необходимости.
Быть гибкими. Использовать различные подходы к объединению и настройке фикстур в зависимости от конкретных задач.
Фикстуры — это инструмент, который при правильном использовании значительно повышает эффективность тестирования. Надеюсь, эта статья помогла вам глубже понять их возможности и вдохновила на их эффективное применение в ваших проектах.
Успешного тестирования и чистого кода!