QA Interview Hub · База знань · Тести

Патерни

Фікстури як Dependency Injection

Фікстури: DI вбудований у Playwright Test

Dependency Injection означає, що об'єкт не створює свої залежності сам, а отримує їх ззовні. У Playwright Test це реалізовано через фікстури: тест оголошує в аргументах, що йому потрібно, а раннер створює, передає і потім прибирає.

page, context, browser, request це вбудовані фікстури. Свої додаються через test.extend:

import { test as base } from '@playwright/test';
import { LoginPage } from './pages/login-page';
import { PaymentsPage } from './pages/payments-page';

type Pages = { loginPage: LoginPage; paymentsPage: PaymentsPage };

export const test = base.extend<Pages>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
  paymentsPage: async ({ page }, use) => {
    await use(new PaymentsPage(page));
  },
});
export { expect } from '@playwright/test';
test('archived payment visible', async ({ paymentsPage }) => {
  await paymentsPage.goto();
  await expect(paymentsPage.archived.rowByText('INV-1')).toBeVisible();
});

Ключові властивості: фікстури ліниві (створюються тільки якщо тест їх запросив), можуть залежати від інших фікстур, код до use це setup, після use це teardown, і teardown виконується навіть якщо тест упав. Кілька наборів фікстур з різних модулів можна об'єднати через mergeTests.

На співбесіді: Фікстури в Playwright це вбудований DI: тест просить залежність в аргументах, раннер її створює і прибирає. Я реєструю Page Objects і клієнтів API як фікстури, тому в тестах немає new і ручного cleanup.

Scope, auto і setup/teardown

Фікстура може мати scope test (за замовчуванням, нова на кожен тест) або worker (одна на воркер-процес, живе між тестами цього воркера).

type WorkerFx = { apiToken: string };
type TestFx = { account: Account };

export const test = base.extend<TestFx, WorkerFx>({
  apiToken: [async ({}, use) => {
    const token = await fetchServiceToken();
    await use(token);
  }, { scope: 'worker' }],

  account: async ({ request, apiToken }, use) => {
    const acc = await createAccount(request, apiToken);
    await use(acc);
    await deleteAccount(request, apiToken, acc.id);
  },
});

Правила, які питають на співбесідах:

  • Worker-фікстура не може залежати від test-фікстури (наприклад, від page), бо живе довше.
  • Worker-фікстури підходять для дорогих і незмінних ресурсів: токен, підключення до БД, тестовий користувач на воркер.
  • { auto: true } запускає фікстуру для кожного тесту, навіть якщо її не запросили. Підходить для логування, збору консольних помилок. Зловживати не варто, бо це прихована магія.
  • Порядок teardown зворотний до setup.

Коли НЕ варто: перетворювати фікстуру на сценарій з десятком кроків. Фікстура готує середовище, а сам сценарій має бути видно в тесті.

На співбесіді: Дорогі незмінні ресурси я роблю worker-фікстурами, а все, що тест може змінити, test-фікстурами з cleanup після use. Auto-фікстури використовую тільки для наскрізних речей на кшталт логування.

Фікстури чи базовий клас тесту

У Java/Selenium світі стандарт це BaseTest з @BeforeEach, де створюється драйвер і всі сторінки. Кандидати часто переносять це в Playwright, і це помилка.

Чому фікстури кращі:

  • Лінивість. Базовий клас створює все для всіх тестів. Фікстура створюється лише для тесту, якому вона потрібна.
  • Ізоляція. Кожен тест отримує свої екземпляри, немає спільних полів класу, які можна випадково змінити.
  • Гарантований teardown навіть при падінні тесту.
  • Композиція. Фікстури комбінуються через extend і mergeTests, а базовий клас можна наслідувати лише один.
  • Типізація. TypeScript знає типи всіх фікстур в аргументах.

beforeEach теж має місце: для простого кроку, специфічного для одного файлу (відкрити потрібну сторінку). Але якщо той самий beforeEach скопійовано в 10 файлів, це кандидат у фікстуру.

Типова помилка: оголосити let loginPage: LoginPage на рівні describe і заповнювати в beforeEach. Це працює, але змінна модуля легко стає спільним станом, а TypeScript не гарантує, що вона ініціалізована.

На співбесіді: У Playwright я не роблю BaseTest, бо фікстури дають ліниве створення, ізоляцію, гарантований cleanup і композицію. beforeEach лишаю для простих локальних кроків у межах одного файлу.