Поддерживать чистый, эффективный и масштабируемый тестовый код становится сложнее по мере усложнения веб-приложений. Playwright, фреймворк для комплексного тестирования, предлагает решение этой проблемы с помощью своей системы фикстур (можно понимать как «приспособления», «связки», «прикрепления», или «предопределенные компоненты»). В этом гайде вы узнаете о передовых методах использования фикстур Playwright для создания надежной и удобной в обслуживании тестовой архитектуры.
Предназначение
Фикстуры в Playwright позволяют обмениваться данными или объектами между тестами, устанавливать в коде предусловия, и эффективно управлять тестовыми ресурсами. Они также помогают сократить дублирование кода и упорядочить структуру тестов.
1. Создание фикстур для объектов страниц
Модели объектов страниц (POM) — паттерн проектирования, который создает слой абстракции между тестовым кодом и кодом веб-страницы.
Пример создания нескольких фикстур для объектов страницы:
// pages/login.page.ts import { Page } from '@playwright/test'; export class LoginPage { constructor(private page: Page) {} async login(username: string, password: string) { await this.page.fill('#username', username); await this.page.fill('#password', password); await this.page.click('#login-button'); } } // pages/dashboard.page.ts import { Page } from '@playwright/test'; export class DashboardPage { constructor(private page: Page) {} async getUserName() { return this.page.textContent('.user-name'); } } // fixtures.ts import { test as base } from '@playwright/test'; import { LoginPage } from './pages/login.page'; import { DashboardPage } from './pages/dashboard.page'; export const test = base.extend<{ loginPage: LoginPage; dashboardPage: DashboardPage; }>({ loginPage: async ({ page }, use) => { await use(new LoginPage(page)); }, dashboardPage: async ({ page }, use) => { await use(new DashboardPage(page)); }, });
2. Создание фикстур для API-классов
API-классы можно использовать для прямого взаимодействия с бэкэнд-сервисами. Пример:
// api/user.api.ts import { APIRequestContext } from '@playwright/test'; export class UserAPI { constructor(private request: APIRequestContext) {} async createUser(userData: any) { return this.request.post('/api/users', { data: userData }); } } // api/product.api.ts import { APIRequestContext } from '@playwright/test'; export class ProductAPI { constructor(private request: APIRequestContext) {} async getProducts() { return this.request.get('/api/products'); } } // fixtures.ts import { test as base } from '@playwright/test'; import { UserAPI } from './api/user.api'; import { ProductAPI } from './api/product.api'; export const test = base.extend<{ userAPI: UserAPI; productAPI: ProductAPI; }>({ userAPI: async ({ request }, use) => { await use(new UserAPI(request)); }, productAPI: async ({ request }, use) => { await use(new ProductAPI(request)); }, });
3. Создание вспомогательных фикстур на уровне worker’ов
Это мощная функция, позволяющая совместно использовать ресурсы нескольких тестовых файлов в одном worker-процессе. Эти фикстуры особенно полезны для операций, которые требуют больших затрат на настройку, и могут быть повторно использованы в нескольких тестах, например подключения к базе данных или генераторы тестовых данных.
Пример, как создавать и применять вспомогательные фикстуры на уровне worker’ов:
// helpers/database.helper.ts import { Pool } from 'pg'; export class DatabaseHelper { private pool: Pool; async connect() { this.pool = new Pool({ user: process.env.DB_USER, host: process.env.DB_HOST, database: process.env.DB_NAME, password: process.env.DB_PASSWORD, port: parseInt(process.env.DB_PORT || '5432'), }); } async query(sql: string, params: any[] = []) { if (!this.pool) { throw new Error('Database not connected. Call connect() first.'); } const client = await this.pool.connect(); try { const result = await client.query(sql, params); return result.rows; } finally { client.release(); } } async disconnect() { if (this.pool) { await this.pool.end(); } } } // helpers/test-data-generator.ts import { faker } from '@faker-js/faker'; export class TestDataGenerator { async init() { // Any initialization logic here console.log('TestDataGenerator initialized'); } generateUser() { return { name: faker.person.fullName(), email: faker.internet.email(), password: faker.internet.password(), }; } generateProduct() { return { name: faker.commerce.productName(), price: parseFloat(faker.commerce.price()), category: faker.commerce.department(), }; } } // fixtures.ts import { test as base } from '@playwright/test'; import { DatabaseHelper } from './helpers/database.helper'; import { TestDataGenerator } from './helpers/test-data-generator'; export const test = base.extend< {}, { dbHelper: DatabaseHelper; testDataGen: TestDataGenerator; } >({ dbHelper: [async ({}, use) => { const dbHelper = new DatabaseHelper(); await dbHelper.connect(); await use(dbHelper); await dbHelper.disconnect(); }, { scope: 'worker' }], testDataGen: [async ({}, use) => { const testDataGen = new TestDataGenerator(); await testDataGen.init(); await use(testDataGen); }, { scope: 'worker' }], });
Пояснения и лучшие практики
Фикстуры уровня worker дают несколько преимуществ:
- Эффективность: Дорогостоящие операции по настройке (например, подключение к базе данных) выполняются один раз для каждого worker’а, а не для каждого теста.
- Совместное использование ресурсов: Несколько тестов в одном рабочем worker’е могут совместно использовать одни и те же ресурсы, что снижает общее потребление ресурсов.
- Согласованность: Все тесты внутри рабочего используют один и тот же экземпляр фикстуры, обеспечивая согласованное состояние и поведение.
- Производительность: Благодаря повторному использованию соединений и инициализированных объектов, тесты могут выполняться быстрее, чем при отдельной настройке этих ресурсов для каждого теста. После использования фикстуры должны быть удалены («разрушены» teardown-процедурой).
Лучшие практики:
- Используйте уровень worker для фикстур, которые требуют больших усилий на настройку, но могут безопасно использоваться совместно с другими тестами.
- Убедитесь, что фикстуры не имеют определенных состояний или могут они быть сброшены между тестами, чтобы предотвратить взаимозависимость тестов.
- Помните об ограничениях ресурсов. Хотя совместное использование ресурсов может быть эффективным, при неправильном управлении оно может привести к их исчерпанию.
- Используйте переменные окружения или файлы конфигурации для управления строками соединений и другими конфиденциальными данными.
Потенциальные подводные камни
- Изоляция тестов: Убедитесь, что тесты, использующие фикстуры уровня worker, не влияют друг на друга, изменяя общее состояние.
- Утечки ресурсов: Правильно управляйте ресурсами на этапе разрушения фикстур, чтобы предотвратить утечки.
Пример, как можно использовать в тесте такие фикстуры:
// user.spec.ts import { test } from './fixtures'; import { expect } from '@playwright/test'; test.describe('User management', () => { test('list users', async ({ page, dbHelper }) => { // The database is already connected and seeded with test data await page.goto('/users'); const userCount = await page.locator('.user-item').count(); expect(userCount).toBeGreaterThan(0); }); test('create new user', async ({ page, dbHelper }) => { await page.goto('/users/new'); await page.fill('#name', 'New User'); await page.fill('#email', 'newuser@example.com'); await page.click('#submit'); // Verify the user was created in the database const result = await dbHelper.client.query('SELECT * FROM users WHERE email = $1', ['newuser@example.com']); expect(result.rows.length).toBe(1); }); });
Лучшие практики
- Используйте такие фикстуры для действительно затратных операций.
- Убедитесь, что фикстура сама «убирает за собой», чтобы предотвратить «загрязнение» тестов.
- Делайте фикстуры устойчивыми к сбоям, реализовав надлежащую обработку ошибок и ведение логов.
- Рассмотрите возможность использования транзакций для операций с базой данных, чтобы легко откатывать изменения после каждого теста.
- Используйте переменные среды или файлы конфигурации для управления строками соединений и другими конфиденциальными данными.
Реальное применение
В крупномасштабном приложении для создания сложной тестовой среды. Это может включать запуск нескольких служб, заполнение базы данных большим количеством тестовых данных, или выполнение трудоемких процессов аутентификации. Выполняя это один раз для каждого worker’а, можно значительно сократить общее время выполнения тестового набора.
4. Создание опциональных фикстур для данных
Дополняющие фикстуры для данных позволяют определить тестовые данные по умолчанию, которые могут быть пропущены в некоторых тестах. Такая гибкость позволяет создать некую «базовую линию» для тестов и при этом учитывать особые случаи.
Дополнительные фикстуры для данных дают несколько преимуществ:
- Предоставляют «дефолтные» тестовые данные, снижая необходимость настройки данных в отдельных тестах
- Позволяют легко переопределять данные для конкретных тест-кейсов
- Улучшают читаемость тестов, отделяя тестовые данные от логики тестов
- Позволяют легко управлять различными сценариями данных в тестовом наборе
Расширим наш предыдущий пример и создадим более полную дополняющую фикстуру для данных:
// types/user.ts export interface User { username: string; password: string; email: string; role: 'admin' | 'user'; } // fixtures.ts import { test as base } from '@playwright/test'; import { User } from './types/user'; export const test = base.extend<{ testUser?: User; }>({ testUser: [async ({}, use) => { await use({ username: 'defaultuser', password: 'defaultpass123', email: 'default@example.com', role: 'user' }); }, { option: true }], });
Теперь используем эту фикстуру в тестах:
// user.spec.ts import { test } from './fixtures'; import { expect } from '@playwright/test'; test.describe('User functionality', () => { test('login with default user', async ({ page, testUser }) => { await page.goto('/login'); await page.fill('#username', testUser.username); await page.fill('#password', testUser.password); await page.click('#login-button'); expect(page.url()).toContain('/dashboard'); }); test('admin user can access admin panel', async ({ page, testUser }) => { test.use({ testUser: { username: 'adminuser', password: 'adminpass123', email: 'admin@example.com', role: 'admin' } }); await page.goto('/login'); await page.fill('#username', testUser.username); await page.fill('#password', testUser.password); await page.click('#login-button'); await page.click('#admin-panel'); expect(page.url()).toContain('/admin'); }); });
Лучшие практики
- Используйте опциональные фикстуры для данных, которые часто используются в тестах, но могут нуждаться в небольших изменениях.
- Дефолтные данные должны быть простыми и стандартными. Используйте переопределения в конкретных сценариях.
- Рассмотрите возможность создания нескольких опциональных фикстур для разных категорий данных (например,
testUser
,testProduct
,testOrder
). - Применяйте интерфейсы TypeScript, чтобы обеспечить безопасность типов тестовых данных.
- При переопределении фикстур указывайте только те свойства, которые необходимо изменить. Playwright объединит переопределения с дефолтными значениями.
Реальное применение
В приложении электронной коммерции могут быть различные типы пользователей (гость, зарегистрированный, премиум) и типы товаров (физические, цифровые, по подписке). Вы можете создать дополнительные фикстуры для каждого из них, что позволит легко тестировать различные сценарии, такие как покупка премиум-пользователем продукта по подписке или покупка гостем физического товара.
5. Определение типов TestFixtures и WorkerFixtures
Типизированные фикстуры используют систему типов TypeScript для улучшения автозаполнения, проверки типов и удобства разработчиков при работе с тестами.
Типизированные фикстуры имеют ряд преимуществ:
- Улучшают полноту кода и уменьшают количество ошибок благодаря статической проверке типов TypeScript
- Улучшают работу в IDE благодаря автозаполнению и возможностям рефакторинга
- Служат в качестве документации, позволяя понять, какие свойства и методы доступны в каждой фикстуре
- Позволяют легко создавать сложные тестовые наборы благодаря пересечению типов
Создадим более полный набор с помощью типизированных фикстур:
// types.ts import { LoginPage, ProductPage, CheckoutPage } from './pages'; import { UserAPI, ProductAPI, OrderAPI } from './api'; import { DatabaseHelper } from './helpers/database.helper'; import { User, Product, Order } from './models'; export interface PageFixtures { loginPage: LoginPage; productPage: ProductPage; checkoutPage: CheckoutPage; } export interface APIFixtures { userAPI: UserAPI; productAPI: ProductAPI; orderAPI: OrderAPI; } export interface HelperFixtures { dbHelper: DatabaseHelper; } export interface DataFixtures { testUser?: User; testProduct?: Product; testOrder?: Order; } export interface TestFixtures extends PageFixtures, APIFixtures, DataFixtures {} export interface WorkerFixtures extends HelperFixtures {} // basetest.ts import { test as base } from '@playwright/test'; import { TestFixtures, WorkerFixtures } from './types'; export const test = base.extend<TestFixtures & WorkerFixtures>({ // Implement your fixtures here }); // playwright.config.ts import { defineConfig } from '@playwright/test'; import { TestFixtures, WorkerFixtures } from './types'; export default defineConfig<TestFixtures, WorkerFixtures>({ use: { baseURL: 'http://localhost:3000', testUser: { username: 'defaultuser', password: 'defaultpass123', email: 'default@example.com', role: 'user' }, // Other default fixture values }, // ... other config options });
Теперь при написании тестов вы получаете полную поддержку типов:
// checkout.spec.ts import { test } from './basetest'; import { expect } from '@playwright/test'; test('complete checkout process', async ({ page, loginPage, productPage, checkoutPage, testUser, testProduct, orderAPI }) => { await loginPage.login(testUser.username, testUser.password); await productPage.addToCart(testProduct.id); await checkoutPage.completeCheckout(); const latestOrder = await orderAPI.getLatestOrderForUser(testUser.id); expect(latestOrder.status).toBe('completed'); });
Лучшие практики
- Определите четкие и отдельные интерфейсы для различных типов фикстур (страница, API, данные и т. д.).
- Используйте пересечение типов для создания сложных фикстур.
- Используйте полезные типы TypeScript (например,
Partial<T>
илиPick<T>
) при определении дополнительных или вложенных фикстур. - Приводите определения типов в соответствие с реальными реализациями.
- Используйте строгие настройки TypeScript, чтобы получить максимальную пользу от проверки типов.
Реальное применение
В крупномасштабном приложении могут быть десятки объектов страниц, клиентов API и моделей данных. Используя типизированные фикстуры, вы можете убедиться, что все части тестового набора работают правильно. Например, можно создать сложный сквозной тест, имитирующий проход пользователя по нескольким страницам, взаимодействующий с различными API и проверяющий результаты в базе данных, и все это с полной безопасностью типов и поддержкой автозаполнения.
Сочетание различных типов фикстур
Одним из наиболее мощных аспектов фикстур в Playwright является возможность комбинировать различные типы для создания сложных тестовых наборов. Пример объединения различных типов фикстур:
// fixtures.ts import { test as base } from '@playwright/test'; import { LoginPage, DashboardPage } from './pages'; import { UserAPI, ProductAPI } from './api'; import { DatabaseHelper } from './helpers/database.helper'; import { User, Product } from './types'; type TestFixtures = { loginPage: LoginPage; dashboardPage: DashboardPage; userAPI: UserAPI; productAPI: ProductAPI; testUser?: User; testProduct?: Product; }; type WorkerFixtures = { dbHelper: DatabaseHelper; }; export const test = base.extend<TestFixtures, WorkerFixtures>({ // Page object fixtures loginPage: async ({ page }, use) => { await use(new LoginPage(page)); }, dashboardPage: async ({ page }, use) => { await use(new DashboardPage(page)); }, // API fixtures userAPI: async ({ request }, use) => { await use(new UserAPI(request)); }, productAPI: async ({ request }, use) => { await use(new ProductAPI(request)); }, // Optional data fixtures testUser: [async ({}, use) => { await use({ id: '1', username: 'testuser', email: 'test@example.com' }); }, { option: true }], testProduct: [async ({}, use) => { await use({ id: '1', name: 'Test Product', price: 9.99 }); }, { option: true }], // Worker-scoped helper fixture dbHelper: [async ({}, use) => { const helper = new DatabaseHelper(); await helper.connect(); await helper.resetDatabase(); await use(helper); await helper.disconnect(); }, { scope: 'worker' }], });
Теперь вы можете писать более сложные тесты:
// e2e.spec.ts import { test } from './fixtures'; import { expect } from '@playwright/test'; test('user can purchase a product', async ({ loginPage, dashboardPage, userAPI, productAPI, testUser, testProduct, dbHelper }) => { // Create a new user const user = await userAPI.createUser(testUser); // Log in await loginPage.login(user.username, 'password123'); // Add product to cart await dashboardPage.addToCart(testProduct.id); // Complete purchase await dashboardPage.completePurchase(); // Verify purchase in database const dbOrder = await dbHelper.getLatestOrderForUser(user.id); expect(dbOrder.productId).toBe(testProduct.id); // Verify product stock updated const updatedProduct = await productAPI.getProduct(testProduct.id); expect(updatedProduct.stock).toBe(testProduct.stock - 1); });
Бонус
Объединение тестовых и worker-фикстур
Теперь объединим наши тестовые и worker-фикстуры:
// fixtures.ts import { test as base, mergeTests } from '@playwright/test'; import { TestFixtures, WorkerFixtures } from './types'; const testFixtures = base.extend<TestFixtures>({ // ... test fixtures implementation }); const workerFixtures = base.extend<WorkerFixtures>({ // ... worker fixtures implementation }); export const test = mergeTests(testFixtures, workerFixtures);
Расширение базовых тестов с помощью типов TestFixture и WorkerFixture
Чтобы обеспечить правильную типизацию для тестов, мы можем расширить базовый тест:
// basetest.ts import { test as baseTest } from './fixtures.ts'; import { TestFixtures, WorkerFixtures } from './types'; export const test = baseTest.extend<TestFixtures, WorkerFixtures>({});
Заключение: Лучшие практики использования фикстур
- Делайте свои фикстуры модульными: Создавайте отдельные фикстуры для различных сущностей (страницы, API, данные и т. д.), чтобы сохранить порядок в тестах и удобство сопровождения.
- Используйте соответствующий масштаб: В большинстве случаев используйте фикстуры уровня теста, а фикстуры уровня worker оставляйте для действительно затратных операций по настройке.
- Пишите на TypeScript: Используйте типизированные фикстуры, чтобы улучшить полноту кода, уменьшить количество ошибок и повысить удобство.
- Соблюдайте баланс между гибкостью и простотой: Используйте опциональные фикстуры для дефолтных данных, но не усложняйте настройку. Стремитесь к балансу.
- Фикстуры должны быть фокусированными: Каждая фикстура должна отвечать за одну задачу. Если фикстура выполняет слишком много задач, подумайте о том, чтобы разбить ее на более мелкие, узконаправленные.
- Сочетайте: Используйте различные типы фикстур, чтобы создать комплексные тестовые наборы, охватывающие все аспекты приложения.
- Поддерживайте согласованность: Используйте соглашения об именовании и упорядочивайте структуру ваших фикстур, чтобы сделать тестовый код более читаемым и удобным для сопровождения.
- Документируйте фикстуры: Обеспечьте четкую документацию для своих фикстур, особенно для сложных наборов или при работе в больших командах.
- Регулярный рефакторинг: По мере роста тестового набора регулярно проверяйте и ваши фикстуры и проводите рефакторинг.
- Тестируйте ваши фикстуры: Для сложных фикстур следует написать тесты для самих фикстур, чтобы сохранять эффективность.