Mobile applications must work reliably across various network conditions. Testing slow connections, intermittent connectivity, and offline scenarios ensures robust user experiences. Network reliability directly impacts mobile app performance, making condition simulation essential for quality assurance.
Why Network Testing Matters
Mobile networks vary dramatically:
- WiFi: 10-100 Mbps, <50ms latency
- 4G/LTE: 5-12 Mbps, 50-100ms latency
- 3G: 0.5-2 Mbps, 100-500ms latency
- Edge: 0.1-0.3 Mbps, 300-1000ms latency
Android Network Simulation
Using OkHttp Interceptors
class NetworkConditionInterceptor(
private val delayMs: Long = 0,
private val packetLossRate: Double = 0.0,
private val bandwidthBytesPerSecond: Long = Long.MAX_VALUE
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
// Simulate latency
if (delayMs > 0) {
Thread.sleep(delayMs)
}
// Simulate packet loss
if (Random.nextDouble() < packetLossRate) {
throw SocketTimeoutException("Simulated packet loss")
}
val response = chain.proceed(chain.request())
// Simulate bandwidth throttling
if (bandwidthBytesPerSecond < Long.MAX_VALUE) {
return throttleResponse(response, bandwidthBytesPerSecond)
}
return response
}
private fun throttleResponse(response: Response, bytesPerSecond: Long): Response {
val originalBody = response.body ?: return response
val throttledSource = object : ForwardingSource(originalBody.source()) {
override fun read(sink: Buffer, byteCount: Long): Long {
val startTime = System.currentTimeMillis()
val bytesRead = super.read(sink, byteCount)
if (bytesRead > 0) {
val expectedDuration = (bytesRead * 1000) / bytesPerSecond
val actualDuration = System.currentTimeMillis() - startTime
val delay = expectedDuration - actualDuration
if (delay > 0) {
Thread.sleep(delay)
}
}
return bytesRead
}
}
val throttledBody = object : ResponseBody() {
override fun contentType() = originalBody.contentType()
override fun contentLength() = originalBody.contentLength()
override fun source() = throttledSource.buffer()
}
return response.newBuilder()
.body(throttledBody)
.build()
}
}
// Usage
val client = OkHttpClient.Builder()
.addInterceptor(
NetworkConditionInterceptor(
delayMs = 500, // 500ms latency
packetLossRate = 0.1, // 10% packet loss
bandwidthBytesPerSecond = 128_000 // 1 Mbps
)
)
.build()
Testing Different Network Conditions
class NetworkConditionTest {
@Test
fun testSlowConnection() = runTest {
val slowClient = OkHttpClient.Builder()
.addInterceptor(NetworkConditionInterceptor(
delayMs = 2000, // 2 second latency
bandwidthBytesPerSecond = 50_000 // ~400 Kbps
))
.build()
val apiService = Retrofit.Builder()
.client(slowClient)
.build()
.create(ApiService::class.java)
val startTime = System.currentTimeMillis()
val response = apiService.getUsers()
val duration = System.currentTimeMillis() - startTime
// Should take at least 2 seconds due to simulated latency
assertTrue(duration >= 2000)
}
@Test
fun testPacketLoss() = runTest {
val unreliableClient = OkHttpClient.Builder()
.addInterceptor(NetworkConditionInterceptor(
packetLossRate = 0.5 // 50% packet loss
))
.readTimeout(5, TimeUnit.SECONDS)
.build()
var successCount = 0
var failureCount = 0
repeat(100) {
try {
apiService.getUsers()
successCount++
} catch (e: SocketTimeoutException) {
failureCount++
}
}
// With 50% packet loss, approximately half should fail
assertTrue(failureCount in 40..60)
}
}
iOS Network Link Conditioner
Using Network Link Conditioner
// Network Link Conditioner is available in Xcode
// Settings > Developer > Network Link Conditioner
// Programmatically configure network conditions for tests
import Network
class NetworkSimulator {
func simulate3GConnection() {
let parameters = NWParameters()
parameters.serviceClass = .background
// Configure 3G-like conditions
let pathEvaluator = NWPathEvaluator()
// Note: iOS doesn't provide direct API for network throttling
// Use proxy-based solutions or Network Link Conditioner
}
}
Network condition testing is crucial for comprehensive mobile testing in 2025, where iOS and Android apps face diverse connectivity scenarios.
Using Charles Proxy for iOS
# Install Charles Proxy on Mac
# Configure iOS device to use Mac as proxy
# Throttle Settings in Charles:
# - Bandwidth: 1000 Kbps
# - Utilisation: 80%
# - Round-trip latency: 200ms
# - MTU: 1500 bytes
Offline Mode Testing
Android Offline Simulation
class OfflineModeTest {
@Test
fun testOfflineDataAccess() = runTest {
// Put app in airplane mode programmatically (requires system permissions)
// Or use interceptor to simulate no network
val offlineClient = OkHttpClient.Builder()
.addInterceptor { chain ->
throw UnknownHostException("No network available")
}
.build()
val repository = UserRepository(offlineClient)
// Should fetch from cache
val users = repository.getUsers()
assertNotNull(users)
assertTrue(users.isNotEmpty())
verify { localDatabase.getUsers() } // Verify cache was used
}
@Test
fun testOfflineWriteQueueing() = runTest {
val syncManager = SyncManager(context)
// Simulate offline
syncManager.setNetworkAvailable(false)
// Attempt to create user while offline
val newUser = User(id = "123", name = "Test User")
syncManager.queueCreateUser(newUser)
// Verify queued for later sync
val pendingOperations = syncManager.getPendingOperations()
assertEquals(1, pendingOperations.size)
// Simulate going back online
syncManager.setNetworkAvailable(true)
delay(1000) // Wait for sync
// Verify operation was synced
val pendingAfterSync = syncManager.getPendingOperations()
assertEquals(0, pendingAfterSync.size)
}
}
Offline-First Architecture Testing
Implementing offline-first patterns requires strategic API caching for mobile applications, ensuring data availability regardless of network state.
class OfflineFirstRepository(
private val remoteApi: ApiService,
private val localDatabase: Database,
private val networkMonitor: NetworkMonitor
) {
suspend fun getUsers(): List<User> {
return if (networkMonitor.isConnected()) {
try {
val users = remoteApi.getUsers()
localDatabase.saveUsers(users)
users
} catch (e: IOException) {
localDatabase.getUsers()
}
} else {
localDatabase.getUsers()
}
}
}
@Test
fun testOfflineFirstStrategy() = runTest {
val networkMonitor = FakeNetworkMonitor(isConnected = false)
val repository = OfflineFirstRepository(
remoteApi = mockApi,
localDatabase = inMemoryDatabase,
networkMonitor = networkMonitor
)
// Prepopulate cache
inMemoryDatabase.saveUsers(listOf(User("1", "Cached User")))
// Request while offline
val users = repository.getUsers()
// Should return cached data
assertEquals(1, users.size)
assertEquals("Cached User", users[0].name)
// Verify API wasn't called
verify(exactly = 0) { mockApi.getUsers() }
}
Retry Logic Testing
Testing retry mechanisms under poor network conditions is essential for API performance testing, ensuring resilience against transient failures.
Exponential Backoff
class RetryInterceptor(
private val maxRetries: Int = 3,
private val initialDelay: Long = 1000
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var attempt = 0
var delay = initialDelay
while (attempt <= maxRetries) {
try {
return chain.proceed(chain.request())
} catch (e: IOException) {
attempt++
if (attempt > maxRetries) {
throw e
}
Thread.sleep(delay)
delay *= 2 // Exponential backoff
}
}
throw IOException("Max retries exceeded")
}
}
@Test
fun testRetryLogic() = runTest {
var attemptCount = 0
val client = OkHttpClient.Builder()
.addInterceptor { chain ->
attemptCount++
if (attemptCount < 3) {
throw SocketTimeoutException("Simulated failure")
}
chain.proceed(chain.request())
}
.addInterceptor(RetryInterceptor(maxRetries = 3))
.build()
val response = client.newCall(
Request.Builder().url("https://example.com").build()
).execute()
assertEquals(3, attemptCount)
assertTrue(response.isSuccessful)
}
Network Testing Tools
1. Android Emulator Network Settings
# Limit network speed
adb shell settings put global airplane_mode_on 1
adb shell settings put global wifi_sleep_policy 0
# Set network profile
adb shell dumpsys netstats set mobile enabled=true
# Throttle bandwidth
tc qdisc add dev eth0 root tbf rate 256kbit latency 50ms burst 1540
2. Chrome DevTools Network Throttling
For WebView testing:
webView.settings.apply {
// Enable remote debugging
WebView.setWebContentsDebuggingEnabled(true)
}
// Use Chrome DevTools to throttle network
// chrome://inspect > Select device > Network tab > Throttling
3. Proxy-Based Solutions (mitmproxy)
# mitmproxy script for network simulation
from mitmproxy import http
import time
def request(flow: http.HTTPFlow) -> None:
# Add 500ms latency
time.sleep(0.5)
def response(flow: http.HTTPFlow) -> None:
# Throttle bandwidth
if flow.response:
chunk_size = 1024 # 1KB chunks
time.sleep(0.1) # 100ms per chunk
Best Practices
- Test with realistic network profiles - 3G, 4G, WiFi
- Implement retry logic with exponential backoff - Handle transient failures
- Use offline-first architecture - Cache data locally
- Test network transitions - WiFi ↔ cellular, online ↔ offline
- Monitor network performance - Track latency, throughput
- Implement request timeouts - Prevent hanging requests
Conclusion
Network condition testing ensures:
- Graceful degradation on slow networks
- Offline functionality
- Proper retry mechanisms
- Data consistency across network states
Testing various network conditions prevents poor user experiences and data loss, making apps resilient to real-world connectivity issues.