Поддержание чистого, эффективного и масштабируемого тестового кода становится всё более сложной задачей по мере увеличения сложности веб-приложений. Playwright, мощный фреймворк для end-to-end тестирования, предлагает решение через свою систему фикстур. Это руководство проведет вас через продвинутые техники использования фикстур в Playwright для создания надёжной и поддерживаемой тестовой архитектуры.

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

Фикстуры в Playwright позволяют делиться данными или объектами между тестами, настраивать предусловия и эффективно управлять тестовыми ресурсами. Они также помогают уменьшить дублирование кода и улучшить организацию тестов.

Данная статья это перевод с английского с некоторыми адаптациями. Перевод сделан совместно с автором тренажера по “Playwright для тестировщика Дмитрием Ереминым

1. Создание фикстур для Page Object

Page Object Model (POM) — это паттерн проектирования, который создаёт слой абстракции между тестовым кодом и кодом, связанным с конкретной страницей. Давайте создадим несколько фикстур для Page Object:

// 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 классы могут использоваться для прямого взаимодействия с backend-сервисами. Вот как создать фикстуры для API классов:

4o

// 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 в Playwright — это мощная функция, которая позволяет делиться ресурсами между несколькими тестовыми файлами в пределах одного рабочего процесса (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 могут использовать одни и те же ресурсы, снижая общее потребление ресурсов.

  • Согласованность: Все тесты в рамках одного worker используют одну и ту же фикстуру, что обеспечивает единое состояние и поведение.

  • Производительность: Повторное использование подключений и инициализированных объектов позволяет тестам выполняться быстрее, чем при настройке этих ресурсов для каждого теста. Фикстуры должны быть корректно завершены после использования.

Лучшие практики при работе с фикстурами на уровне Worker:

  • Используйте worker scope для фикстур, которые требуют значительных затрат на настройку, но могут безопасно использоваться между тестами.

  • Убедитесь, что фикстуры на уровне worker являются безсостоящими или могут быть сброшены между тестами, чтобы избежать зависимости тестов друг от друга.

  • Обращайте внимание на ограничения ресурсов. Совместное использование ресурсов может быть эффективным, но также может привести к истощению ресурсов, если не управлять ими должным образом.

  • Используйте переменные окружения или конфигурационные файлы для управления строками подключения и другими конфиденциальными данными.

Потенциальные проблемы:

  • Изоляция тестов: Убедитесь, что тесты, использующие фикстуры на уровне 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 для действительно затратных операций, которые приносят пользу от совместного использования между тестами.

  • Убедитесь, что фикстура очищает за собой ресурсы, чтобы предотвратить загрязнение тестов.

  • Сделайте фикстуру устойчивой к сбоям, реализовав правильную обработку ошибок и ведение логов.

  • Рассмотрите возможность использования транзакций для операций с базой данных, чтобы легко откатывать изменения после каждого теста.

  • Используйте переменные окружения или конфигурационные файлы для управления строками подключения и другой конфиденциальной информацией.

Применение в реальном мире

В крупномасштабных приложениях можно использовать фикстуру на уровне worker для настройки сложного тестового окружения. Это может включать запуск нескольких сервисов, заполнение базы данных большим количеством тестовых данных или выполнение трудоёмких процессов аутентификации. Выполняя это один раз на 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 объединит переопределения с дефолтными значениями.

Пример из реального мира

В e-commerce приложении у вас могут быть разные типы пользователей (гость, зарегистрированный, премиум) и типов продуктов (физические, цифровые, подписки). Вы можете создать опциональные фикстуры для каждого типа, что позволит вам легко тестировать различные сценарии, например, премиум-пользователь покупает подписку, или гость покупает физический товар.

5. Определение типов TestFixtures и WorkerFixtures

Типизированные фикстуры используют систему типов TypeScript для улучшения автозаполнения, проверки типов и общего опыта разработки при работе с тестами Playwright.

Типизированные фикстуры предоставляют несколько преимуществ:

  • Улучшают полноту кода и снижают количество ошибок благодаря статической проверке типов в 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-клиентов и моделей данных. Используя типизированные фикстуры, вы можете гарантировать, что все части вашего тестового набора работают корректно. Например, вы можете создать сложный end-to-end тест, который симулирует путь пользователя через несколько страниц, взаимодействует с различными 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>({});

Заключение: Лучшие практики эффективного использования фикстур Playwright

  • Модульность фикстур: Создавайте отдельные фикстуры для различных задач (страницы, API, данные и т.д.), чтобы поддерживать организованность и удобство сопровождения тестового кода.

  • Использование правильной области видимости: Для большинства случаев используйте фикстуры с областью теста. Фикстуры с областью worker применяйте для действительно затратных операций настройки.

  • Использование TypeScript: Применяйте типизированные фикстуры для улучшения полноты кода, снижения количества ошибок и улучшения опыта разработки.

  • Баланс гибкости и простоты: Используйте опциональные фикстуры для предоставления данных по умолчанию, но избегайте чрезмерной сложности в настройках. Стремитесь к балансу между гибкостью и удобством использования.

  • Сфокусированные фикстуры: Каждая фикстура должна выполнять одну задачу. Если фикстура выполняет слишком много функций, подумайте о её разделении на более мелкие и целенаправленные фикстуры.

  • Композиция: Комбинируйте различные типы фикстур для создания комплексных тестовых настроек, охватывающих все аспекты вашего приложения.

  • Согласованность: Используйте согласованные соглашения об именах и структуре для фикстур, чтобы улучшить читаемость и сопровождаемость тестового кода.

  • Документирование фикстур: Обеспечьте чёткую документацию для фикстур, особенно для сложных настроек или при работе в больших командах.

  • Регулярный рефакторинг: По мере роста тестового набора регулярно пересматривайте и рефакторьте фикстуры, чтобы они оставались эффективными и действенными.

  • Тестирование фикстур: Для сложных фикстур рассмотрите возможность написания тестов для самих фикстур, чтобы убедиться, что они работают так, как ожидается.

Следуя этим практикам и используя весь потенциал фикстур Playwright, вы сможете создать надёжный, поддерживаемый и эффективный тестовый набор, который будет развиваться вместе с вашим приложением.

Удачного тестирования!

Данная статья это перевод с английского с некоторыми адаптациями. Перевод сделан совместно с автором тренажера по “Playwright для тестировщика Дмитрием Ереминым

Комментарии (0)