Як за допомогою тестів пришвидшити реліз
Every Company is Now a Tech Company
Привіт! Я Олег і з 2012 року працюю в тестуванні: почав як Manual QA Engineer, але більшу частину кар’єри займаюсь автоматизацією тестування. Веду блог на платформі «Медіум», де пишу нетривіальні статті про тестування українською. Також ви можете знати мене як автора найпопулярнішого коментаря на ДОУ за 2018 рік.
Для мене тестування — це не просто пошук багів і написання тестів. Це процес удосконалення продукту. У цій статті поділюся порадами, як культура розробки, спрямована на якість, у комбінації з комунікацією між учасниками команди та різні типи тестів допоможуть пришвидшити розробку. Або ні. Але мало б допомогти, чесно.
Сьогоднішній бізнес залежить від програмних продуктів. І щоб йти у ногу з часом, розвиватися, компанії мають розвивати свій софт. І бізнес хоче це робити швидко і часто. Двотижневі релізи сьогодні вже неактуальні. Такі гіганти, як Facebook чи Netflix, деплоять на прод по кілька разів на день.
Continuous delivery стає не просто однією з best practices, а важливою бізнес-вимогою. Як таке можливо? Баланс між культурою компанії, комунікацією між учасниками та технічними рішеннями сприяє швидкій розробці нового функціоналу.
Декомпозиція завдань
Завдання, що приходить на розробку, має бути маленьке. Уявімо, що у вас є фіча кошика в онлайн-магазині, на розробку якої йде 5 днів. У Jira ви можете створити одне завдання на 5 днів і працювати над ним. Або ж розділити його на кілька менших. Мені, приміром, не подобається розв’язувати задачі більше як
Якщо завдання маленьке, то:
- оцінювання проходить з мінімальними зусиллями та високою точністю. Що менша задача, то зрозуміліше, що потрібно в ній зробити. У таску на 5 днів ви можете поверхово бачити, що треба виконати, але не врахуєте деталі чи забудете якусь частину. Неодноразово стикався із ситуаціями, коли роботу над фічею оцінювали на три дні, а після розбиття на підзадачі згадали, що треба ще тести або документацію дописати і додали
0.5-1 день. Або навпаки — під час оцінювання задачі на фронтенді дійшли висновку, що спільні компоненти вже реалізовані, і естімейт зменшився з 6 до 2 годин. - Відбувається якісніший трекінг прогресу. Команда чітко розуміє, який стан завдання у поточний момент. Якщо є одна задача і вона «висить» три дні у стані in progress, це менш інформативно, ніж те, що робота над двома з п’яти вже завершена. Коли я бачу, що розробник переніс завдання з бекенду в Code Review, розумію, що варто починати оновлювати E2E API тести, фронтенд ще не завершений.
- Ймовірність, що щось піде не так, зменшується. Менше завдання -> менше розробляти -> менше тестувати -> менше деплоїти. Дрібні зміни простіше тестувати (як вручну, так і автотестами). Краще видно, на що звернути увагу і на що реалізовані зміни можуть вплинути. Отже, локалізувати проблему набагато легше та швидше. Не варто запускати всі тести, а лише ті, які пов’язані з функціоналом, що зазнав змін.
Щоб відповісти на запитання, як саме розбивати завдання, треба спершу зрозуміти контекст команди та проєкту. Мені більше подобається розділяти на бізнес-сценарії, а не технічні процеси: хтось робить фронтенд, а хтось — бекенд. Більше схиляюсь до Full Stack підходу. Тоді зникає проблема комунікацій між FE та BE розробником, коли FE очікує один API, а на бекенді він реалізований по-іншому. Один розробник реалізує API з урахуванням фронтенду краще, бо він його й реалізовує.
Повернемося до прикладу з кошиком в онлайн-магазині. Завдання можна розділити за функціоналом: «Додавання у кошик», «Видалення з кошика», «Оплата», «Очищення кошика». Цей приклад, можливо, банальний, але чудово ілюструє мою ідею.
Головне у цій справі — розбивати завдання на підзавдання, а те, як це зробити, краще визначити самій команді.
Чітке розуміння бізнесу
Ми часто не знаємо, хто наш кінцевий користувач і що йому потрібно. Відповідно, десь докладаємо забагато зусиль. А десь навпаки — не приділяємо уваги тим фічам, якими клієнти активно користуються. Вся команда має розуміти, як люди використовують продукт. Я стикався з ситуацією, коли операції завантаження файлу приділили забагато уваги. І кілька спринтів реалізовували підтримку різних форматів, валідацій, recovery тощо. Але виявилось, що у 99% випадках користувачі обирали лише один формат і перезалити файл для них не було проблемою. Водночас ми не зосередилися на формах, які юзери заповнювали, і в базу потрапляли невалідні дані. Тому:
- приділяйте увагу всьому бізнеc-процесу, а не конкретним його частинам. Краще швидше реалізувати весь процес, який не працює повністю, ніж одну частину, яка працює ідеально;
- спілкуйтеся мовою бізнесу. Часто в розмові із замовником чи всередині команди розробники виходять на рівень реалізації і говорять про ендпоїнти, сервіси, репозиторії. Не всім потрібно знати такі деталі. Краще спілкуватись про продукт з погляду користувачів.
Процес тестування
Якщо хочемо релізитись частіше, потрібна впевненість у якості продукту. А для цього треба його протестувати. Новий функціонал, якщо він маленький, можна тестувати вручну. Але оскільки це треба робити на всіх рівнях, на перевірку йтиме все більше й більше часу. Ручні регресії — це дорога в нікуди. Лише баланс тестів на різних рівнях дасть упевненість, що продукт працює так, як очікували.
Автотести — швидкі. Юніт-тести виконуються за секунди, UI можуть одночасно працювати у кількох браузерах і бути відносно швидкими. Вони не йдуть пити каву на кухню, а от ви можете :) Автотести ще й точні: їхнє виконання не змінюється, тест щоразу однаковий, що не завжди скажеш про тест, виконаний вручну.
Хто має тестувати
Часто команда складається з кількох розробників, QA. Якщо замовник готовий вкладати у майбутнє, є ще AQA. Процес приблизно такий: розробник займається функціоналом, передає його тестувальнику і перемикається на інше завдання. У тестувальника не запускається білд, він смикає розробника, вони обоє витрачають час, щоб розібратись, хто і що поламав. Ось ці повернення до, здавалось, уже завершених задач є перемиканням контексту. На це йде багато часу та зусиль. Згадайте, як вам було важко у понеділок знову заглиблюватися у функціонал, який ви завершили у п’ятницю.
Чому так відбувається:
- Розробник не розуміє бізнес і тому не знає, як тестувати функціонал. Він чудово знає, як працює код, що у якому класі лежить, які алгоритми використовуються. Але не розуміє продукт з погляду кінцевого користувача. Відповідно й те, як тестувати. І під час імплементації припускається помилок, які ламають бізнес-логіку. Як це вирішити: якщо розробник проводить поверхове тестування, це допомагає краще розуміти бізнес і користувача.
- Тестування починається до завершення автотестів. Переважно це стосується E2E-тестів, таких як UI, які не запускають після деплою. Як це вирішити: можливо, запускати всі ваші 345 тестів буде зайвим, бо вони виконуються кілька годин. Але якщо вибрати стабільні тести, які покривають основні сценарії та виконуються
10-15 хвилин, то це пришвидшить виявлення дефектів.
Автотести — поняття дуже широке, вони бувають на різних рівнях. Розгляньмо, на яких рівнях і чому треба використовувати тести.
Піраміда тестування
Піраміда тестування — це схема, що розділяє тести за рівнями покриття. З неї ми розуміємо пропорції покриття тестами, час виконання та ціну.
- Що ширший рівень, то більше тестів потрібно зробити.
- Ціна підтримки зростає знизу вверх. Ті, що розташовані нижче — дешевші у підтримці.
- Час виконання падає зверху вниз. Ті, що вище — виконуються довше.
Unit-тести
Популярною є думка, що юніт-тести знаходять баги найшвидше. Але це не так. Юніт-тести не допускають появи дефектів. Якщо хоча б один тест падає — зміни не мають бути реалізовані.
Відповідальність за написання цих тестів на людині, що створює код. Основний мінус юніт-тестів у тому, що вони залежать від реалізації. І коли ви робитимете великий рефакторинг, доведеться чимало переписувати чи викидати.
Integration-тести
Інтеграційні тести не мають чітких визначень, і навколо них завжди точаться дискусії. Їхня реалізація дуже залежить від системи. У моєму розумінні інтеграційний тест — це white box тест, коли працюємо з кодом системи. Його мета — перевірити взаємодію двох чи більше unit-of-work. Можуть використовуватись зовнішні ресурси: in-memory DB чи щось схоже.
Наприклад, такими тестами можна спробувати тестувати взаємодію DAL та рівня бізнес-логіки. Створюємо фейкову БД для DAL, використовуємо моки для бізнес-логіки і перевіряємо інтеграцію. У всьому іншому ці тести будуть схожі на unit-тести.
Service-тести
У стандартній піраміді тестів цього рівня немає. Unit та integration тести — це white-box тести, тобто вони безпосередньо взаємодіють з кодом. А якщо для тестування підняти лише один сервіс, а не весь продукт, і працювати з ним як з black-box через HTTP чи AMQP? Тоді ми не будемо залежати від сервісу. Потрібно зауважити, що ці тести гарно вписуються у мікросервісну архітектуру, де кожен сервіс незалежний. На жаль, працювати з монолітом буде проблемно, адже підняти лише окрему частину неможливо. У таких тестах легко підставити «фейкові» (спеціально створені для підготовки тестів) ресурси, такі як БД чи Kafka. Наприклад, Kafka піднята у Docker-контейнері чи в окремому хмарному середовищі.
Такі тести не можна використати всюди, але якщо розробляєте мікросервіси чи архітектуру з кількох незалежних або мало залежних сервісів — спробуйте їх. Протестувавши один сервіс ізольовано з фейковими ресурсами, будемо впевненими, що він працює коректно. І якщо так буде з кожним сервісом, можна припустити, що бізнес-логіка працює. Звісно, іноді вилазить проблема — 2 unit tests pass, 1 integration fail. І щоб цього уникнути, є наступні рівні тестів.
Відповідальність за такі тести я б теж покладав на розробників, адже часто компонент чи сервіс є технічним і його нелегко зрозуміти з погляду користувача. Наприклад, коли це Orchestrator Service, що координує роботу кількох сервісів. Його призначення є суто технічним, і без розуміння архітектури неможливо збагнути логіку.
Але також раджу розділяти цю відповідальність з тестувальниками, які проводять рев’ю тестів чи доповнюють їх.
Contract-тести
Чи траплялося у вас, що сервіс, від якого залежите, змінив свій API? Contract-тести запобігають схожим проблемам. Ідея в тому, що в тесті є дві сторони: Provider і Consumer.
Provider — це сервіс, який надає свій API. За допомогою тесту описується контракт — структура HTTP-запиту та відповіді. Consumer — це сервіс чи сервіси, які споживають API від провайдера. Кожен Consumer описує свій контракт. Ці контракти зіставляюся з контрактом провайдера, і, якщо API змінився, тест падає. Такі тести популярні саме у мікросервісних архітектурах. Але підійдуть і системам, де комунікація між компонентами відбувається за допомогою HTTP.
Для реалізації тестів часто використовується фреймворк Packt. У деяких реалізаціях є підтримка не лише HTTP-протоколу, а й AMQP. Якщо у вашій архітектурі провайдер має лише одного споживача, ці тести стають зайвими, бо зусиль на їхню підтримку витрачаєш більше, ніж отримуєш користі. Відповідальність у цьому разі теж за тими, хто пише код.
End-to-end тести
У цю категорію потрапляють тести, що виконують повноцінний сценарій так само, як кінцевий користувач. Переважно тут ми працюємо з системою, яку задеплоїли. Сюди належать як API, так і UI-тести.
Ці тести переважно розробляють і підтримують AQA. Але неправильно покладати на них усю відповідальність. Щонайменше до рев’ю треба залучати розробників, які ознайомлені з тестами та можуть розробляти нові.
Часто на початку проєкту стоїть питання — яку мову програмування обрати? І нерідко є один з двох варіантів: або ту, яку використовував AQA на попередньому проєкті, або ту, якою написаний бекенд. Під час вибору мови програмування потрібно зважати на навички інженерів, які будуть розробляти та підтримувати тести. Якщо розробники готові допомагати з підтримкою E2E-тестів, поцікавтесь, якою мовою їм буде зручніше їх бачити. Не думаю, що Ruby-фахівці будуть дуже раді, що ви почали писати тести на C#. Щоб написати тест чи навіть запустити його, розробникам потрібна буде платформа .NET і Visual Studio. Тому, можливо, краще обрати щось з ближчої до них екосистеми.
Варто по-різному підходити до API-тестів і UI-тестів.
API-тести є швидшими та більш стабільними, ніж UI. З їхньою допомогою зручно покривати різні комбінації вхідних даних. Відповідно, якби ми покривали такі сценарії через UI, виконання тестів зайняло б багато часу. Для API-тестів це будуть секунди.
За останні роки з’явились нові інструменти для UI-тестування, і WebDriver, можливо, скоро не буде де-факто стандартом для UI-автоматизації. Такі інструменти, як Cypress, Playwright і Puppeteer, завоювали підтримку ком’юніті. Одна з основних переваг — можливість перехоплювати трафік вебзастосунку і змінювати відповіді бекенду. Під час E2E-тестування проблемою стає підготовка та перевикористання тестових даних. Іноді на створення цих даних чи інфраструктури для їхнього автоматичного створення йшло більше часу, ніж на сам тест. Тому можливість змінювати відповідь бекенду допоможе мінімізувати цю проблему. Звісно, не варто цим зловживати, пам’ятайте, що у вас мають бути тести й зі всіма реальними компонентами.
Антипатерни AQA
Тести, що цікаві лише автоматизаторам. Пам’ятаєте бородаті анекдоти про сисадмінів, котрі щось роблять, але ніхто не знає що? Часто AQA стають такими. Вони працюють окремо від команди, пишуть якісь свої тести, самі їх запускають, самі фіксять, і результат нікому не цікавий. Це велика проблема. Тести повинні допомагати всім учасникам команди. Насамперед розробникам.
Уся команда має знати про тести: які вони, де їх знайти, як запустити й де переглянути результат. Це мінімум.
Тести, створені окремо від функціоналу. Мені не подобаються фрази у стилі «у нас автотести відстають від продукту, бо все може змінитись». Якщо все може змінитись, то, може, не варто писати код чи тестувати? Тести треба створювати разом із функціоналом. І важливо, щоб їх можна було легко модифікувати. У такому разі, навіть якщо щось зміниться із часом — це буде легко оновити.
Багато нестабільних тестів. Часто показником є кількісна характеристика, а не якісна. І якщо у вас є 345 тестів і при кожному запуску більша частина з них падає через нестабільність — вигода від автоматизації сумнівна. Краще мати набагато меншу кількість тестів, але стабільних на 99%. Нестабільні тести — це гірше, ніж відсутність тестів, тому що з часом їм перестануть довіряти та заб’ють на них.
Гігантські сценарії. Не варто створювати автотест на десятки кроків. Особливо це стосується UI-тестів, що покривають рідкісний сценарій. Великий тест важко підтримувати, він буває нестабільний і довго виконується. Уникайте таких тестів або оптимізуйте їх за допомогою API чи БД.
Аутсорсинг тест-кейсів. Не варто аутсорсити написання тест-кейсів Manual QA. Це може призвести до того, що ви не будете розуміти бізнес-логіку і писатимете неефективні тести, бо людина, яка писала цей тест-кейс, не знає, як працює автоматизація.
Best practices AQA
Я вважаю, що автоматизація тестування пришвидшує розробку. Але щоб цього добитись, потрібно уникати антипатернів. Натомість варто:
- приєднати AQA в команду і залучати до всіх активностей;
- долучати розробників і тестувальників до розробки автотестів. Щонайменше для рев’ю, а краще і до написання нових;
- не робити 100% покриття коду чи вимог;
- писати швидкі та стабільні тести;
- покривати функціонал тестами одразу.
Summary
У цій статті я хотів поділитись досвідом, як наскрізне тестування може поліпшити процес розробки. Не очікуйте, що воно одразу збільшить кількість story point’ів, які ви закриваєте за спринт. Це інвестиція у майбутнє.
На початку впровадження цих змін виникне багато проблем — як технічних, так і людських. Людям складно змінювати погляди, особливо якщо раніше вони не інвестували в тестування. Будьте готові до опору.
Також врахуйте, що написання та підтримка тестів займатиме багато часу. Але тестування на різних рівнях зменшить кількість дефектів регресії та збільшить упевненість у ваших діях. Ви не будете боятись змінювати старий код, якщо він покритий тестами. І не отримаєте сюрприз у вигляді дефекту в найбільш неочікуваному місці. Тож не буде ручних регресій, перемикання контекстів, а кількість багів зменшиться.
Отже, найголовніше:
- Комунікуйте та спілкуйтесь. Залучайте всіх членів команди до релевантних для них дискусій.
- Пишіть тести.
- Запускайте ці тести часто.
- Обмінюйтесь обов’язками та розділяйте відповідальність.