Testing Mobile Application Offline Mode
Offline mode — one of features declared in requirements and tested superficially. Works without internet — and okay. But for apps used in subway, on trips, in weak coverage zones, it's critical. Worse than not working — working incorrectly: shows stale data without timestamp, loses entered data, silently fails.
What We Test
Three main scenarios:
Startup without network. What user sees opening app without internet? Correct: last cached data with timestamp ("updated 2 hours ago"). Incorrect: white screen or "no connection" without any content.
Connection loss during use. User browsing feed, connection disappears. Already loaded content should remain accessible. Unfinished actions — saved as drafts or queued.
Connection recovery. Data syncs, queue of deferred actions executes, conflicts resolve. User sees actual data without manual refresh.
Local Caching: What and How
On iOS — NSURLCache for HTTP responses (if server returns correct Cache-Control headers), Core Data or Realm for structured data, UserDefaults for small settings. For media — FileManager with own cache directory.
Core Data with NSPersistentCloudKitContainer gives CloudKit sync out of box, but resolves conflicts primitively — last-write-wins. For complex conflict logic implement custom merge policy.
On Android — Room for structured data, DataStore for settings, Cache-Control via OkHttp for HTTP cache:
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 minutes
.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 hours from cache
.build()
}
chain.proceed(request)
}
.build()
only-if-cached + max-stale — read from cache even if data stale while offline.
Deferred Actions Queue
User actions offline should execute on network recovery. Not lose with error, but save and execute.
Android — WorkManager with 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)
}
Action saved in WorkManager database, executes as soon as network appears, survives device reboot.
iOS — BackgroundTasks framework (BGProcessingTask) or simpler variant: local queue in Core Data, retry attempt on applicationDidBecomeActive and on network switch via NWPathMonitor.
Test Scenarios and Tools
Network disable in test:
iOS simulator: Hardware → Network Link Conditioner → 100% Loss or xcrun simctl status_bar + simulation via Network Link Conditioner profile.
Android emulator: adb shell svc wifi disable && adb shell svc data disable. Recovery: enable.
In autotests (Detox):
it('shows cached data when offline', async () => {
// Load data with network
await element(by.id('feed_list')).waitForVisible();
// Disable network (Android only via adb)
await device.setNetworkConditions({ offline: true });
// Restart and check cache
await device.reloadReactNative();
await expect(element(by.id('cached_banner'))).toBeVisible();
await expect(element(by.id('feed_list'))).toBeVisible(); // cache works
});
Sync Conflicts
User edited record offline. Another user changed same record online. On connection recovery — conflict.
Strategies: last-write-wins (simpler, but loses data), server-wins (predictable, but ignores offline edits), merge (correct, but complex). For most apps enough to show user dialog choosing version.
Timeline
2–3 days — testing by offline scenarios checklist, evaluating cache and action queue correctness, report. Cost is calculated individually.







