22 квітня 2020 р.

Тренінг 360, який відбувся 17 жовтня 2019 року, ознайомтеся з ажіотажем зустрічі розробників. У цьому я прочитав лекцію на тему "Труднощі інтеграційних тестів" (на Java), хоча я скоріше обговорював позитивні сторони інтеграційних тестів.

Увага! Наступна публікація містить елементи, які можуть порушити ваш спокій. Моя мета - підкреслити, що нам також іноді доводиться ставити під сумнів основні твердження та елементи, такі як тестова піраміда. Тому в дописі ви можете зіткнутися з деяким зміщенням акцентів, вам пропонується вирішити це на місці.

інтеграційним

У дописі я розгляну тестову піраміду та поняття і навіть свої застереження щодо неї. Потім я розгляну альтернативний підхід, який особливо застосовний до мікросервісів. Тим часом я також наведу приклади тестування простої програми Spring Boot. Приклад проекту доступний на GitHub.

Тест-піраміда

Тестову піраміду ввів Майк Кон у своїй книзі "Успіх з Agile", щоб наочно уявити, як розмістити різні рівні тестування.

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

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

На жаль, це вже викликає у мене кілька питань. З одного боку, поняття чітко не визначені, всі інші означають різні. Вже не зовсім зрозуміло, які частини програми називаються. Класи на найнижчому рівні, ми можемо з цим домовитись, але які вищі рівні? Це так звані модулі (наприклад, книга архітектури програм Java, OSGi), компоненти (наприклад, книга чистої архітектури, що дуже протиставляється, наприклад, Spring Framework/Java EE, де компонент є компонентом), плагіни тощо. Навіть для програми використовуються різні імена, такі як система, сервіс тощо. Книга Clean Architecture та мікросервіси називають додаток послугою, і це мене бентежить, оскільки Spring Framework також називає компоненти на рівні бізнес-логіки в трирівневій архітектурі. Я буду використовувати клас (і так, у цьому випадку інтерфейси, переліки, анотації тощо), модуль, імена додатків.

При модульному тестуванні очевидно, що зовнішні залежності потрібно висміювати. Так, але клас може використовувати багато елементів у бібліотеці класів Java SE, наприклад. рядок, список тощо. Це зовнішні залежності? Очевидно, ні, тому ми можемо сказати, що ми не глузуємо над ними. Що щодо цього у випадку із зовнішніми бібліотеками, які реалізують подібні структури даних, такі як напр. колекції гуави чи Apache Commons? А як щодо наших подібних власних класів, об’єктів вартості? А як щодо зовнішніх залежностей, таких як напр. зареєструвати SLF4J? Чи можна викликати одиничний тест при запуску контейнера, напр. Весняний фреймворк чи його частина? (Тест одиниці Spring Framework - це коли ви тестуєте компонент, але запускаєте певні пристрої Springes, менший контейнер.) Де провести межу?

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

Є також чимало питань щодо тестів E2E. Чи включає він лише поверхневі випробування? Або сюди можна включити тести API, коли інший інтерфейс програми, напр. Ми звертаємось до веб-сервісу REST. Чи можна це назвати тестом E2E, якщо ми тестуємо лише одну підфункцію через інтерфейс? Ми тестуємо в повній ізоляції від інших програм, або точка E2E означає інтеграцію з іншими програмами?

Крім того, виникають такі поняття, як сервісний тест, компонентний тест, системний тест, що вони означають?

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

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

Сумніви щодо модульного тестування

У прикладах я покажу програму (яка також виділяється як мікросервіс), що являє собою трирівневу програму Spring Boot, яка відстежує дані міста. Я не реалізував інтерфейс JavaScript, він доступний на REST API. Під базою даних H2. Ви знаєте координати міста. За допомогою алгоритму Haversine він обчислює і повертає свою відстань від Будапешта. Він також повертає температуру, виміряну в місті, за допомогою зовнішньої служби (Час).

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

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

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

Розглянемо наступний клас контролера, для якого корисність модульного тесту не зовсім зрозуміла.

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

Це можливо у Spring Boot за допомогою @WebMvcTest, який запускає лише рівень контролера, а сервісний рівень потрібно глузувати і також називати модульним тестом, оскільки він тестує контролер, але запускає Spring. Тому для мене це належить більше до рівня інтеграції.

Давайте розглянемо рівень бізнес-логіки, сервіс. Тут ситуація складніша.

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

І тут варто написати модульний тест для таких випадків, як:

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

Їх можна знайти в прикладі.

Є також питання щодо тестування одиниці стійкого шару. У більшості випадків це прості виклики відповідних об'єктів JDBC (DataSource, Connection тощо), JdbcTemplate або EntityManager. Я маю застереження щодо того, чи варто з них глузувати. Для Spring Data JPA все, що вам потрібно зробити, це написати інтерфейс, і він реалізований самим фреймворком, тому цікаво, як вони можуть бути модульно протестовані. Тут, у випадку з JPA, анотації та запити знову виходять на перший план, що було б непогано протестувати, але це неможливо з модульним тестом.

Spring Boot також має рішення для цього за допомогою анотації @DataJpaTest, яка також називається unit test, для тестування сховища, але окрім запуску Spring, вона також запускає вбудовану базу даних (наприклад, H2). Тому для мене це також належить до рівня інтеграції.

Відповідає за зв’язок з іншими системами, т. Зв. тестування класів шлюзу знову сумнівне. Тут, залежно від протоколу, ми обов’язково використовуємо якусь сторонній бібліотеку, без якої не обов'язково тестувати.

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

З них тестування перших двох безумовно є частиною інтеграційного тестування.

Зовнішній додаток, до якого ми підключаємось, можна легко розблокувати, для цього існує кілька інструментів, напр. WireMock або MockServer. Вони можуть працювати як окремі сервери http (обидва, звичайно, також інтегровані з JUnit), і ви можете вказати, яку відповідь на запит (наприклад, html, json тощо) повертати. Таким чином, весь стек http також керується. Їх використання корисно не тільки в тому випадку, якщо ми розробляємо його таким чином, що відповідна програма не готова або може бути недоступною під час розробки, але гілки помилок також можна дуже добре перевірити, наприклад що якщо зовнішній додаток не реагує або лише повільно реагує, повертає неправильну відповідь тощо. Тестовий приклад з обома з них можна знайти в прикладі програми.

Сумніви щодо тестування E2E

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

У книзі «Чиста архітектура» зазначено, що графічний інтерфейс - це крихкий, часто мінливий шар, тому ми повинні залежати від нього якомога менше. Знову ж таки, для багатьох поверхневих випробувань ми можемо зіткнутися лише з явищем проблеми крихкого тесту.

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

Немає сумнівів у важливості тестування E2E, але варто тримати кількість на низькому рівні. Я точно рекомендую лише протестувати основні функціональні можливості бізнесу, які «генерують гроші». Тут я хотів би згадати ще один напрямок. Ті, хто усвідомив, наскільки складно або дорого створити таке тестове середовище, яке, крім того, є копією живого середовища, винайшли концепцію тестування в реальному часі. Очевидно, що це можливо лише для певних додатків. Обов’язковою умовою є професійний моніторинг і можливість негайного виявлення помилок, а також можливість негайного та автоматичного повернення до попередньої версії у разі помилки. Тут добре відома концепція - розгортання Blue-Green, коли стара і нова версії живуть паралельно і можуть бути повернуті в будь-який час. Так само, як і випуск Canary, коли нову версію одночасно реалізовує лише вузьке коло користувачів.

Випробування стільника

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

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

(Свою назву вона отримала завдяки тому, що своєю формою нагадує гексагональні клітини селезінки у вулику.)

Інтеграційні тести мають наступні переваги:

  • Незалежно від деталей реалізації, якщо ми спираємось на API, внутрішній рефрактор не порушить тести.
  • Вони можуть бути використані для перевірки деталей, які не можуть бути охоплені модульними випробуваннями, наприклад рівень контролера для серіалізації JSON, відображення URL-адрес або рівень сховища для інтеграції баз даних.
  • Шар шлюзу також можна протестувати, висміюючи зовнішні системи. Однак зовнішні системи не потрібно встановлювати або інтегрувати.
  • При найменшому обсязі роботи ми досягаємо найбільшого охоплення.
  • Вони швидші за тести E2E.

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

Наступним кроком може бути знущання над класами, пов'язаними із зовнішніми ресурсами. Прикладами є CityRepository, який пов'язаний з базою даних, і TemperatureGateway, який пов'язаний з часом. Пов’язаний тест - InMemoryCityIT, який керує класами CityController та CityService.

Наступним кроком є ​​запуск програми за допомогою гарантованої REST сторонній бібліотеки, база даних - це вбудований H2, а TemperatureGateway підключений до вбудованого http-сервера, реалізованого за допомогою WireMock.

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

Резюме

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

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

Як фанат Java, я розробляю, викладаю, веду блог, організовую події та відвідую конференції. Я закінчив Університет Дебрецена за спеціальністю математик-програміст, зараз викладаю в Training360.

JTechLog - це щоденний щоденник-блог про тонкощі мови та платформи Java.