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)
    }
}
// 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

  1. Test with realistic network profiles - 3G, 4G, WiFi
  2. Implement retry logic with exponential backoff - Handle transient failures
  3. Use offline-first architecture - Cache data locally
  4. Test network transitions - WiFi ↔ cellular, online ↔ offline
  5. Monitor network performance - Track latency, throughput
  6. 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.