How to mock Retrofit API calls (or any other interface) | by Roman Kamyshnikov | Apr, 2022

In Android development, it’s quite common that we need to start working on a feature before the backend is ready. While we’re waiting for it to be ready, there are several ways to mock it, like using Postman, Firebase or Charles Proxy, but all these methods require some sort of initial setup that often needs to be performed on each device.

An alternative approach, that uses an OkHttp Interceptor, was described by Wahib Ul Haq in this article. Even though the described method is a completely valid solution, mocking several methods at the same time and accessing the parameters of those methods can get complicated pretty fast. Also, using the URL string address to identify the invoked method and returning a JSON created from a string can easily lead to mistakes if you plan to keep the mocks in the app for debug builds — a change in the name of the method or its return type or implementation might go unnoticed until the method gets called.

So, based on known solutions and their disadvantages, let’s create a list of requirements that we’d like to see in an interface mock:

  • easy and compile-time safe to add, modify and remove mocks;
  • no tedious setup procedures for different devices. We should be able to easily enable and disable all mocks (or some of them if needed);
  • the ability to add a delay for suspend functions before returning a result. API calls are never instant and you probably want to see that a loader is being displayed correctly, animations play as they should, etc.

The first solution that comes to mind, when you want to “intercept” some method calls, while calling the real implementation for the rest is to just use a delegate. Luckily for us, this is trivial in Kotlin. Let’s say that we have some interface and an implementation:

Now, we can easily delegate one of the methods:

And we can wrap our actual implementation with the mock:

This lets us meet the first requirement. Adding or modifying mocks is super-easy and safe: if the methods signature or its parameter changes and we forget to update the mocks we will get an error at compile time.

What about the other two points? We could add a variable, like isFooMockEnabled, to control the mock or add a global variable to control all mocks, or do both. For delays, we could just add the delay in the mocked method too. It’s not complicated at all, but we’d need to do the same thing for each method that we mock and that would be boilerplate! Global mock control and delays can and should be controlled from a single place and Java’s dynamic proxy comes to the rescue here.

A Proxy can be created by calling the Proxy.newProxyInstance(…) method and providing at least one interface for it as well as an InvocationHandler:

The proxy will dispatch all method invocations to the provided InvocationHandler, which has one method — invoke:

An important thing to notice is that Java’s Dynamic Proxy relies on reflection, which has some overhead, but since we’re only going to use it in debug builds, it should not be a problem.

Also, you might have noticed MockProxyController in the code above. It’s the interface that we’ll use to control the behavior of our mocks.

Note that we can use the MockProxyController instance to alter the behavior of our mocks at runtime — just add a simple debug menu with a couple of toggle buttons somewhere.

Another useful thing that we might make use of is an exception, that a mocked function can throw at any time if it wants the invocation handler to repeat the method invocation, but use the actual implementation this time:

First, we need to choose whether to call the actual implementation or the mock. A handy way to do that is by adding the private field targetObject and using our MockProxyController to return one of them:

To call a method, we can use the input parameters of the function and reflection:

This will seem to work, but there are some problems:

  • If the method that is invoked using reflection throws an exception, it’ll be wrapped in an InvocationTargetException, so we need to intercept it and unwrap it before re-throwing it.
  • No delay for suspend functions yet. We need a way to find out if the invoked method is a suspending one and delay the return somehow.

First, let’s split our implementation into two parts: one for regular functions and a second implementation for suspending functions.

Due to the way suspend functions are implemented, they get an extra Continuation parameter added at the end during compilation. We can check for this parameter to determine if the function is a suspending one or not:

Note that for the suspending function, we don’t return the result immediately. Instead, we return the constant COROUTINE_SUSPEND, which is used internally in coroutines to let the caller know that the function is suspended and will later resume using the Continuation object. To get a better understanding of how coroutines work internally, please check out Kt.Academy — it has a very detailed example. For a general overview of coroutines — please refer to my previous article.

The invokeSuspendingFunction implementation is similar, but with a few nuances caused by coroutines:

The details about the delay are added as comments in the code above, and exception handling is similar to the code in invokeMethod. Note that for a positive result the delay is used only after we actually get the result: if a mocked method throws a MockDisabledException, we’d want to call the actual implementation straight away. Also, calling delay for a canceled coroutine is not possible and makes no sense, so we just pass the CancellationException to the caller like a regular coroutine would.

The coroutine creation process might seem a bit tricky, but it’s actually not that hard:

We should follow structured concurrency and cancel our coroutine if it’s scope is cancelled: for this, we use the context of the continuation to create a CoroutineScope.

We need to intercept all exceptions to unwrap them and allow for flow control using MockDisabledException: the method that we call might have a coroutine inside and if an exception is thrown there — it’ll be propagated up the job hierarchy causing us to miss it! To catch these exceptions — we can use coroutineScope. Although it is generally used for parallel decomposition of work, it has the property of catching all exceptions from its child coroutines and actually re-throwing them instead of propagating them up, which is exactly what we need — catching the exception in the try-catch statement while still remaining inside the coroutine will allow us to apply the delay before re-throwing it to the original caller.

Note: in general, using exceptions for flow control is an anti-pattern, but it seems to be an ok solution for this case. Please share any thoughts about this or other possible solutions in the comments.

Leave a Comment