Компоненти фреймворку автоматизації тестування за допомогою Selenium та Python
Привіт! Мене звати Володимир Обрізан, я кандидат технічних наук, директор компанії Design and Test Lab.
У цій статті ми розглянемо важливість архітектур та патернів проєктування; ключові компоненти, які ми використовуємо; яких правил проєктування ми дотримуємося. Стаття написана на основі багаторічного досвіду впровадження автоматичних тестів в компанії Design and Test Lab.
Про важливість архітектури фреймворку тестування
Одразу попереджаю, що це погана ідея думати: «Цікава стаття про архітектуру фреймворку! Я зроблю таку саму архітектуру на моєму проєкті».
Чому не можна брати та бездумно копіювати архітектуру? Тому що архітектура створюється не тому, щоб вона була такою, як у книзі, або як у когось у доповіді. Архітектура створюється на основі вимог та певних обмежень на поточному проєкті. Важливе правило — якщо є різні вимоги або обмеження до нашого фреймворку автоматизації або до нашого проєкту, то й архітектура буде різна.
Подивіться на метафору з різними авто: Формула-1 — дуже швидка машина, Mercedes — дуже гарна, а вантажівка — дуже потужна. Яку мені обрати? Якщо треба поїхати до бабусі за трьома мішками картоплі, то я буду брати самоскид. Якщо я поїду на бізнес-зустріч, то на Мерседесі. Під різні вимоги я обираю різні авто.
Які фактори та обмеження впливають на вибір архітектури фреймворку для тестування?
1. Строк та бюджет на розробку. Якщо проєкт триватиме місяцями, а може і роками, буде виходити багато нових версій продукту, то й регресійне тестування нам потрібно буде робити багато разів. Ми можемо обрати складну архітектуру, в нас буде багато часу на те, щоб її розробити та налагодити. Але якщо строк навпаки обмежений — треба протестувати за тиждень або місяць, то в нас не вистачить часу, щоб розробити складний фреймворк. Це не дасть того повернення на інвестицію часу та зусиль, тому що розробка фреймворку буде займати більше часу ніж тестування.
2. Складність об’єкта тестування. Скільки сторінок у вебзастосунку, який треба протестувати, скільки функцій, скільки інтеграцій з іншими сервісами тощо. Наприклад, якщо в нас застосунок на п’ять сторінок, то іноді й Page Object можна не робити. Чому? Тому що це не дасть ніякої вигоди з часом. Але якщо це 50 сторінок та якісь 50 кейсів, то тоді, мабуть, треба розробляти якийсь фреймворк, якось відокремлювати код в Page Object та інше.
3. Вимоги по покриттю тестами. Кількість: треба зробити 10 тестів, 100 тестів або 1000 тестів? Види тестів: перевіряти лише дії користувача за допомогою UI, або ще будуть API-тести, Security-тестування тощо? Платформи: треба тестувати лише в браузері на десктопі або треба перевірити застосунок ще на мобільних пристроях, в мобільних браузерах? Може існувати нативна версія застосунку для iOS та Android, і тесткейси будуть однакові, але ті засоби, за допомогою яких ми будемо натискати кнопки у нативному застосунку, будуть інші ніж у вебзастосунку, і це вплине на архітектуру фреймворку тестування.
4. Кількість та кваліфікація розробників (last but not least). Хто буде розробляти та підтримувати цей фреймворк? Яка у них кваліфікація? Якщо у розробників немає певної кваліфікації, вони не знають певного ноу-хау, як це зробити правильно, то складна архітектура тестування може не покращити роботу, не пришвидшити її з часом, а навпаки — загальмувати так, що ніхто не буде знати, як з нею працювати.
Софтверна архітектура — це не архітектура будинку. В будинку є несучі стіни, які я не можу зрушити з місця. Це правило проєктування будинку, яке неможливо порушити. А в софті усі ці правила проєктування порушити дуже легко. Та з часом, якщо їх не дотримуватись, то від архітектури нічого не залишиться.
Гарна архітектура з часом може пришвидшити роботу, тому що розробники будуть дуже швидко приймати рішення, де який компонент розташувати, який код ми відносимо до тестів, а який до Page Object тощо. Та навпаки: погана архітектура або недотримання правил проєктування може почати заважати. Ось чому усі ці фактори будуть впливати на архітектуру. Якщо в нас немає бюджету та стислі терміни, немає кваліфікованих розробників, які будуть все це підтримувати, тоді, мабуть, краще все робити в одному файлі. Для таких умов це буде найкраще рішення.
Патерни проєктування
Визначення патернів проєктування в контексті розробки програмного забезпечення надав Еріх Ґамма ще у 1994 році (Design Patterns: Elements of Reusable Object-Oriented Software, 1994, Erich Gamma). Патерн проєктування — це опис взаємодії об’єктів і класів, адаптованих для вирішення загального завдання проєктування в конкретному контексті.
По-перше, патерн проєктування — це опис взаємодії об’єктів та класів. Тобто коли ми пишемо код, алгоритм автоматизації чи ще щось, ми використовуємо об’єкти та класи, і патерн проєктування надає опис, як наше завдання розкласти на об’єкти та класи та як налаштувати між ними взаємодію.
По-друге, патерн проєктування надає конкретний контекст. Наприклад, контекст створення об’єктів, тобто як та за допомогою чого отримати посилання на новостворені об’єкти. Інший приклад контексту: в нас є купа об’єктів, як нам їх структурувати й працювати як з одним великим об’єктом?
По-третє, патерни проєктування надають спільну мову між розробниками. Коли хтось каже, що тут будемо застосовувати Abstract Factory, то усі розуміють, як це зробити, які там будуть присутні класи, які правила взаємодії між цими класами будуть та інше. Інший приклад, коли два розробники спілкуються: «Слухай, нам треба створювати тестових користувачів. Давай зробимо таку фікстуру, ми будемо використовувати патерн Command», то усі розуміють, що таке патерн Command, тому що можна піти до книги, де буде описано, як цей патерн зробити, за допомогою яких класів, які в них будуть інтерфейси.
Тому це спільна мова, що допомагає не витрачати час на зайві пояснення: «Розроби такий-то клас з такими-то методами, там повинні бути такі параметри, яке значення повинен цей метод повернути та інше». За допомогою патернів спілкування дуже коротке: «Імплементуй патерн Command» або «імплементуй патерн Strategy», і кожен знає, про що мова.
Якщо застосовувати ці патерни, код буде дуже легко використовуватися повторно. А це те, що ми прагнемо зробити. Ми не розробляємо фреймворк, який будемо кожного разу створювати наново. Мені цікаво розробити такі класи, щоб я міг їх використовувати повторно. Якщо я розробив Page Object для однієї сторінки, то я можу цей Page Object почати використовувати не лише в одному тесті, а в п’яти тестах або в 10 тестах. І навпаки, якщо я вже розробив тести, наприклад, для Profile Page, то я можу протестувати вебзастосунок та нативний застосунок. Якщо правильно застосовувати патерни проєктування, то потенціал повторного використання коду значно зростає. Гнучкість, легкість підтримки та розвитку — це теж дуже гарний ефект.
Усі патерни проєктування відповідають головному правилу — програмувати відповідно до інтерфейсу, а не реалізації. Коли в нас є об’єкт або клас, і нам відомий його інтерфейс (сукупність методів та параметрів цих методів), то ми повинні використовувати лише цю інформацію. На прикладі нижче клієнт команди Invoker використовує лише інформацію про інтерфейс класу Command, але нічого не знає про конкретну реалізацію ConcreteCommand.
А як це застосувати до фреймворку автоматизації? Розглянемо патерн Command на прикладі команди CreateUser. Вона використовується в фікстурах, де потрібно створити користувача до початку виконання тестів. Можна створити різні реалізації цієї команди: а) створити користувача через UI за допомогою Selenium, б) створити користувача за допомогою REST API, в) створити запис користувача в БД за допомогою SQL-запиту, г) створити користувача за допомогою сили думки.
Це жарт, але не з точки зору головного правила проєктування — мені байдуже на реалізацію, важливо, що команда має певний інтерфейс. Які переваги це надає? Гнучкість у тому, що у першій версії фреймворку ми можемо зробити команду CreateUser, яка буде це робити за допомогою Selenium. Через декілька місяців програмісти кажуть: «Ми розробили API!». Можна зробити іншу реалізацію команди CreateUser, та миттєво замінити її в усіх тестах без необхідності змінювати їхній код.
Архітектура тестового фреймворку
Моє уявлення про архітектуру тестового фреймворку еволюціонувало з часом та досвідом. На діаграмі нижче версія 2024 року. Розглянемо ключові компоненти.
Application Under Test — об’єкт тестування. На діаграмі зазначений веббраузер, тому що більшість застосунків, які ми тестуємо, — це вебзастосунки. У 2024 ми додали інтеграцію з Appium і можливість тестувати мобільні застосунки та сайти в мобільних браузерах на реальних пристроях. Компонент Server відокремлений, тому що є можливість тестувати та виконувати команди за допомогою REST API (security tests, API tests).
Third-party Applications — сторонні сервіси застосунку. Наш застосунок не працює у вакуумі. Він може надсилати повідомлення у месенджери, надсилати емейли, запитувати геодані. Багато сучасних застосунків використовують сторонні API Services.
Фреймворк тестування (test framework) складається з декількох компонентів: Web Driver Factory, Environment, Fixtures, API Client, Tests, Test Data Factory, Page Objects & Components, 3rd Party Clients.
Environment — це зовнішнє оточення тестів для конфігурації інших компонентів. На нього посилання мають майже всі інші компоненти. На діаграмі на нього немає посилань, але це зроблено, щоб не перевантажувати її звʼязками.
Якщо ми хочемо повторно використовувати компоненти в різних контекстах, то нам треба іноді якісь параметри їм задавати. Реалізація дуже проста — це змінні оточення, за допомогою environment variables, і ці змінні оточення споживають дуже багато інших компонентів: Test Data Factory, WebDriver Factory, Third Party Clients, API Clients. Наведу лише декілька прикладів.
- ADMIN_TOKEN — це токен автентифікації на нашому бекенді для API-клієнтів, тому що багато операцій треба робити від імені адміністратора системи: створити користувача, видалити користувача. Наприклад, змінні оточення, які пов’язані з тим, як вам сконфігурувати посилання на end, посилання на frontend, тому що, наприклад, frontend — це треба Page Objects, щоб вони знали, як відкривати яку сторінку.
- BACKEND_URL — API клієнт надсилає API запити.
- ENVIRONMENT_KIND — тип оточення: develop, stage, чи production. За допомогою цієї змінної ми можемо, наприклад, надати різні тестові дані для різних оточень.
- SELENIUM_DRIVER_KIND — це за допомогою якого там цього драйвера ми будемо тестувати.
- TESTMAIL_API_KEY — токен аутентифікації з third-party клієнтом Testmail App. Його ми використовуємо, щоби перевіряти пошту, яку надсилає наш застосунок.
- декілька змінних, пов’язаних з Тестрейлом, щоб отримувати інформацію про поточні тестові плани та створювати тестові звіти.
- windows resolution — у якому браузері ми будемо тестувати, тому що наші застосунки повинні працювати на десктопі, у браузері, і ми тут можемо задати розподільну здатність екрана.
На зображені нижче показано, як це використовується в Bitbucket Pipelines. Коли ми запускаємо тести, ми можемо там надати інформацію щодо оточення: де знаходиться frontend, де знаходиться backend, яка розподільна здатність тощо.
Призначення модулю WebDriver Factory — отримати абстрактний вебдрайвер, налаштований для подальшої роботи вже з тестами. Тут дуже важливе слово — це абстрактний. Пам’ятаєте перше правило Еріха Ґамми? Ми повинні програмувати згідно інтерфейсу класу, а не згідно його реалізації. Тести, Page Objects, та fixtures не повинні знати деталі реалізації, тобто за допомогою якого вебдрайвера ми наразі тестуємо: Chrome, Safari, Appium тощо.
Ключовий метод — це get_driver, він повертає абстрактний WebDriver. Яку саме реалізацію? Цю інформацію метод отримує зі змінної оточення SELENIUM_DRIVER_KIND.
Призначення компонента Page Object — це адаптувати інтерфейс низького рівня (HTML-елементи або деталі реалізації нативного застосунку) до високого рівня термінів застосунку, та інкапсулює усю цю складність роботи з ними. Клієнти Page Object цієї складності знати не повинні.
Ось певні правила проєктування, дотримання яких ми перевіряємо, щоб Page Objects були гарно зроблені.
- Інтерфейс Page Object записується в термінах застосунку, а не HTML-сторінки.
- Не потребує змін, якщо змінюється елемент керування на сторінці.
- Робить лише те, що може зробити користувач. Бачить лише те, що бачить користувач.
- Не робить ніяких перевірок (assertions), що стосуються тестового плану. Але може робити перевірки щодо поточного стану застосунку.
- Нічого не знає про тестові дані.
- Нічого не знає про браузер: працює лише з абстрактним WebDriver.
- Приховує асинхронні операції, які виглядають для користувача як синхронні (WebDriverWait).
Page Component — спрощення роботи з Page Object. Інтерфейс користувача звичайно створюється за допомогою візуальних компонентів. На одній сторінці може бути декілька компонентів, та програмісти їх використовують повторно на інших сторінках. Page Component створюється, якщо є потенціал повторного використання.
Приклад із застосунку, який ми тестуємо: це соціальна мережа для дизайнерів та архітекторів, які будують будинки. LocationField Page Сomponent вживається багато разів. Це такий контрол, який допомагає знайти географічну назву (країна, місто, вулиця) за допомогою Google Maps. Цей компонент використовується на багатьох сторінках: пошук проєктів, створення проєктів, створення та редагування статей, профіль користувача.
Ось як це виглядає у коді. CreateProjectPage — це Page Object, де в конструкторі ми створюємо посилання на LocationField (PageComponent).
Код CreateProjectPage став простіше, тому що там менше зайвого коду. Якщо цей LocationField компонент зміниться, я зміню лише один файл, та усі ці зміни автоматично відобразяться в усіх Page Objects та тестах, які його використовують.
Цей LocationField page component нічого не знає ані про тести, а ні про сторінку, тому я можу його повторно використати не лише на поточному проєкті, а й в інших, які на сторінці будуть використовувати той самий Angular-компонент.
Самі тести (tests) в нас поділені на кілька наборів (test suites).
- functional — Selenium-класіка;
- security-тести, які перевіряють базові правила доступу до певних ресурсів, зроблені за допомогою API-клієнту;
- mail-тести — функціональна перевірка мейлів. Ми перевіряємо, що усі події та нотифікації надходять до споживачів, а текст в них відповідає вимогам. Та ми ще перевіряємо, що, наприклад, якщо в мейлі є якесь посилання на верифікацію акаунта, відновлення пароля, що ці посилання валідні, вони працюють.
- screenshots-тести — перевірка дизайну сторінок та компонентів.
Правила проєктування тестів:
- тести не повинні використовувати методи WebDriver (findElement, сlick, send_keys тощо). Тести нічого не повинні знати про те, з чим взагалі ми працюємо.
- використовують лише інтерфейс Page Object;
- повинні бути проаннотовані ідентифікаторами тесткейсів (для інтеграції звітів з TestRail);
- setup та teardown код повинен бути в модулі fixtures.
Тут не згадаються усі загальновідомі правила написання тестів, тому що ці правила, які відносяться лише до фреймворку та архітектури.
Призначення модуля Fixtures — ввести об’єкт тестування в стан, потрібний тестам. Він допомагає іншим тестам створювати передумови для тестування. Наприклад: посилання на користувачів (admin); створення користувачів; створення тестових даних; завантаження файлів; sign-in/logout; ban/unban user; та багато інших.
Призначення Test Data Factory — відокремити тестові дані різних оточень від тестів. Використовується патерн Abstract Factory, який має дві реалізації: Develop і Stage. На Develop тестові дані створюються тестувальниками, а на Stage експортують дані з Production. Наприклад, користувача Nick Gonzales не буде на проді, тому на Stage він теж не потрапить. Таким чином один й той же тестовий сценарій ми можемо перевірити на Develop-оточенні одним користувачем, але на Stage для цього може бути інший користувач.
Модуль API Client. Його призначення — це доступ до об’єкта тестування за допомогою API. Його використовують API-тести та security-тести, ще його використовують фікстури. Як звичайно робиться API Client? У нашому випадку ми робимо автоматичну генерацію з Open API специфікації (Swagger).
Модуль 3rd Party API Clients. Його призначення — це доступ до інших сервісів, які використовує застосунок. Наприклад, в email-тестах ми отримуємо повідомлення через Testmail App (це такий спеціальний email-sandbox). Створюємо поштові скриньки у домені Testmail App, і за допомогою API отримуємо листи.
Приклад використання: тестовий сценарій перевірки запрошення користувача в проєкт: треба перевірити, що наш застосунок надсилає листи, зміст коректний та усі посилання в ньому працюють, як треба. Як його зробити, цей модуль? Звичайно, треба обирати готові API-клієнти на сайті сервісу або в репозиторії пакетів (pip install).
Висновки
При різних вимогах або обмеженнях проєкту повинні бути різні архітектури тестового фреймворку. Якщо на проєкті працює одна не дуже кваліфікована людина, та часу на тестування лише один місяць, то краще не робити складну архітектуру. Бо весь час вийде на розробку та підтримку фреймворку, а самого тестування буде мало. Якщо є декілька розробників, є певний досвід, багато часу, то можна робити Page Objects, Page Components, fixtures та інше.
Краще, коли архітектура еволюціонує, розвивається з проєктом. Це коли зміна в архітектурі зроблена тому, що команда вже відчувала труднощі в певному питанні та треба було якось це вирішувати.
Ця стаття написана за доповіддю «Компоненти фреймворку автоматизації тестування» на зустрічі спільноти «Selenium + Python», яка відбулася 22 листопада 2024 року. А долучитися до чату спільноти «Selenium + Python» можна в Телеграмі.
Запрошую вас також підписатися на мій канал «Надійне програмування», де раз на два тижні я викладаю доповіді, повʼязані з автоматизацією тестування.
27 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів