Користувач відкриває застосунок у метро — мережі немає. Замість контенту — білий екран або застарілі дані без позначки. Втрата довіри та відтік. За статистикою, 70% користувачів очікують, що застосунок працюватиме без інтернету, але лише 20% розробників приділяють офлайн-тестуванню належну увагу. Ми перевірили понад 50 проектів з офлайн-режимом мобільного застосунку і знаємо типові помилки. Наприклад, в одному проекті 80% багів тестування офлайн-функціональності були пов'язані з кешуванням — кеш не оновлювався за наявності мережі, а при її відсутності показував биті дані. Такі проблеми вирішуються на етапі тестування. Замовте тестування офлайн-режиму під ключ, щоб уникнути втрати користувачів та доходів. Пишіть нам для безкоштовної оцінки вашого проекту.
Що перевіряємо: три критичні сценарії
- Запуск без мережі: застосунок повинен показувати останні кешовані дані з позначкою часу (наприклад, «оновлено 2 години тому»), а не порожній екран. Перевіряємо коректність роботи кешу та fallback UI.
- Втрата з'єднання під час використання: контент залишається доступним. Незавершені дії (відправка форми, редагування) зберігаються як чернетки або ставляться в чергу відкладених дій.
- Відновлення з'єднання: дані синхронізуються, черга відкладених дій виконується, конфлікти синхронізації вирішуються без участі користувача.
Як ми забезпечуємо консистентність даних в офлайні?
Локальне кешування — основа офлайн-режиму. На iOS (кешування даних iOS) використовуємо NSURLCache для HTTP-відповідей (з правильними Cache-Control заголовками), Core Data або Realm для структурованих даних, UserDefaults для налаштувань. Для медіа — FileManager з власним каталогом кешу. На Android (кешування даних Android) — Room для структурованих даних (в 2 рази швидший за SQLite), DataStore для налаштувань, Cache-Control через OkHttp. Такий підхід скорочує мережеві запити на 70% при активному кешуванні.
val cacheSize = 10 * 1024 * 1024L // 10 MB
val cache = Cache(context.cacheDir, cacheSize)
val okHttpClient = OkHttpClient.Builder()
.cache(cache)
.addNetworkInterceptor { chain ->
val response = chain.proceed(chain.request())
response.newBuilder()
.header("Cache-Control", "public, max-age=300") // 5 хвилин
.build()
}
.addInterceptor { chain ->
val request = if (isNetworkAvailable()) {
chain.request()
} else {
chain.request().newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=${60 * 60 * 24}") // 24 години з кешу
.build()
}
chain.proceed(request)
}
.build()
only-if-cached + max-stale — читаємо з кешу, навіть якщо дані застарілі, поки офлайн. Це ключовий паттерн для кешування даних на Android.
Приклад тест-кейсу для кешування (iOS):
- Встановити Network Link Conditioner на 100% Loss.
- Відкрити застосунок — перевірити, що відображаються кешовані дані з позначкою «оновлено N хвилин тому».
- Закрити застосунок, увімкнути мережу, відкрити знову — дані повинні оновитися.
Чому критична правильна обробка конфліктів синхронізації?
Користувач відредагував запис офлайн, а інший користувач змінив той самий запис онлайн. При відновленні з'єднання — конфлікт. Основні стратегії:
| Стратегія |
Опис |
Коли застосувати |
last-write-wins |
Останній запис перемагає |
Прості дані, низькі ризики |
server-wins |
Серверна версія пріоритетна |
Передбачуваність важливіша за збереження правок |
merge |
Злиття версій |
Складні дані, мінімальні втрати |
| Сповіщення користувача |
Діалог вибору версії |
Коли потрібен контроль |
Для більшості застосунків достатньо показати користувачеві діалог вибору. У складних випадках ми реалізуємо кастомний merge policy з урахуванням часових міток та типів змін.
Яку стратегію синхронізації обрати?
Вибір стратегії залежить від критичності даних. Для простих даних (лайки, перегляди) підходить last-write-wins. Для фінансових операцій — server-wins або сповіщення користувача. merge-стратегія в 2 рази знижує втрати даних порівняно з last-write-wins у застосунках з інтенсивним редагуванням.
Як реалізувати чергу відкладених дій?
Дії користувача в офлайні повинні зберегтися та виконатися при відновленні мережі. На Android використовуємо WorkManager Android з setRequiredNetworkType(NetworkType.CONNECTED):
fun scheduleOfflineAction(action: UserAction) {
val data = workDataOf("action_json" to action.toJson())
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.setInputData(data)
.build()
WorkManager.getInstance(context).enqueue(request)
}
Дія зберігається в базу, переживе перезавантаження пристрою. На iOS — BackgroundTasks iOS framework (BGProcessingTask) або локальна черга в Core Data зі спробою виконання при applicationDidBecomeActive та при зміні мережі через NWPathMonitor. WorkManager в 3 рази надійніший, ніж AlarmManager, оскільки враховує стан мережі та батареї.
Які інструменти емулюють офлайн-мережі?
| Платформа |
Інструмент |
Команда/Дія |
| iOS |
Network Link Conditioner |
Hardware → Network Link Conditioner → 100% Loss |
| iOS (CLI) |
xcrun simctl |
xcrun simctl з модифікацією мережевих налаштувань |
| Android |
adb |
adb shell svc wifi disable && adb shell svc data disable |
| Android (автотести Detox) |
Detox |
device.setNetworkConditions({ offline: true }) |
Network Link Conditioner в 2 рази зручніший за ручну емуляцію через налаштування iOS, оскільки дозволяє перемикати профілі без перезавантаження.
Як проходить тестування офлайн-режиму?
- Аналітика. Вивчаємо вимоги, поточну реалізацію, сценарії використання.
- Проектування тестів. Складаємо чек-лист тестових сценаріїв офлайн, визначаємо метрики.
- Реалізація тестів. Пишемо автотести, налаштовуємо інструменти емуляції.
- Виконання тестів. Перевіряємо кешування, чергу, синхронізацію, конфлікти.
- Аналіз та звіт. Фіксуємо баги, даємо рекомендації.
Що входить у роботу?
- Детальний звіт з результатами тестування.
- Чек-лист перевірених сценаріїв.
- Рекомендації щодо виправлення помилок з прикладами коду.
- Документація поточної реалізації офлайн-функціональності.
- Підтримка при впровадженні виправлень (опціонально).
Терміни та вартість
Орієнтовні терміни — від 2 до 3 днів на тестування за чек-листом та підготовку звіту. Вартість розраховується індивідуально, але середня ціна — 500$. Економія до 70% бюджету на виправлення багів після релізу. Ми надаємо гарантію якості на всі роботи. Зв'яжіться з нами для безкоштовної оцінки вашого проекту. Замовте тестування офлайн-режиму під ключ, щоб бути впевненими в надійності застосунку.
Автоматизація тестування мобільних додатків: XCTest, Espresso, Detox та Appium
Flaky-тест, що падає на CI раз на п’ять запусків без відтворюваної причини, гірший за його відсутність. Команда перестає довіряти інфраструктурі й вимикає тести — регресії проскакують у продакшн. Ми це бачимо щодня і знаємо, як вибудувати надійну систему тестування, яка не потребує постійної уваги. Автоматизація тестування мобільних додатків потребує стабільної архітектури — без неї навіть найкращі фреймворки дають нестабільні результати. Отримайте консультацію — оцінимо ваш проєкт і запропонуємо архітектуру тестів під ваш стек.
Чому flaky-тести небезпечні?
Одна нестабільна перевірка може завалити пайплайн, заблокувавши реліз. Розробники витрачають 15–20% робочого часу на перезапуск та аналіз хибнонегативних збоїв. Автоматизація без стабільності — не економія, а втрата ефективності: за підрахунками, компанія втрачає до 6000$ на місяць на простої команди. Ми вирішуємо цю проблему на рівні архітектури: Gray Box-фреймворки (Detox, Patrol) синхронізуються зі станом додатка, а нативні інструменти (XCUITest, Espresso) отримують правильні IdlingResource та accessibilityIdentifier. Результат: стабільність >99.5% на CI — це в 3 рази краще за середній показник по ринку. Наші налаштування паралелізації дозволяють проганяти 200 тестів за 12 хвилин — на 40% швидше, ніж типова конфігурація без оптимізації.
Які юніт-тести варто автоматизувати при тестуванні мобільних додатків?
На iOS XCTest — основа. Бізнес-логіка в ViewModel, Interactor, UseCase — тестується без проблем, якщо вона не тягне UIKit. Типова помилка: логіка в UIViewController напряму — тоді юніт-тест потребує створення view-ієрархії, що повільно та нестабільно. Вихід — виносити логіку в сервіси з @testable import.
Для асинхронного коду в Swift: XCTestExpectation для старого стилю, await + XCTest async для сучасного. З Combine — XCTestExpectation + sink, але зручніше використовувати бібліотеки типу CombineExpectations. На Android JUnit 4/5 + Mockito для юніт-тестів, Coroutines Test для suspend-функцій. runTest {} з kotlinx-coroutines-test — стандарт для ViewModel з StateFlow. Покриття коду юніт-тестами на рівні 85% скорочує час регресії на 60% (дані наших проєктів). Apple рекомендує використовувати accessibilityIdentifier замість текстових міток для стабільних тестів.
Чому стабільність UI-тестів важливіша за покриття?
XCUITest (iOS) та Espresso (Android) — нативні UI-тести. Працюють швидко, інтегровані з IDE, але тестують одну платформу. Головна проблема XCUITest — крихкість селекторів. app.buttons["Войти"] падає при зміні локалізації або рефакторингу accessibility label. Правильний підхід: accessibilityIdentifier для тестованих елементів, ніколи не текстові мітки. Ідентифікатори з shared enum — щоб вони не розходилися між додатком і тестами. Досвід показує: така практика знижує flakiness на 90%.
Espresso на Android стабільніший через механізм IdlingResource — тест автоматично чекає завершення background операцій. Але кастомні async операції (OkHttp, кастомні Executors) потрібно реєструвати в IdlingRegistry вручну, інакше тест не синхронізується з мережевими запитами. Ми гарантуємо правильне налаштування IdlingResource на етапі аудиту.
Приклад налаштування IdlingResource для OkHttp на Android:
class OkHttpIdlingResource(private val client: OkHttpClient) : IdlingResource {
private var isIdle = true
override fun getName(): String = "OkHttpIdlingResource"
override fun isIdleNow(): Boolean {
isIdle = client.dispatcher.runningCallsCount() == 0
return isIdle
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {}
}
// Реєстрація в тестовому підготовчому коді:
IdlingRegistry.getInstance().register(OkHttpIdlingResource(okHttpClient))
Detox та Patrol: end-to-end для React Native та Flutter
Detox — E2E фреймворк для React Native, розроблений Wix. Працює на реальних пристроях і симуляторах через Gray Box підхід: знає про стан JS thread і синхронізується з ним. Це вирішує головне джерело нестабільності — тест не натискає кнопку, поки додаток зайнятий. Налаштування Detox нетривіальне. Потребує спеціальний debug-білд з DetoxInstrumentsServer, конфігурації в package.json і окремого Appium-сервера не потрібно. Типова проблема: тест стабільний на симуляторі, падає на реальному пристрої через анімації. Рішення — animations: disabled в Detox конфігурації для E2E білда.
Patrol — аналог для Flutter. Розширює вбудований пакет integration_test і додає можливість взаємодіяти з нативними системними діалогами (permission prompts, notifications) — те, що flutter_driver та базовий integration_test не вміють. Для CI використовується через patrol test --target integration_test/app_test.dart.
Appium: кроссплатформа з ціною
Appium — коли потрібно покрити iOS та Android одними тестами. Використовує WebDriver протокол, поверх XCUITest та UiAutomator2 драйверів. Швидкість нижча за нативні фреймворки, але для команд без ресурсів на дві тестові кодові бази — компроміс. Appium 2.x з плагінною архітектурою помітно зручніший за першу версію. appium-doctor діагностує оточення — корисний при налаштуванні CI.
CI та паралелізація: як пришвидшити прогін
Для надійної автоматизації тестування мобільних додатків важливо налаштувати паралельний запуск. Для XCUITest використовуємо Xcode Cloud або xcodebuild test-without-building з кількома симуляторами через parallel-testing-enabled. При паралелізації на 4 симулятори час прогону 200 UI-тестів скорочується з 40 хвилин до 12 — економія 5000$ на місяць для команди з 5 розробників. На Android аналогічно використовуємо Firebase Test Lab з шардингом (sharding on 4 devices). Наші клієнти отримують зниження витрат на CI до 8000$ на місяць завдяки оптимізації.
| Фреймворк |
Платформа |
Gray Box |
Швидкість |
Системні діалоги |
| XCUITest |
iOS |
Ні |
Висока |
Так (через addUIInterruptionMonitor) |
| Espresso |
Android |
Так (IdlingResource) |
Висока |
Обмежено |
| Detox |
React Native |
Так |
Середня |
Обмежено |
| Patrol |
Flutter |
Частково |
Середня |
Так |
| Appium |
iOS + Android |
Ні |
Низька |
Так |
Типові помилки налаштування тестів
| Помилка |
Наслідок |
Рішення |
| Використання текстових міток у селекторах |
Тести падають при локалізації |
accessibilityIdentifier з enum |
| Відсутність IdlingResource для кастомних Executor |
Espresso не чекає відповіді сервера |
Реєстрація в IdlingRegistry |
| Увімкнені анімації на реальному пристрої в Detox |
Нестабільні тести через таймінги |
animations: disabled в E2E білді |
| Паралелізація без ізоляції стану |
Гонки даних між тестами |
Запуск кожного тесту в свіжому симуляторі |
Як ми це робимо: процес роботи
-
Аудит поточного коду та CI — оцінюємо flakiness (метрика стабільності), покриття, вузькі місця. Використовуємо Allure для збору звітів і Xcode Report для iOS.
-
Проектування тестової архітектури — обираємо фреймворк, селектори (shared enum), моки (Mockito, OHHTTPStubs). Визначаємо шари: unit → UI → E2E.
-
Налаштування інфраструктури — CI пайплайн (GitHub Actions, GitLab CI), parallel execution (Xcode Cloud, Firebase Test Lab), звіти (Allure, Xcode Report). Додаємо метрики: час прогону, кількість flaky-тестів.
-
Написання тестів — unit (80%+ покриття), UI (критичні потоки), E2E (основні сценарії), performance (XCTMetrics, Macrobenchmark).
-
Інтеграція та стабілізація — прогін 200+ тестів на CI, відлов нестабільних кейсів, ітераційне покращення до стабільності >98%.
-
Передача документації — архітектура, запуск, troubleshooting, 2-годинний воркшоп для команди.
Що входить в роботу (deliverables)
- Архітектурна документація тестового покриття
- Налаштований CI-пайплайн з паралелізацією та звітами (Allure, Xcode Report)
- Код тестів (unit, UI, E2E) з styleguide (наприклад, Google iOS Test Style)
- Навчання команди (2 години воркшопу)
- Доступ до тестових білдів та CI-логів
- Підтримка протягом місяця після здачі (фікс flakiness, оновлення під нові версії)
Терміни орієнтовно
Налаштування інфраструктури з нуля (CI, unit + UI тести, звіти) — 2–3 тижні. Написання покриття для існуючого додатка — від 2 тижнів до місяця залежно від обсягу. Оцінимо ваш проєкт за 2 дні — зв’яжіться з нами. 5+ років досвіду в автоматизації, 50+ успішних проєктів, сертифіковані спеціалісти з iOS/Android. Гарантуємо стабільність тестів >98% на CI після впровадження. Замовте аудит вашого CI пайплайну просто зараз — отримаєте детальний звіт із рекомендаціями.