SOLID у тестовому фреймворку
SOLID застосовний до тестового коду, але з поправкою на мету: тести мають бути простими і читабельними.
- S, Single Responsibility. Page Object описує UI, фабрика створює дані, API-клієнт ходить в API, тест перевіряє. Page Object, який ще й створює користувача через БД, порушує SRP.
- O, Open/Closed. Новий спосіб логіну додається новою стратегією, а не правкою
switch у десяти місцях. - L, Liskov. Якщо
AdminPage extends BasePage, то всюди, де очікують BasePage, AdminPage має працювати коректно. Глибокі ієрархії сторінок часто це порушують. - I, Interface Segregation. Не роби один
ApiClient на 200 методів. Краще PaymentsApi, UsersApi. - D, Dependency Inversion. Тест залежить від абстракції (фікстура
loggedIn), а не від конкретного способу логіну. У Playwright це природно реалізують фікстури.
Головне застереження: SOLID у тестах не самоціль. Інтерфейс з однією реалізацією і трьома шарами абстракції для тесту на 5 рядків робить код гіршим.
На співбесіді: Найважливіші для мене в тестах SRP і Dependency Inversion: кожен клас робить одну річ, а тести отримують залежності через фікстури. Решту застосовую, коли є реальна варіативність, а не заради шаблону.
DRY vs DAMP і KISS
DRY (Don't Repeat Yourself) каже прибирати дублювання. DAMP (Descriptive And Meaningful Phrases) каже, що тест має читатися сам по собі, навіть ціною повторів.
У тестах треба балансувати:
- DRY для інфраструктури: локатори, кроки, фікстури, фабрики.
- DAMP для самого тесту: вхідні дані і очікування мають бути видно в тілі тесту.
Погано (занадто DRY):
test('limit', async ({ transferPage }) => {
await runScenario(transferPage, SCENARIOS[3]);
});
Щоб зрозуміти, що перевіряється, треба відкрити два файли. Краще:
test('blocks transfer above daily limit', async ({ transferPage }) => {
await transferPage.fill('UA002', 50_001);
await transferPage.confirm();
await expect(transferPage.error).toHaveText('Daily limit exceeded');
});
KISS (Keep It Simple) і YAGNI: не будувати абстракцію "на майбутнє". Три тести з однаковими двома рядками ще не причина для нового шару. Логіка в тестах (if, цикли з умовами) теж сигнал: тест має бути лінійним.
Параметризація через цикл for над масивом кейсів з test() всередині нормальна, якщо кожен кейс має зрозумілу назву.
На співбесіді: Я застосовую DRY до інфраструктури, а тести пишу в стилі DAMP, щоб дані і очікування було видно без переходів у інші файли. Абстракцію додаю, коли дублювання стало реальною проблемою, а не наперед.
Типові антипатерни у фреймворках
Список, який варто знати і вміти пояснити:
- God Page Object і God Helper:
utils.ts на 2000 рядків, куди скидають усе. - Глибоке наслідування сторінок і тестів замість композиції.
- Спільний мутабельний стан між тестами: змінні модуля, порядок тестів як залежність. У Playwright тести мають бути ізольовані, бо йдуть паралельно і можуть перезапускатися.
- Жорсткі очікування
waitForTimeout замість web-first асертів і auto-waiting. - Асерти без мети в хелперах: хелпер мовчки перевіряє десять речей, і незрозуміло, що впало.
- Обгортки над обгортками: свій
click(), що викликає locator.click() без додаткової цінності. Він ховає можливості API і заважає трейсу. - Логіка в тестах:
if (await el.isVisible()) робить тест недетермінованим. - Тестові дані-константи, які мутують тести.
- Абстракція на виріст: Screenplay, інтерфейси і фабрики для 20 тестів.
Як говорити про це на співбесіді: не просто перелічити, а дати приклад з досвіду і що зробили. Наприклад: "У нас був BasePage з 60 методами, ми винесли хедер і таблиці в компоненти, а логін переробили на storageState. Час прогону зменшився, а конфліктів у PR стало менше."
На співбесіді: Найчастіші антипатерни, які я бачив, це god objects, глибоке наслідування, спільний стан між тестами і жорсткі таймаути. Я виправляв їх через компоненти, фікстури, ізольовані дані і web-first асерти.