Інтеграційні тести в дії. Розбираємо на прикладі з вебсервісом

💡 Усі статті, обговорення, новини про тестування — в одному місці. Приєднуйтесь до QA спільноти!

Вітаю. Мене звати Ганна Касай. Я .NET-розробниця в компанії ITERA. Певний час моєю зоною відповідальності на посаді було написання інтеграційних тестів, тож в даній статті я хочу поділитись накопиченими досвідом і знаннями.

Ми з’ясуємо, що таке інтеграційні тести і чому їх варто реалізувати під час розробки багатокомпонентної системи. Також ми розглянемо низку корисних API, які наша команда використовує для тестування сервісів і додаткові інструменти, які дозволяють максимально автоматизувати і, відповідно, оптимізувати процес розробки.

Всю інформацію я подаватиму на прикладі програми, написаної для тестування вебсервісу.

Intro

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

Забігаючи наперед, розкажу, що саме на нас очікує:

  1. кодогенерація;
  2. взаємодія з браузером в інтеграційному тесті;
  3. проходження двофакторної автентифікації під час виконання програми;
  4. генерація одноразового паролю для проходження другого кроку автентифікації;
  5. обробка тимчасових збоїв під час тестування системи з подійно-орієнтованою архітектурою;
  6. альтернатива Xunit.Assert;
  7. Azure DevOps CI/CD.

Все це ми розглянемо на прикладі інтеграційного тесту для вебсервіса. В ньому перевіряється коректність роботи POST /checkid ендпоінту, який отримує запит з ідентифікаційним номером платника податків і, якщо він коректний, кладе його в базу даних і повертає нам id запису.

Солюшн програми містить xUnit проєкт з одним тестом, реалізованим згідно з патерном AAA, де:

1. Arrange: формування даних для тесту.

2. Act: автентифікація і авторизація, генерація токена доступу, запит до POST /checkId ендпоінту з коректними даними, пошук запису в базі даних, формування очікуваного результату.

3. Assert: перевірка коректності отриманого результату.

Integration tests

Інтеграційні тести — це один з різновидів автоматизованих тестів, задача яких полягає у визначенні коректності роботи складної системи, представленої взаємодією різних модулів.

Цілком імовірно, що одні модулі будуть реалізовані ізольовано від інших, можливо, навіть іншою командою розробників. Скоріше за все такі модулі і підтримуватись будуть неузгоджено, незалежно один від одного. В таких випадках вкрай важливо регулярно перевіряти їхню взаємодію і працездатність системи в цілому.

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

Інтеграційні тести найчастіше покривають роботу складної, багатокомпонентної системи, тому потребують підключення додаткових залежностей і залучення додаткових інструментів розробки. Про них далі і поговоримо.

Code generation

В інтеграційних тестах ми нерідко маємо справу з базою даних і сторонніми вебсервісами. Цілком імовірно, що ми б могли тестувати API з десятком ендпоінтів і комунікувати з декількома базами даних. За таких умов реалізувати моделі для інтеграцій самостійно стає затратним по часу.

А що станеться, якщо розробники тестованого вебсервісу раптово змінять модель запиту до ендпоінту? Тоді під час виклику API ми можемо отримати відповідь зі статусом, скажімо, 400 Bad Request.

Відповідно, для того, щоб інтеграційний тест знову коректно запрацював, нам треба внести правки в модель запиту. А якщо у нас немає доступу до репозиторію вебсервісу або правки вносяться часто, але нас про них не попереджають? Тоді тримати інтеграційний тест в актуальному стані стає складніше.

Тобто, під час розробки і підтримки інтеграційного тесту ми можемо зіткнутись з наступними проблемами: довготривалість реалізації моделей для інтеграції і складність їхньої підтримки в актуальному стані. Обидві проблеми вирішуються за допомогою кодогенерації.

Кодогенерація — це процес автоматичного створення коду програмами-генераторами на основі документації в json/yaml/xml та інших форматах представлення даних.

Кодогенерація допомагає швидко і точно реалізувати моделі для інтеграції з ресурсом і тримати їх в актуальному стані.

З ряду причин інтеграційний тест — це гарне середовище для використання кодогенерації:

  1. ми, найімовірніше, матимемо справу з великою кількістю моделей, складних в реалізації;
  2. ми зацікавленні лише у відтворенні моделей і не зацікавлені в їхній кастомізації (перейменуванні, зміні типів тощо);
  3. для нас важливо в будь-який момент часу швидко відтворити зміни в моделях, не вникаючи в суть цих змін, якщо це можливо;

Розглянемо кодогенерацію на прикладі тестового проєкту. За допомогою генератора Swagger Codegen в ньому реалізоване створення моделей для інтеграції з тестованою API. Все, що для цього потрібно — swagger-codegen-cli.jar файл і встановлена Java версії 7 або вище (в залежності від вимог).

Процес генерації винесено в окремий файл, таким чином можна швидко перегенерувати моделі за необхідністю:

Всі згенеровані моделі можна знайти за посиланням.

Варто додатково зупинитись на генерації моделей для роботи з базою даних. Представлений інтеграційний тест комунікує з Cosmos DB. Оскільки це NoSQL база даних, вона не має чіткої схеми, тому кодогенерація до неї не застосовна. Проте для реляційних баз даних це можливо реалізувати.

Наприклад, Entity Framework і Entity Framework Core дозволяють згенерувати класи-сутності та клас DbContext на основі схеми бази даних. В Entity Framework цей підхід називається Database First, а в Entity Framework Core — Scaffolding.

Implementation tools

Тепер, коли ми вже знаємо, що таке інтеграційний тест і чому його варто використовувати, настав час ознайомитись з його можливостями на практиці.

Ця глава являє собою огляд деяких корисних і потужних залежностей, які широко використовуються в інтеграційних тестах. Я розкажу про складнощі, з якими ми можемо зіткнутись під час реалізації тестів, і як саме названий інструмент допомагає справитися з ними.

Розглянемо деталі реалізації інтеграційного тесту, який перевіряє роботу POST /checkId ендпоінта деякого вебсервісу. Ендпоінт приймає ідентифікаційний номер платника податків і, якщо номер коректний, кладе його в базу даних і повертає id запису:

Під час виконання тесту компоненти програми взаємодіють наступним чином:

Першою цікавою задачею є процес проходження автентифікації. На цьому етапі виникають певні ускладнення:

  1. автентифікація потребує взаємодії з вебсторінкою через браузер;
  2. автентифікація може містити декілька кроків, тобто бути багатофакторною;
  3. ми можемо бути обмежені у виборі способу автентифікації.

Отже, можна зробити висновок, що чим більше факторів містить автентифікація і чим більше обмежень на способи її проходження, тим складніше це програмно реалізувати.

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

Імітувати даний процес можна за допомогою бібліотек Selenium.WebDriver і WebDriverManager. Вони дозволяють запустити майже будь-який браузер з персональними налаштуваннями (мова інтерфейсу, режим інкогніто і т.д.) і взаємодіяти через код з UI-елементами так, як би ви це робили вручну:

З сильних боків, Selenium також використовуються для кросбраузерного тестування. Це означає, що його можна застосувати не лише для тестування бекенд логіки, але й для кросбраузерної сумісності сайту.

Перший фактор автентифікації передбачав введення пошти та паролю:

Після проходження першого кроку ми стикаємось з другим і третім ускладненнями: існує другий фактор автентифікації, і ми обмежені можливими способами його проходження: використання одноразового паролю, SMS з кодом на телефон, дзвінок на телефон:

Невеликим спойлером буде те, що, маючи бажання, пройти цей етап програмно можна будь-яким з представлених способів. Все залежить лише від того, скільки саме ресурсів ми готові витратити на написання інтеграційних тестів.

Найменш ресурсозатратним способом серед представлених є використання одноразового паролю, тому для другого кроку автентифікації оберемо саме його:

Отримати такий пароль можна за допомогою Otp.NET. Це бібліотека для генерації одноразових паролів на основі TOTP (Time-based one-time password) і HOTP (Hash-based one-time password) алгоритмів.

Обидва алгоритми використовують секретний ключ. Цей ключ статичний і отримується користувачем під час реєстрації акаунта в застосунку-аутентифікаторі (наприклад, Authenticator):

Детальніше про те, як пройти двофакторну автентифікацію з використанням Selenium і Otp.NET можна почитати за посиланням.

Після завершення автентифікації ми можемо зробити POST /token запит для генерації токена доступу. З ним ми коректно викликаємо POST /checkId ендпоінт і отримуємо ідентифікатор:

Використовуючи отриманий id, ми маємо дістати дані з бази даних — це наступна цікава і нетривіальна задача.

Тестований вебсервіс має подійно-орієнтовану архітектуру. Це означає, що користувачу і сервісу не потрібно чекати один одного, щоб продовжити виконувати задачі. Як наслідок, ми можемо отримати id запису раніше, ніж сам запис буде доданий в базу даних.

Проте сервіс гарантує, що запис з надісланим id буде доданий в базу даних, і зрештою, запитавши, ми зможемо його отримати. Цей тип гарантії називається кінцевою узгодженістю.

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

Різниця в часі між отриманням відповіді від вебсервісу і запитом до бази даних в тесті з великою ймовірністю буде менше, ніж тривалість вікна неузгодженості. Тобто на момент спроби витягнути дані з бази їх там ще не буде, ми нічого не отримаємо і тест впаде.

Цю проблему було вирішено за допомогою бібліотеки обробки тимчасових збоїв Polly. Вона дозволяє розробникам налаштувати певний шаблон виконання запитів, задати очікуваний результат, обробляти і обходити можливі помилки тощо.

В нашому випадку за допомогою Polly ми обійшли вікно неузгодженості шляхом реалізації ланцюга методів (HandleResult, WaitAndRetryAsync, ExecuteAsync) з заданою кількістю спроб і очікуваним результатом.

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

Після отримання результату ми переходимо до його порівняння з очікуваним. Для цього було використано FluentAssertions. Це .NET бібліотека, призначена для тих самих цілей що і Xunit.Assert, тобто для валідації об’єктів. Проте FluentAssertions має низку переваг.

По-перше, можливість будувати ланцюги методів. Таким чином можна легко відокремити перевірку одного об’єкту від перевірки іншого і покращити читабельність коду.

По-друге, опції, яких немає в Xunit.Assert, наприклад, порівняння об’єктів, перевірка вмісту колекції і т.д.

По-третє, API FluentAssertions дозволяє читати код як природну мову. Наприклад, actualResult.Should().BeEquivalentTo(expectedResult) краще сприймається оком, ніж Assert.Equal(actualResult, expectedResult).

Перевірка об’єктів в інтеграційному тесті:

Additional tools

Запуск інтеграційних тестів можна винести окремим етапом CI/CD пайплайну. Це може стати надзвичайно корисною практикою, якщо запускати інтеграційні тести після деплойменту. Таким чином, якщо тести впадуть після нового релізу, це допоможе виявити і локалізувати проблему одразу, за гарячими слідами.

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

Як середовище для запуску пайплайну я використала Azure DevOps. Він зручний тим, що пропонує для всіх одну безкоштовну паралельну роботу тривалістю до 1800 хвилин на місяць.

Пайплайн для солюшена з інтеграційним тестом:

Даний пайплайн складається з чотирьох етапів:

1. Підставлення змінних. Не виключено, що для написання тестів вам доведеться використовувати конфіденційні дані. В описаному вище інтеграційному тесті такі дані використовуються для отримання токена доступу і підключення до бази даних.

Для безпечного зберігання конфіденційних даних в сервісах (AzureDevops, Jenkins тощо) існує механізм створення змінних (в тому числі і приватних). Такі змінні можна співвіднести зі змінними з конфігураційного файлу солюшена і підставити в них значення в процесі виконання пайплайна. Самим змінним в конфігураційному файлі можна надати будь-які значення:

Змінні в AzureDevops:

2. Встановлення Google Chrome. Як ми вже зазначили, Selenium WebDriver працює з браузером, і нам необхідно переконатись, що в середовищі, в якому запуститься тест, встановлений браузер, який використовується під час його роботи.

Даний інтеграційний тест запуститься в докер-контейнері, який збереться на основі імеджа mcr.microsoft.com/dotnet/sdk:7.0. В ньому немає Google Chrome, тому його треба встановити.

3. Збірка проєкту. В окремому докер-контейнері ми збираємо проєкт і виносимо окремим етапом, щоб переконатись, що він не містить помилок компіляції.

4. Запуск тестів. На останньому етапі ми запускаємо тест (також в окремому докер-контейнері), щоб переконатись, що він відпрацьовує без помилок.

Структура пайплайна під час виконання з логами для кожного етапу виглядає наступним чином:

Для зручного отримання результатів можна налаштувати сповіщення через webhook. Після відпрацювання пайплайну ви отримаєте сповіщення зі статусом успіху та іншими деталями в зручному для вас месенджері (Slack, Teams тощо):

Для візуалізації в AzureDevops також можна переглянути аналітику.

Статистика за тривалістю пайплайну з вказаною середньою тривалістю і найдовшим етапом:

Статистика успішності завершення пайплайну:

Conclusions

Отже, ми поговорили про інтеграційні тести і важливість їхнього використання, на прикладі реалізованого тесту розглянули корисні інструменти, які можна використати під час розробки.

Варто зазначити, що інтеграційні тести не обмежуються поданою в статті інформацією. Існують типи інтеграційного тестування, безліч підходів (мокінг, стабінг тощо) і фреймворки для їхньої реалізації (Moq, NSubstitute та інші).

Також тестові проєкти, як і будь-які інші, мають свої стандарти і найкращі практики: правила найменування тестових методів, патерни проєктування, правила імплементації (уникнення магічних констант (строк, чисел і т.д.), власної логіки у вигляді розгалужень і циклів тощо). Детальніше про це можна дізнатись на цьому ресурсі.

Інтеграційне тестування використовується на будь-яких рівнях, починаючи від взаємодії декількох компонент всередині однієї програми, закінчуючи тестуванням великої розподіленої системи. Під час тестування API, що є частиною розподіленої системи, можна навіть виявити проблеми в зовнішніх підсистемах, з якими взаємодіє тестована програма.

Інтеграційне тестування є важливою ланкою при розробці комерційних проєктів.

👍ПодобаєтьсяСподобалось5
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

О, схоже, сову (Selenium) таки натягнули на глобус (інтеграційні тести). ))

автентифікація потребує взаємодії з вебсторінкою через браузер;

Невже цей пункт є обов’язковим? І не можно зимітувати роботу веб сторінки завдяки тесту?
Звісно, якщо ціль протестувати функціональність сайту що стосується автентифікації, то питань нема. Але ж в статті говориться про endpoint бекенду.

Дякую за коментар!

В даному конкретному випадку можна було б обійти використання Selenium і отримати токен доступу за допомогою бібліотеки Microsoft.Identity.Client, але є ряд нюансів:

1. ми б сильніше залежали від технологій розробки. Selenium підтримує Ruby, Java, PHP, Perl, Python, JavaScript, C#. Таким чином інформація, подана в статті, могла б зацікавити не тільки .NET розробників.
2. Microsoft.Identity.Client працює лише з Azure AD (знову залежність від технологій);
3. я від початку мала за мету продемонструвати певний стек інтсрументів, корисних під час розробки інтеграційного тесту, і Selenium був одним із них. Саме під час тестування даного ендпоінту Selenium можна було використати тільки для проходження автентифікації.

Варто також додати, що для того, щоб згенерувати валідний токен на основі акаунту за допомогою Microsoft.Identity.Client, довелось б змінити налаштування автентифікації тестованого веб-сервісу, що є, вочевидь, поганою практикою. Також, Microsoft.Identity.Client не може згенерувати токен доступу на основі персонального Microsoft акаунту без входу в акаунт через браузер. Тому в даному випадку Authorization Code Flow є єдиним прийнятним варіантом.

Доброго дня, цікавить, що ви думаєте про насиупне: Селеніум є дуже не гнучким інструментом, фреймворк під який потрібно окремо писати і апдейтити (вейти, оновлення локаторів і тд, і тп) протягом його всього існування. Тримаючи Селеніум виключно як спосіб автентифікації, коли автентифікацію було б краще зробити через АПІ якийсь, ви робите вашу автоматизацію більш крихкою (іншими словами: менш надійною), адже чим більше кроків та додаткових сетапів у тесті, тим більш шансів, що він впаде на одному з цих багаточисельних кроків.

Це ні в якому разі не хейт Селеніуму, бо я сам працюю з Селеніумом вже 5 років, але імо, Селеніум це інструмент для суто UI тестів, який варто залишати виключно для цього, і його використання в будь-яких інших цілях виглядає як якийсь костиль.

Дякую за відгук!

Да. ви абсолютно праві. Коли справа доходить до використання Selenium, часто можна зітнутись з різного роду проблемами.

Проблема очіккування не є суттєвою, оскільки, який би тип очікування ви не використовували (implicit, explicit, fluent), драйвер перестане очікувати як тільки знайдеться потрібний елемент (implicit wait) або виконається умова (explicit, fluent waits). Тому в випадку, коли ми точно знаємо, що певний елемент має з’явитись, підвищення часу очікування не вплине на перфоманс тесту. Проте якщо елемент так і не з’явиться, то драйвер чекатиме весь зазначений час, перш ніж кинути помилку.

Використання локаторів дійсно є проблемою, оскільки інколи їх доводиться оновлювати, якщо змінюється UI. Проте вплив змін UI на коректність роботи тесту можна мінімізувати. Під час локалізаціїї певного елементу ви можете користуватись різними його властивостями, такими як id елементу, назва класу, значення, атрибут і т.д. Достатньо прикинути які параметри змінюватимуться з меншою імовірністю. Наприклад, скоріш за все текст на кнопці зміниться з більшою імовірністю, ніж її id або назва CSS класу. Таким чином можна мінімізувати вплив змін UI на працездатність тесту.

Я могла згенерувати токен за допомогою інших інструментів, таких як Microsoft.Identity.Client, IdentityServer4 і т.д., проте я від початку мала за мету продемонструвати певний стек інтсрументів і Selenium був одним із них. Саме під час тестування даного ендпоінту Selenium можна було використати тільки для проходження автентифікації. Використання Selenium також дало вікно для ознайомлення з бібліотекою OTP.NET.

Не розглядайте тестування, представлене в статті, як оптимальне, а скоріше як огляд інструментів, які можна використати під час розробки, на конкретних прикладах.

Мені здається чи тут трохи наплутано інтеграційні тести з end to end. Я можу помилятися, але в інтеграційних тестах ми маємо протестувати зовнішні залежності в нашому коді, можна обійтися простим arrange. Якщо ми підключаємо браузер, то це трішки більше.

+, трохи сумбурна стаття, більше виглядає як overview тулів що юзають на проекті
1. початок за тестування api endpoint, де достньо виключно бекенда, а переходимо до selenium
2. ui-тести на selenium не обов’язково є інтеграційними
3. застосування моків, стабів це скоріш шлях до юніт-тестів ніж інтеграційних

Дякую за відгук!

Ви вірно підкреслили, що ця стаття є оглядом на інструменти, які ми використовуємо під час розробки на проєкті (це було також зазаначено в статті). В даній статті я ставила за мету продемонструвати можливості інтеграційних тестів на конкретному прикладі і розказати про ряд корисних інструментів і в яких ситуаціях їх слід застосовувати.

Щодо пронумерованих зауважень:

1. не бачу невідповідності в переході в back-end логіці до Selenium, особливо в статті, спрямованій на розкриття можливостей інтсрументів розробки, одним з яких якраз і є Selenium. Імовірна ситуація, кли без Selenium не обійтись;
2. в даній статті не йдеться про UI-тести. Якщо ви посилаєтесь на абзац, в якому говориться, що Selenium можна використовувати для кросбраузерного тестування, то я не наголошувала на тому, що це інтеграційне тестування. Мета цього абзацу — розглянути Selenium з іншого боку, поза контекстом тесту, представленого в статті, і розкрити ще один кейс використання Selenium;
3. застосування мокінгу і стабінгу не є характерною рисою unit-тестів.
Приклад: вам необхідно протестувати нотифікацію зареєстрованих користувачів при додаванні нового продукту на сайт. Якщо ви протестуєте повністю реальний функціонал, то додасте фейковий товар в базу даних і розішлете реальним користувачам нотифікації про його наявність. Для того, щоб уникнути цього, можна, наприклад, створити фейковий об’єкт, замість реального репозиторію, і повертати спеціальну тестову пошту, на яку прийде нотифікація.

Щодо сумбурності статті, можу лише сказати, що я намагалась продемонструвати повний процес розробки інтеграційних тестів, охопити всі можливі корисні існтрументи розробки і підтримки і зробити це так, щоб читач не знудився. Шкода, що дана стаття здалась вам сумбурною, проте я прийняла до уваги ваше зауваження.

Ще раз дякую за ваш відгук!

Дякую за відгук!

Почнемо з визначення. «Integration testing means testing two or more dependent software modules as a group.» (The Art of Unit Testing). Представлене в статті тестування відповідає визначенню. Як сказано в статті, часто інтеграційний тест, на відміну від unit-тестів, вимагає підключення додаткових залежностей. В даній статті одним із таких є Selenium. Взаємодія з браузером не є рисою, яка відрізняє E2E тестування від інтеграційного тестування.

Говорячи про E2E тести, погоджуюсь, що тестування API ендпоінту можна розглядати як E2E тестування або інтеграційне тестування. Ця неоднозначність викликана тим, що іноді межа між ними тонка або навіть розмита. E2E тестування передбачає перевірку коректності роботи певного юзер флоу. Мета цього тестування відтворити досвід користувача і переконатись в правильності роботи програми, починаючи від точки входу в програму, в яку потрапляє користувач, закінчуючи очікуваним результатом. Також іноді E2E тестування асоціюють з happy-path (golden-path).

Розробляючи представлений в статті інтеграційний тест, я не мала за мету відтворити досвід користувача, а лише протестувати коректність взаємодії компонент складеної системи на одному наборі тестових даних з позитивним флоу, тому це тестування представлене в статті як інтеграційне.

Підписатись на коментарі