Юніт-тести Android: де ламається логіка і як це ловити
Логіка Android-застосунку ламається не у верстці, а в асинхронних сценаріях. Екран відкрився, користувач пішов у фон, повернувся — і бачить застарілі дані, бо відповідь мережі прийшла після скасування запиту.
Такі дефекти не відтворюються вручну стабільно: вони залежать від порядку подій, швидкості мережі та життєвого циклу.
Юніт-тести Android фіксують саме цей порядок — підміняють мережу й сховище, подають відповіді в потрібній послідовності та перевіряють, що стан екрана набуває очікуваного вигляду.
Друге вузьке місце — стан екрана. Завантаження, помилка, порожній результат і успіх часто змішуються в одному флазі, і при швидкому перемиканні інтерфейс показує комбінацію, якої не повинно бути: спінер поверх помилки або порожній список замість даних із кешу. Тест на переходи станів ловить такі комбінації до того, як їх побачить користувач.
Третє — обробка помилок і кеш. Що відбувається при таймауті, при невалідній відповіді, при розсинхронізації локальних даних із сервером? Без покриття логіки ці гілки залишаються неперевіреними, і перша ж нештатна ситуація йде в реліз як регресія.
Юніт-тести проганяють кожну гілку ізольовано — без емулятора й пристрою, за секунди, тому їх можна ганяти на кожну зміну.
Сенс не у відсотках покриття заради звітності, а в тому, які класи відмов закриті. Ми покриваємо насамперед асинхронні сценарії, переходи стану екрана, роботу з кешем і обробку помилок — те, що найчастіше ламається при доопрацюваннях. Далі розбираємо, як це влаштовано в проєкті.
Що дає покриття тестами: вимірюваний ефект
Юніт-тести Android дають команді те, чого не дає жоден етап ручного тестування — зворотний зв'язок за секунди замість годин.
Поки збірка та прогін на емуляторі тривають хвилинами, набір модульних перевірок на JUnit і MockK відпрацьовує за час, порівнянний із компіляцією, і розробник дізнається про поломку одразу після збереження файлу, а не на етапі регресії перед викладкою.
Це змінює саму механіку роботи: правка в бізнес-логіці перестає бути ризиком, бо її наслідки видно негайно.
Частка дефектів, які виявляються до код-рев'ю, зростає передбачувано — граничні значення, порожні відповіді, помилки парсингу та гілки обробки винятків закриваються тестами на рівні функцій і класів.
Чим щільніше покриті сценарії ViewModel і шару роботи з даними, тим менше помилок доходить до збірки і тим рідше реліз гальмується через знахідки на прийманні.
Стабільність релізів — прямий наслідок. Коли логіка зафіксована тестами, рефакторинг і оновлення залежностей перестають бути лотереєю: будь-яка розбіжність у поведінці спливає на прогоні, а не в користувачів. Відкат і повторна викладка потрібні рідше, а сам цикл випуску стає коротшим.
Ми вибудовуємо покриття там, де воно окупається: критичні сценарії, розрахунки, робота з мережею та сховищем — під тестами, другорядні екрани не гальмують роботу команди зайвими перевірками. Опишіть задачу — повернемося з оцінкою та планом покриття під ваш проєкт.
Підходи до тестування: порівняння варіантів
Вибір підходу визначається тим, що саме перевіряємо і наскільки дорого нам дається зворотний зв'язок. Чисті юніт-тести ізолюють логіку без Android-оточення і дають результат за секунди, але не ловлять помилки стикування шарів.
Тести з підміною залежностей закривають взаємодію з мережею, сховищем і системними сервісами, залишаючись швидкими. Інтеграційні перевірки збирають реальні компоненти і підтверджують, що модулі працюють разом — ціною часу прогону і крихкості до змін оточення.
| Підхід | Коли підходить | Обмеження |
|---|---|---|
| Чисті юніт-тести | Бізнес-логіка, розрахунки, розбір відповідей, граничні умови | Не перевіряють реальні залежності та поведінку платформи |
| Тести з підміною залежностей | Сценарії з мережею, базою, системними сервісами; перевірка розгалужень і помилок | Підміна може розходитися з реальною поведінкою залежності |
| Інтеграційні перевірки | Стикування шарів, робота зі сховищем, наскрізні сценарії | Довгий прогін, чутливість до оточення, складніше локалізувати збій |
Як поєднувати підходи
Практика — піраміда: основа з чистих юніт-тестів на бізнес-логіку, середній шар з підміною залежностей на взаємодії, вершина з інтеграційних перевірок на критичні шляхи.
Такий розподіл тримає прогін швидким, а покриття осмисленим: більшість дефектів відловлюється на нижніх шарах, де їх дешевше виправляти.
Обмеження, які важливо врахувати
Жоден підхід не замінює решту. Чисті тести не зловлять несумісність версій бібліотек, підміна — розходження контракту з реальним сервісом, інтеграційні перевірки — рідкісні граничні випадки логіки. Тому набір будується під ризики конкретного застосунку, а не за шаблоном.
Як ми впроваджуємо тести: етапи роботи
Впровадження тестів починається не з написання коду, а з інвентаризації: які модулі взагалі існують, де зосереджена логіка, а де — лише розмітка екранів.
На цьому етапі ми дивимося історію змін і знаходимо класи, які правляться найчастіше: саме там помилки коштують найдорожче, а тести дають найбільший ефект.
- Аудит коду. Розбираємо структуру проєкту, залежності між модулями, якість розділення шарів. Відзначаємо місця, де логіка перемішана зі зверненням до мережі або бази, — їх доведеться трохи розділити, інакше тест не ізолювати без зовнішніх систем.
- Схема покриття. Фіксуємо, що саме перевіряємо: розрахунки, перетворення даних, обробку помилок, граничні значення. Домовляємося про цільову частку покриття за критичними модулями — не «все підряд», а там, де ціна помилки вища.
- Налаштування середовища. Піднімаємо тестовий контур у збірці: окремий набір задач, звіт про покриття, автоматичний запуск на кожну зміну. Тести, які потрібно запускати вручну, перестають запускати через два тижні.
- Написання тестів. Йдемо від критичного до другорядного. Кожен тест перевіряє одну поведінку і не залежить від порядку запуску. Зовнішні залежності підмінюємо керованими двійниками, тому набір проходить за секунди і не потребує мережі.
- Перевірка на реальних сценаріях. Прогоняємо набір на змінах, які вже ламали застосунок раніше, — переконуємося, що тести їх ловлять. Тест, який завжди зелений, гірший за відсутній: він створює хибну впевненість.
- Передача. Віддаємо набір, інструкцію із запуску, правила додавання нових тестів і розбір типових помилок. Проводимо розбір із вашою командою, щоб підтримка не залежала від нас.
Ключовий критерій на виході — не кількість тестів, а час до виявлення регресії: помилка має спливати на збірці, а не в магазині застосунків. Опишіть задачу — повернемося з оцінкою та планом за вашим проєктом.
Артефакти та регламенти: що залишається в проєкті
Після здачі проєкту в репозиторії залишається не «набір тестів», а робочий контур перевірок, який можна запускати без автора. Нижче — що саме передаємо на руки і як цим користується команда замовника.
- Тестові набори за шарами. Домен і бізнес-логіка покриті швидкими тестами без Android-залежностей, робота зі сховищем і мережею — окремими наборами з підміною зовнішніх викликів. Кожен набір запускається ізольовано, тому падіння локалізується за секунди, а не за прогін усього проєкту.
- Конфігурації запуску. Готові профілі для локального прогону, для перевірок на кожній зміні та для нічної збірки з повним покриттям. Розробник запускає потрібний профіль однією командою, без ручного налаштування середовища.
- Схеми залежностей. Діаграми, що показують, які модулі від чого залежать і де проходить межа підміни. За ними видно, чому тест падає при зміні конкретного класу, і куди додавати нові перевірки.
- Звіти про покриття. Налаштований зведений звіт за модулями з порогом, нижче якого збірка не проходить. Видно, які гілки логіки не перевірені, — це список задач, а не абстрактна цифра.
- Документація. Угоди про іменування тестів, правила оформлення тестових даних, опис того, що вважається достатнім покриттям для нового коду.
- Регламенти запуску. Коли тести обов'язкові, що блокує злиття змін, хто розбирає упалі перевірки і в який термін. Правила зафіксовані в репозиторії, а не в листуванні.
- Інструкція з супроводу. Як додати тест під нову функціональність, як оновлювати підміни при зміні зовнішніх сервісів, що робити при нестабільному тесті.
Усе це лежить поруч із кодом і версіонується разом із ним. Новий інженер у команді розгортає середовище і отримує зелений прогін першого ж дня — без усних пояснень від автора.
Кейс: скорочення регресій після впровадження тестів
Проєкт — Android-застосунок з оплатою та підписками, реліз раз на два тижні. Тести були, але писалися через раз і здебільшого на «чисті» функції: логіка оплати, розрахунку знижок і відновлення підписки залишалася непокритою.
Регресії ловили на ручному прогоні перед викладкою, і приблизно кожна четверта доходила до користувачів — найчастіше у вигляді повторних списань і «завислих» статусів замовлення.
Що змінили
Розділили логіку та платформний код: бізнес-правила винесли в окремі класи без залежностей від Android-оточення, а зовнішні сервіси — оплату, сховище, мережу — закрили інтерфейсами.
Далі на MockK описали поведінку цих інтерфейсів, включно з відмовами та таймаутами, а JUnit 5 дав параметризовані перевірки: один набір граничних значень замість десятка майже однакових методів.
Тести ганяються на кожен коміт, тому збірка падає до рев'ю, а не після. Окремо зафіксували правило: нова гілка логіки оплати не проходить перевірку, якщо не покрита сценаріями помилок — не лише успішним шляхом.
Метрики до і після
| Показник | До | Після |
|---|---|---|
| Частка дефектів, що дійшли до користувачів | ~25% | ~6% |
| Ручна перевірка релізу | 2 дні | 4-6 годин |
| Збірки з падінням на інтеграції | кожна третя | поодинокі |
| Покриття логіки оплати | фрагментарне | основні та помилкові сценарії |
Ручний прогін скоротився не тому, що перевірок стало менше, а тому що критичні шляхи тепер відсікаються автоматично.
Час, що звільнився, пішов на перевірку того, що тестами не закривається: поведінка на реальних пристроях, робота з платіжними діалогами, відновлення після згортання застосунку.
Через два релізи частка дефектів стабілізувалася на новому рівні, а відкат версії перестав бути штатним сценарієм.
Чим юніт-тести відрізняються від інтеграційних?
Юніт-тести перевіряють один модуль в ізоляції: усі зовнішні залежності — репозиторії, мережеві клієнти, системні сервіси — підмінюються керованими двійниками. Запускаються такі перевірки на JVM, без емулятора й пристрою, тому весь набір із сотень сценаріїв проходить за секунди.
Ізоляція модулів тут не самоціль: вона прибирає хибні падіння, коли тест падає через недоступний сервер або чужий код, а не через помилку в логіці, яку перевіряють.
Інтеграційні тести працюють на стику модулів і перевіряють, що вони справді стикуються: серіалізація запиту й розбір відповіді, поведінка кешу при зміні даних, коректність запитів до локальної бази.
Тут уже піднімається реальне оточення — тестова база, підставний сервер, іноді емулятор. Такі перевірки тривають довше й падають частіше, зате ловлять клас помилок, який ізольований тест у принципі не побачить.
Межа перевірок проходить по питанню «чия це відповідальність». Логіка розрахунку, валідація полів, розбір станів екрана, перетворення моделей — це юніт-тести, бо результат залежить лише від вхідних даних.
Взаємодія з мережею, базою, файловою системою, дозволами й життєвим циклом — інтеграційні, бо результат залежить від оточення. Змішувати їх в одному наборі не можна: втрачається швидкість і точність локалізації помилки.
На практиці набір будується пірамідою: основна маса швидких ізольованих перевірок на бізнес-логіку, поверх — інтеграційні на критичні стики, і трохи наскрізних сценаріїв на головні користувацькі шляхи.
Такий розподіл дає ранній зворотний зв'язок у процесі розробки й не перетворює прогін перед релізом на багатогодинне очікування. Якщо межа розмита й усе звалюється в інтеграційні тести, команда перестає запускати їх локально — і перевірки перестають захищати код.
Часті технічні сумніви
Тестованість Android-додатка рідко впирається в сам фреймворк — вона впирається в архітектуру, яка склалася до того, як про тести взагалі задумалися.
Нижче — чотири питання, які технічні керівники ставлять найчастіше, і те, як ми на них відповідаємо на рівні коду, а не загальних слів.
У нас легасі-код без тестів — з чого ви починаєте?
Спочатку фіксуємо поточну поведінку тестами-характеристиками: покриваємо найбільш навантажені сценарії так, як вони працюють зараз, включно з дивацтвами. Це страховка перед рефакторингом — після кожної зміни прогін показує, що поведінка не поїхала.
Далі виносимо логіку з актівіті та фрагментів в окремі класи, які можна перевірити без емулятора: тест на чистій логіці виконується за частки секунди, і весь прогін перестає бути вузьким місцем збірки.
Як тестувати асинхронність і корутини, щоб тести не «блимали»?
Підмінюємо диспетчери на тестовий планувальник і просуваємо віртуальний час вручну — тест перестає залежати від реальних затримок і мережевих таймаутів.
Асинхронний код перевіряється детерміновано: один і той самий прогін дає один і той самий результат і на локальній машині, і на сервері збірки. Окремо покриваємо скасування корутин і обробку помилок — саме там найчастіше ховаються дефекти, які не ловляться ручним тестуванням.
Наскільки глибоко ви підмінюєте залежності?
Ми ізолюємо зовнішні межі — мережу, базу, файлову систему, системний час — і залишаємо справжньою ту логіку, яку перевіряємо. Мок на кожен чх робить тест крихким: він падає при невинному рефакторингу і нічого не говорить про поведінку.
Підміна залежностей будується на інтерфейсах, тому тест переживає зміну мережевого клієнта або сховища без переписування.
Хто підтримує тести після здачі проєкту?
Тести — частина кодової бази, а не окремий артефакт: вони лежать поруч із кодом, проходять рев'ю в тому самому запиті на злиття і запускаються на кожній зміні.
Ми передаємо команді угоди про структуру тестів і розбір типових сценаріїв, щоб новий розробник писав їх у тому самому стилі. Якщо тест застаріває разом із вимогою — він правиться або видаляється, а не вимикається назавжди.
Опишіть вашу кодову базу та поточне покриття — повернемося з оцінкою та планом, які ділянки закривати в першу чергу.
Обговоримо задачу та повернемося з планом
Щоб повернутися з планом, а не із загальними словами, нам потрібно зрозуміти будову вашого Android-проєкту та поточний стан тестів.
Надішліть доступ до репозиторію або вивантаження модулів — подивимося, як розбито код за шарами, де проходить межа з мережею та сховищем, які залежності впроваджуються через конструктори, а які тягнуться статикою.
Від цього залежить, де юніт-тести ляжуть без переписування продакшн-коду, а де потрібно спочатку розв'язати пов'язані компоненти.
Окремо уточнимо вимоги: які сценарії для вас критичні (розрахунок бізнес-логіки, обробка помилок мережі, кеш, валідація), який рівень покриття вважаєте достатнім і чи є вже тести, які не можна ламати.
Якщо в проєкті використовується DI-контейнер та асинхронність на корутинах, скажіть — під неї налаштовується тестовий диспетчер, щоб перевірки йшли детерміновано, без випадкових падінь.
Що підготуємо до наступного кроку:
- карту модулів і шарів, де тести дають максимальний ефект при мінімальних правках;
- перелік класів під перевірку з пріоритетом за ризиком регресій;
- схему моків для зовнішніх залежностей — мережа, сховище, системний час;
- правила ізоляції тестів, щоб вони не залежали від порядку запуску;
- конфігурацію запуску в збірці та звіт про покриття;
- оцінку обсягу робіт за етапами з точками проміжної приймання;
- план впровадження перевірок у конвеєр збірки, щоб регресії ловилися до релізу.
Опишіть задачу у вільній формі: що за застосунок, на якій стадії розробки, що болить зараз — часті регресії, страх перед рефакторингом або підготовка до передачі коду іншій команді. Цього достатньо, щоб повернутися з оцінкою, планом робіт і пропозицією щодо наступного кроку.







