Testing on a bad network — 8 scenarios and tools
Most mobile apps break not on «no internet» but on «yes, but bad». QA teams typically test only two states — Wi-Fi present and Airplane mode. Real users live in between: 4G on the subway with drops, captive portal at a coffee shop, slow guest Wi-Fi at a hotel, a cellular tower at the far corner of a mall.
This guide — 8 scenarios that a mobile app regression checklist can’t be considered complete without. For each: what exactly breaks, which tools reproduce it, and the minimum acceptance test.
1. Throttling (2G / 3G / weak Wi-Fi)
What breaks: client-side timeouts fire before the server can respond → retry logic spawns duplicate requests; spinners hang with no cancel button; SDK initializations (Adjust, Firebase, AppLovin) hold the startup bootstrap hostage to a slow config fetch.
Tools: Network Link Conditioner (iOS: Settings → Developer → Network Link Conditioner; macOS: install via Additional Tools for Xcode), Proxyman (Tools → Network Conditions → Throttling), Charles (Proxy → Throttle Settings), Android Studio Network Profiler.
Minimum test: «3G» profile (~780 kbps down / 330 kbps up, 100ms latency) — golden path (login → home screen → key action → exit) completes in ≤ 30 seconds with no freezes. Every «loading…» has either progress or a cancel button.
2. Packet loss and jitter
What breaks: WebSocket connections lose ACKs and send duplicates; chunked downloads and uploads break mid-flight; gRPC streaming doesn’t re-initialize; chats show messages twice.
Public Wi-Fi at a café, airport, or airplane easily gives 1–5% loss. Atc (Augmented Traffic Control by Facebook) and tc qdisc netem on a Linux gateway emulate this accurately. Network Link Conditioner has a «100% Loss» profile for the extreme case.
Minimum test: 3% loss on a «3G» profile — every retry mechanism (SDK events, analytics) is idempotent and produces no duplicates on the backend. Verify on the backend, not in client logs.
3. Latency spikes (5–30 seconds)
What breaks: user double-taps a button → duplicate request (IAP — a double purchase, like — two likes); race conditions between in-flight requests; cache TTL expires between request and response and the UI renders inconsistent state.
Tool: Proxyman → Breakpoints on a specific URL, manually delay the response by 10–20 seconds and watch what the UI does. Charles can do the same.
Minimum test: purchase and important-action requests block the UI button or show loading on the button itself. A double tap 10 seconds after the first does not trigger a second request. Guarantee idempotency via an Idempotency-Key header server-side.
4. Captive portals (hotels, airports, cafés)
What breaks: DNS resolves, TCP opens, but HTTP 200 returns the provider’s login page, not your content. SDKs on startup get garbage JSON and either crash on parsing or hang retrying for 60 seconds. iOS auto-opens a WebView for the captive portal — this conflicts with deep links.
Tool: locally — run nginx on your machine, point your API domain at it via /etc/hosts, have it serve HTTP 200 with a captive-portal HTML page. Real-world test — go with a test device to Starbucks or Marriott.
Minimum test: open the app on a captive-portal network before authenticating — SDK init does not hang > 5 seconds, falls back to cached config or shows an explicit error UI, not a white screen.
5. Wi-Fi → Cellular handoff during a request
What breaks: in-flight HTTP requests are cut off mid-flight → client retries → IAP gets a double-charge; uploaded video gets halfway and the server writes a corrupted file; WebSocket drops without reconnect logic.
Tool: manually — open the app on Wi-Fi, start a critical operation (IAP / upload / auth), physically turn off the Wi-Fi on the router or toggle the device → it falls back to LTE. On Android an alternative is toggling Airplane mode for 2 seconds.
Minimum test: an IAP during the handoff either successfully completes (via idempotent retry) or explicitly tells the user «buy again». Never charges twice. Adjust SDK correctly persists its queue to a file — verify your analytics code does the same.
6. DNS failures
What breaks: the first request hangs for 60–90 seconds (system DNS timeout), no fallback to a cached IP, no clean error state in the UI. Corporate networks often filter DNS and reject non-whitelisted domains. Some providers do DNS hijacking — on NXDOMAIN they return a search page.
Tool: Proxyman → DNS Spoofing, return 127.0.0.1 or rejected. On macOS — /etc/resolver/<domain> with an invalid IP. Or set the router DNS to 192.0.2.1 (TEST-NET-1).
Minimum test: with DNS blocked the app shows a clean error in ≤ 10 seconds (not 60), does not crash. If an offline mode exists — switch to it automatically.
7. IPv6-only networks
Apple has required IPv6 readiness since 2016 at App Store review. T-Mobile USA already operates IPv6-only. It only gets worse for IPv4-only code from here.
What breaks: hardcoded IPv4 in code, 127.0.0.1 vs ::1 conflicts, NAT64/DNS64 translations into synthetic IPv6, third-party SDKs with outdated getaddrinfo.
Tool: macOS → System Settings → Sharing → Internet Sharing with the IPv6-only option, or Apple Configurator with a custom Wi-Fi profile.
Minimum test: the app on an IPv6-only Wi-Fi — every SDK host (analytics, ads, backend, push) resolves and works. Pay special attention to third-party SDKs — known bugs in older versions of Facebook SDK and InMobi.
8. TLS / Certificate edge-cases
What breaks: SSL pinning hard-blocks the app when the server cert rotates (Adjust, AppLovin, Firebase pin their CDNs — if the pin in the build expired, you only fix it via a release); user’s system time set «a year ahead» → cert validity check fails → cryptic crash; MITM scenario through a corporate proxy.
Tool: manually — set the device’s system clock to a year ahead, launch the app. Proxyman in MITM mode without the cert installed on the device — watch what error the UI shows.
Minimum test: with wrong system time the app either works (if the SSL check is tolerant) or shows a clean «check the time on your device» error. Not a crash.
Regression checklist on every release
- Golden path on a «3G» profile ≤ 30 seconds
- IAP during a Wi-Fi → cellular handoff = exactly one charge
- Captive portal does not block SDK init > 5 seconds
- DNS failure = error in ≤ 10 seconds, not 60
- System clock +1 year = clean error, not a crash
- 10-second latency + double tap on a CTA = one request
- IPv6-only network = all SDKs work
- 3% packet loss = analytics events idempotent on the backend
Takeaway
«No internet / yes internet» — that’s coverage of two points on the spectrum. Real users live in between. These 8 scenarios in your regression checklist raise production resilience by an order of magnitude with minimal added cost — most tools are already free in iOS, macOS, and Android Studio.