vyvchy
    Теми розділу

    06 · Автоматизація: стратегія

    Патерни в автоматизації: Screenplay, Builder, Factory, Facade

    Зміст

    Ти відкриваєш чужий тестовий проєкт і бачиш класи LoginTask, UserBuilder, BrowserFactory. Три незнайомі імені — але якщо ти знаєш патерни, то вже здогадуєшся, що кожен робить, ще не читаючи коду. У цьому і сенс патерна: це спільна мова, якою інженери називають рішення, що повторюються. На співбесіді на AQA чи SDET патерни питають не тому, що чекають переказу класичної книжки напам'ять, а щоб перевірити, чи відрізняєш ти «додав абстракцію, бо була реальна біль» від «додав абстракцію, бо так у книжці написано».

    Ця глава — про чотири патерни, які справді трапляються в автоматизації (Screenplay, Builder, Factory, Facade), про Singleton, з яким найчастіше стріляють собі в ногу, і про головну пастку — патерн заради патерну. Page Object ми вже розібрали окремо (/avtomatyzatsiia/page-object-vid-klasyky-do-komponentiv); тут дивимось на нього як на точку відліку.

    Патерн — це словник, а не готовий код

    Термін «патерн проєктування» (design pattern) прийшов не з програмування, а з архітектури: Крістофер Александер описував повторювані рішення для будівель і міст. У 1994-му «банда чотирьох» (Gang of Four, GoF) — Гамма, Гелм, Джонсон і Вліссідес — перенесла ідею в ООП, зібравши 23 патерни у три групи:

    ГрупаЩо вирішуєПриклади з глави
    Породжувальні (creational)як створювати об'єктиBuilder, Factory, Singleton
    Структурні (structural)як складати об'єкти у більші структуриFacade
    Поведінкові (behavioral)як об'єкти взаємодіють між собоюScreenplay близький сюди

    Головне непорозуміння новачка: патерн — це не бібліотека і не шматок коду, який можна заімпортувати. Патерн — це назва для рішення в певному контексті. Коли ти кажеш «зробимо через Builder», колега розуміє задум за одне слово, і це вже цінність. Screenplay, до речі, у GoF немає взагалі: він народився саме в тест-автоматизації, тому його часто плутають з класичними патернами.

    І одразу застереження, до якого повернемось наприкінці: патерн має з'являтися як відповідь на біль — дублювання, крихкість, конструктор на п'ятнадцять параметрів, — а не тому, що «так правильно». Патерн без проблеми, яку він розв'язує, — це чиста складність без вигоди. Про цю межу — глави про DRY/KISS/YAGNI (/avtomatyzatsiia/pryntsypy-dyzainu-testovoho-kodu-dry-kiss-yagni-kompozytsiia) і про SOLID як overengineering (/avtomatyzatsiia/solid-u-test-freimvorku).

    Screenplay проти Page Object

    Page Object групує локатори й дії навколо сторінки. Це працює, поки сторінок мало. На великих сюїтах POM повзе в бік god object: клас на півтори тисячі рядків, глибокі ієрархії BasePage, дублювання між схожими екранами. Screenplay (інша історична назва — Journey) пропонує інший центр тяжіння — не сторінку, а актора (actor), тобто користувача з його намірами. Патерн виріс із доповідей Ентоні Маркано (Antony Marcano) й Енді Палмера (Andy Palmer) і реалізований у фреймворках родини Serenity (Serenity/JS, Serenity BDD).

    Модель складається з п'яти понять:

    • Актор (actor) — хто діє: конкретна персона чи роль (admin, guest).
    • Здібності (abilities) — чим актор володіє: BrowseTheWeb, CallAnApi. Це обгортки над інструментами.
    • Задачі (tasks) — бізнес-дії високого рівня («залогінитись», «оформити замовлення»), складені з інших задач та інтеракцій.
    • Інтеракції (interactions) — атомарні дії: Click, Enter, Navigate.
    • Питання (questions) — читають стан системи, щоб живити перевірки.

    володіє

    виконує

    складені з

    ставить

    Актор

    Здібності
    BrowseTheWeb, CallAnApi

    Задачі
    Login, Checkout

    Інтеракції
    Click, Enter

    Питання
    стан системи

    Перевірки

    володіє

    виконує

    складені з

    ставить

    Актор

    Здібності
    BrowseTheWeb, CallAnApi

    Задачі
    Login, Checkout

    Інтеракції
    Click, Enter

    Питання
    стан системи

    Перевірки

    У коді (стиль Serenity/JS) тест читається майже як сценарій:

    await actor.attemptsTo(
      Navigate.to('/login'),
      Enter.theValue('qa@example.com').into(LoginForm.email),
      Enter.theValue('secret').into(LoginForm.password),
      Click.on(LoginForm.submit),
    );
    
    await actor.attemptsTo(
      Ensure.that(Text.of(Dashboard.heading), equals('Ласкаво просимо')),
    );

    Ключова перевага — композиція. Дрібні інтеракції збираються у задачі, задачі — в більші задачі, і кожна дрібниця перевикористовується:

    const LogIn = {
      as: (email: string, password: string) =>
        Task.where(`#actor логіниться як ${email}`,
          Navigate.to('/login'),
          Enter.theValue(email).into(LoginForm.email),
          Enter.theValue(password).into(LoginForm.password),
          Click.on(LoginForm.submit),
        ),
    };
    
    await actor.attemptsTo(LogIn.as('qa@example.com', 'secret'));

    Чому це взагалі виникло: Screenplay набагато строгіше тягне за принципами SOLID, ніж класичний POM. Кожна задача, інтеракція й питання — маленький клас з однією відповідальністю, а система будується композицією, а не успадкуванням (канон «композиція проти успадкування» — /avtomatyzatsiia/pryntsypy-dyzainu-testovoho-kodu-dry-kiss-yagni-kompozytsiia). Плата за це — більше церемоній і крутіша крива входу.

    АспектPage ObjectScreenplay
    Центр моделісторінкаактор (користувач)
    Основний механізмуспадкування, методи класукомпозиція дрібних задач
    Ролі/персонинезручно, окремі методиприродно: різні актори
    Поріг входунизькийвищий, більше коду
    Ризик god objectвисокийнижчий за побудовою
    Де доречнобільшість проєктіввеликі довговічні сюїти з ролями

    Практичний висновок без фанатизму: більшість команд на Playwright чи CodeceptJS живуть на POM, і це нормальний, достатній вибір. Screenplay виправданий, коли акторів багато (різні ролі й персони), сюїта велика й довговічна, а команда готова платити за структуру. Тягнути Screenplay у проєкт із тридцяти тестів — уже класичний патерн заради патерну.

    Builder: складні дані без конструктора-жаху

    Уяви модель User з п'ятнадцятьма полями. Створювати її в кожному тесті через new User('qa@example.com', 'secret', 'admin', true, null, 'UA', ...) — це шум: тест перелічує пів моделі, хоча йому важливе одне поле, а позиційні аргументи легко переплутати місцями.

    Builder — породжувальний патерн: він будує складний об'єкт крок за кроком і ховає розумні дефолти. У тестах його найчастіша форма — test data builder з fluent-інтерфейсом, де ти задаєш лише те, що важливо саме для цього кейсу:

    const user = aUser()
      .withRole('admin')
      .withEmail('qa@example.com')
      .build();

    Тест каже «мені потрібен адмін», а не диктує всі поля. Дефолти живуть в одному місці — у білдері; додав нове обов'язкове поле в модель — правиш білдер, а не двісті тестів. Fluent-ланцюжок читається зверху вниз і не залежить від порядку аргументів.

    Детально фабрики й білдери тест-даних (faker, ізоляція, підготовка стану) — окрема канонічна глава (/avtomatyzatsiia/test-dani-fabryky-fikstury-izoliatsiia). Тут нам важливий сам патерн і його межа: білдер без розумних дефолтів вироджується у звичайні сеттери й нічого не економить.

    Factory: хто вирішує, що створити

    Factory (фабрика) — родина породжувальних патернів (фабричний метод, абстрактна фабрика). Її суть: інкапсулювати рішення про те, який саме об'єкт створити, за одну дію. Виклик просить «дай мені браузер» чи «дай мені клієнт», а деталі того, який конкретний клас підставити, ховаються всередині фабрики:

    type BrowserName = 'chromium' | 'firefox' | 'webkit';
    
    function browserFor(name: BrowserName): BrowserType {
      switch (name) {
        case 'firefox': return firefox;
        case 'webkit': return webkit;
        default: return chromium;
      }
    }
    
    const browser = await browserFor(process.env.BROWSER as BrowserName).launch();

    Тест (чи фікстура) не знає й не хоче знати про if/else вибору рушія — це рішення інкапсульоване в одному місці.

    Builder і Factory часто плутають, бо обидва «створюють об'єкти». Різниця у питанні, на яке вони відповідають:

    FactoryBuilder
    Питанняякий об'єкт створитияк зібрати складний об'єкт
    Крокиодин крокпокроково, з опціональними полями
    Типовий результатодна з кількох реалізаційодин складний об'єкт
    Маркер виборувибір із варіантів (браузер, клієнт)багато опціональних полів і дефолтів

    Простий орієнтир: багато опціональних полів — бери Builder; треба обрати одну реалізацію з кількох — бери Factory.

    Facade: одна проста дія замість п'яти

    Facade (фасад) — структурний патерн: простий інтерфейс поверх складної підсистеми. Клієнт не знає про п'ять класів усередині — він викликає один зрозумілий метод.

    У тестах фасад присутній усюди, часто неявно. Метод loginPage.loginAs(user), який всередині робить navigate, заповнює два поля, натискає кнопку й чекає редиректу, — це вже фасад. По суті крокові методи Page Object і є фасадами над локаторами. Ставши на цю думку явно, ти можеш зібрати фасад над кількома сторінками:

    class Checkout {
      constructor(private readonly page: Page) {}
    
      async buy(product: string): Promise<void> {
        await new Catalog(this.page).addToCart(product);
        await new Cart(this.page).proceedToCheckout();
        await new Payment(this.page).payWithSavedCard();
      }
    }

    Тест пише await new Checkout(page).buy('Клавіатура') замість десятка кліків — він читається мовою бізнесу, а коли флоу оформлення змінюється, правка живе в одному місці. Небезпека протилежна: фасад-«бог», який знає забагато й тягне пів застосунку в один клас, перетворюється на ту саму god-object проблему, від якої фасад мав рятувати (/avtomatyzatsiia/page-object-vid-klasyky-do-komponentiv).

    Singleton і чому конфіг — небезпечна глобальна змінна

    Singleton — породжувальний патерн: гарантує, що клас має рівно один екземпляр, і дає до нього глобальний доступ. Спокуса очевидна: один драйвер, один конфіг, один логер на весь прогін. Саме тому Singleton — найпопулярніший патерн для пострілу собі в ногу, а найгірший його підвид — глобальний мутабельний конфіг.

    Чому з ним обережно:

    • Ламає ізоляцію. Автотест має бути незалежним (/avtomatyzatsiia/anatomiia-avtotestu-aaa-nezalezhnist-atomarnist). Тест, що змінив глобальний Config.get().baseURL чи поточного юзера, тихо протікає в наступні тести — і ти отримуєш падіння, що залежить від порядку виконання.
    • Вбиває паралель. Під час паралельного запуску воркери — це окремі процеси чи контексти (/avtomatyzatsiia/paralelizatsiia-vorkery-shardinh-masshtabuvannia). Один спільний драйвер-singleton означає, що кілька тестів б'ються за одну вкладку. Плаваючі падіння гарантовані.
    • Ховає залежності. Із сигнатури тесту не видно, що він спирається на глобальний стан. Читаєш тест — і не розумієш, звідки взявся baseURL чи токен.
    • Заважає підміні. Дати одному тесту іншу роль чи інше середовище, коли конфіг глобальний і єдиний, — біль.

    Read-only незмінний конфіг (прочитали змінні оточення раз і більше не чіпаємо) — ще терпимо. А от мутабельний глобальний конфіг — це прямий генератор флаку.

    Правильна відповідь у сучасних фреймворках — впровадження залежностей (dependency injection) через фікстури замість глобальної змінної. У Playwright worker-scoped фікстура дає кероване спільне використання: фреймворк сам вирішує область життя (scope) й ізоляцію, і кожен тест явно оголошує, що йому потрібно.

    export const test = base.extend<{}, { workerConfig: Config }>({
      workerConfig: [async ({}, use) => {
        await use(loadConfig()); // один екземпляр на воркер, без глобальної змінної
      }, { scope: 'worker' }],
    });
    
    test('замовлення видно в кабінеті', async ({ workerConfig }) => {
      // конфіг приходить явно, а не через Config.get()
    });

    Це той самий «один екземпляр на процес», але без глобального доступу: залежність видно в сигнатурі, а область життя контролює ранер.

    Патерн заради патерну

    Найчастіша помилка після знайомства з патернами — тягнути їх усюди. Абстрактний інтерфейс з єдиною реалізацією, фабрика, що обгортає один клас, Screenplay на двадцять тестів, шар «на майбутнє», якого ніхто не просив, — усе це overengineering. Кожен зайвий патерн додає стрибків по файлах і когнітивного навантаження, не даючи натомість нічого.

    Орієнтир простий: спершу біль — потім патерн. Одна копія коду — це не привід для абстракції; правило трьох каже задуматись, коли ти скопіпастив утретє. Патерн виправдовує себе, коли знімає конкретне дублювання чи крихкість, а не коли додає «правильності». Це прямий наслідок YAGNI (/avtomatyzatsiia/pryntsypy-dyzainu-testovoho-kodu-dry-kiss-yagni-kompozytsiia): не будуй того, що ще не потрібно.

    Типові помилки

    • Виглядає як чистий код за патернами, а насправді — абстрактний шар з однією реалізацією, який лише додає стрибків по файлах. Передчасна абстракція, яку варто прибрати.
    • Виглядає як Builder, а насправді — набір сеттерів без дефолтів і без build(). Білдер без розумних дефолтів не економить нічого й лише подовжує код.
    • Виглядає як Factory, а насправді — той самий switch на пів екрана, скопійований у кожен тест. Фабрику проголосили, але не винесли, дублювання лишилось.
    • Виглядає як зручний Singleton-конфіг, а насправді — джерело флаку в паралелі: два тести правлять один об'єкт і залежать від порядку.
    • Виглядає як Screenplay, а насправді — POM, перейменований на «Tasks»: задачі лізуть напряму до локаторів, ані акторів, ані здібностей немає. Карго-культ без вигоди патерна.
    • Виглядає як Facade, а насправді — god object, що тягне пів застосунку в один клас і знає забагато.

    Підсумок

    • Патерн — це спільна назва для рішення в контексті, а не імпортований код; головна його цінність — передбачуваність для команди.
    • POM простий і page-центричний; Screenplay actor-центричний, строгий за SOLID і виправданий на великих сюїтах з ролями, але надлишковий на малих.
    • Builder беруть, коли багато опціональних полів і потрібні дефолти; Factory — коли треба обрати одну реалізацію з кількох. Плутати їх — типова помилка.
    • Facade ховає складність підсистеми за простим методом; його межа — не перетворитися на god object.
    • Singleton і глобальний мутабельний конфіг ламають ізоляцію й паралель; у Playwright їх заміняють фікстури (dependency injection). Спершу біль — потім патерн.

    Що питають на співбесіді

    • «Чим Screenplay відрізняється від Page Object і коли оберете який?» Інтерв'юер дивиться, чи розумієш тредоф, а не чи знаєш модне слово. Сильна відповідь: POM простіший і достатній для більшості проєктів; Screenplay — композиція дрібних задач навколо актора, кращий на великих сюїтах з ролями, ціна — церемонії й поріг входу.
    • «У чому різниця між Builder і Factory?» Очікують чіткий маркер: багато опціональних полів покроково — Builder; вибір однієї реалізації за раз — Factory.
    • «Де у ваших тестах Singleton і чому це небезпечно?» Чекають згадку ізоляції й паралельного запуску; бонус — «глобальний конфіг заміняю фікстурами / DI».
    • «Наведіть патерн, який ви прибрали як зайвий.» Перевірка на карго-культ. Хороший кандидат розповідає, як спростив overengineered код, а не хвалиться кількістю патернів.
    • «Що таке патерн проєктування?» (trainee-рівень) Очікувана відповідь — «названий рецепт розв'язання типової проблеми в контексті», а не «щось із класичної книжки».

    Червоний прапорець для інтерв'юера — кандидат, який додає патерни всюди й не може назвати їхню ціну.

    Джерела

    • Gamma, Helm, Johnson, Vlissides. «Design Patterns: Elements of Reusable Object-Oriented Software» (1994) — першоджерело GoF-патернів Builder, Factory, Facade, Singleton.
    • Refactoring Guru — каталог патернів — доступний довідник з класифікацією та прикладами.
    • Serenity/JS — Screenplay Pattern — офіційна документація патерну Screenplay (актори, здібності, задачі, інтеракції, питання).
    • Martin Fowler — PageObject — канонічний опис Page Object як точки відліку для порівняння.

    Примітка: патерни проєктування не входять до силабусу ISTQB CTFL 4.0 — це інженерна тема поза фундаментальним рівнем; дотичну архітектуру автоматизації оглядово торкається силабус ISTQB CT-TAE (Test Automation Engineer), але без конкретних GoF-патернів.