Decide what correct looks like
Before changing any network settings, write down what the app should do. For each kind of action, decide the states it can be in, such as saved on the device, waiting to send, sent, confirmed by the server, and rejected, and what the person sees in each. Then define the rules that must hold whatever happens:
- No change the person saw as saved is lost.
- No change is applied twice.
- Every pending change is visible as pending.
- A rejected change is reported with a way forward.
These rules turn vague checks like “does it work offline” into assertions a test can make. The design choices behind them are covered in How do you design an offline-first architecture?
Tools for simulating bad networks
Each platform has a way to cut or degrade the connection without leaving your desk.
Browsers. Chrome DevTools can simulate a completely offline network from the Network panel’s throttling menu, which is the quickest way to check a service worker. It also has presets for slower connections, and its settings let you create custom profiles with download and upload speed, latency, packet loss, packet queue length, and packet reordering. DevTools throttles WebSocket connections as well as HTTP requests.
Android. The Android Emulator’s extended controls set the cellular network type, signal strength, and data status, including an unregistered state with no network. From the emulator console, network delay and network speed change latency and bandwidth while the app is running, and gsm data switches the data connection between states such as home, roaming, and unregistered.
Tests in code. Background sync is often scheduled to run only when a network is available. WorkManager’s test helper provides a test driver that can mark a job’s constraints as met, so a test can check what happens when the network condition becomes true without waiting for a real one.
Your own fault injection. Put a fake or proxy in front of the network layer that can fail, delay, drop, or repeat responses on command. This is how to reproduce the cases no system setting can, such as a server that commits a write and then loses the response.
Scenarios to cover
Work through these on every build that changes data or sync code:
- Cold start offline. Open the app with no connection. It should show local data, not a spinner or an empty screen.
- Write offline, reconnect. Make several changes offline, restore the connection, and confirm each reaches the server once, in an acceptable order.
- Drop mid-request. Cut the connection after a request is sent but before the answer arrives. The retry must not create a duplicate, which depends on idempotency.
- Slow and lossy links. Use high latency and packet loss. Check timeouts, retries, and whether the interface stays responsive.
- Flapping connection. Toggle the network repeatedly. Watch for duplicate syncs and runaway retries.
- Restart while offline. Force-quit the app with changes pending. They must still be there and still be pending.
- Long gap. Leave a device offline while others change the same data, then reconnect. Check stale data and the conflict rules.
- Rejection. Have the server refuse a queued change. The person must see which change failed and what to do.
Not every disconnection is the same
“Offline” covers several situations. A phone can lose the internet but still reach a device beside it, or the reverse: devices in the same room can lose each other while both still reach the server. Apps that sync directly between devices need both cases tested, along with the difference between being offline and being partitioned.
Offline Protocol’s platform documentation puts it as a rule for its SDK: test internet loss separately from peer-link loss, and remember that a simulator build checks application integration, not radio delivery. The same applies to any app that uses Bluetooth or local networking: radio roles, permissions, background execution, and restart behaviour all need confirming on the actual hardware.
Make failures visible during testing
Offline bugs are hard to reproduce after the fact, so give testers a way to see what the app believes. A debug screen showing the queue of pending changes, the last successful sync, and the current connection state turns “it did not sync” into a specific report. Log each send, retry, and server response with the change’s identifier, so a duplicate or a lost change can be traced to the step where it happened.