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 лишаю для простих локальних кроків у межах одного файлу.