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

Патерни

SOLID, DRY, KISS у тестах і антипатерни

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 асерти.