Learn how to test your Android app for flaky connections!

Request timed out! But you didn’t expect it, did you? Of course not because while you were writing the app and testing the code you were always on your blazing fast WiFi connection or a 4G LTE network. But in the real world, all your users don’t have access to such a network connection at all times.

Would you want them to suffer?
Would you want your app to behave in a strange manner in that case?
Would you want to create a bad user experience?
Would you want to have an unsatisfied customer?
Do you always test all your features depended on network requests for flaky connections?

If your answer to all the above questions is “No”, then I have a simple solution for you, that’ll make your life a lot easier to test your features for these scenarios and have a failure mechanism in place to be more responsive to the user.


Implementation

The solution is pretty straight forward, create an Interceptor for your network requests and delay or fail them. Let’s look at how to implement this interceptor with OkHttp and provide easy access to it through a UI to all the stakeholders that are responsible or are willing to test your application.

public class NetworkThrottlingInterceptor implements Interceptor {   private static boolean failRequests;   private static final AtomicLong failRequestCount = new AtomicLong(Long.MAX_VALUE);   private static boolean delayAllRequests;   private static long minRequestDelay;   private static long maxRequestDelay;   private final Random random = new Random(4);   @Override   public Response intercept(Chain chain) throws IOException {      if(failRequests) {         long failC = failRequestCount.get();         if (failC > 0) {failRequestCount.compareAndSet(failC, failC-1);            throw new IOException("FAIL ALL REQUESTS");         }      }      if(delayAllRequests) {         long delay = minRequestDelay;         if(minRequestDelay != maxRequestDelay) {            delay = (long) ((random.nextDouble() * (maxRequestDelay - minRequestDelay)) + minRequestDelay);         }         long end = System.currentTimeMillis() + delay;         long now = System.currentTimeMillis();         while(now < end) {            try {               Thread.sleep(end - now);            } catch (InterruptedException e) {               // Nothing to do here, timing controlled by outer loop.            }            now = System.currentTimeMillis();         }      }      try {         return chain.proceed(chain.request());      } catch (Exception ex) {         if (BuildConfig.DEBUG) {            Request request = chain.request();            Log.e("NETWORK", "Exception during request", ex);            Log.e("NETWORK", "Request was to: " + request.url().toString());         }         throw ex;      }   }public static void delayAllRequests(long minRequestDelay, long maxRequestDelay) {      if(minRequestDelay == 0 && maxRequestDelay == 0) {delayAllRequests = false;      } else {         NetworkThrottlingInterceptor.minRequestDelay = minRequestDelay;         NetworkThrottlingInterceptor.maxRequestDelay = maxRequestDelay;delayAllRequests = true;      }   }public static void failNextRequests(long failCount) {      if(failCount == 0) {failRequests = false;      }      else {failRequestCount.set(failCount);failRequests = true;      }   }}

The logic in the interceptor is quite simple, but for verbosity, I’ll still explain it here:

We have two scenarios that we deal with, through this interceptor:

  1. Delay a response for a given network request.
  2. Fail next n network requests.

Scenario 1: Delay a response for a given network request

For this scenario, we set a minimum and maximum value and for a given request we find a random value between this range and ask the thread to sleep for that time.

You can create a different combination of settings that you give access to, through your UI, for example:

  1. Good Network (min = 0, max = 0)
  2. Slow Network (min = 1 second, max =5 seconds)
  3. Very Slow Network (min = 5 seconds, max = 10 seconds)

By default, your app can always be set to work on the Good Network. And then the users can switch to other networks as and when needed.

Scenario 2: Cause next n network requests to fail

For this scenario, we maintain a fail counter that keeps decreasing on each request until it becomes zero. And while this happens we just throw an IO Exception to cause the network request to fail, you can even cause a failure by other means like creating a fake failure response with some status code 5xx.


Interceptor Integration

Injection of this interceptor is quite simple you can add it at the time of configuration of your OkHttp client instance like so.

OkHttpClient okHttpClient = new OkHttpClient.Builder()      .addNetworkInterceptor(new NetworkThrottlingInterceptor())      
.build();

Interceptor Exposure

In our app, we do it through a debug screen that contains a simple Spinner widget that holds all these values that we can select and modify the interceptor configuration at runtime, the code for which looks like this

private void setupNetworkThrottler() {   spNetworkInterceptor = findViewById(R.id.sp_network_throttle);   ArrayAdapter<String> networkSpeedTypeAdapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line,         new String[]{"Good network", "Moderate network 1-5s delay", "Poor network 5-10s delay"});   spNetworkInterceptor.setAdapter(networkSpeedTypeAdapter);   spNetworkInterceptor.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {      @Override      public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {         switch (position) {            case 0: NetworkThrottlingInterceptor.delayAllRequests(0,0);               break;            case 1: NetworkThrottlingInterceptor.delayAllRequests(1000, 5000);               break;            case 2: NetworkThrottlingInterceptor.delayAllRequests(5000, 10000);               break;         }      }      @Override      public void onNothingSelected(AdapterView<?> parent) {         NetworkThrottlingInterceptor.delayAllRequests(0,0);      }   });}

If you have other interesting solutions that you can leverage this interceptor for, please share them with me on Twitter where you can find me as @droidchef.

Loading comments...
You've successfully subscribed to Ishan Khanna
Great! Next, complete checkout to get full access to all premium content.
Error! Could not sign up. invalid link.
Welcome back! You've successfully signed in.
Error! Could not sign in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.