Blog Infos
Author
Published
Topics
Published
Topics

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 step: delegation

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:

interface SomeApi {
suspend fun foo(): String
suspend fun bar(): String
suspend fun baz(): String
}
class SomeApiImpl : SomeApi {
override suspend fun foo() = "Actual foo"
override suspend fun bar() = "Actual bar"
override suspend fun baz() = "Actual baz"
}
view raw !SomeApi.kt hosted with ❤ by GitHub

Now, we can easily delegate one of the methods:

class SomeApiMock(
private val actual: SomeApi,
) : SomeApi by actual {
override suspend fun foo() = "Mocked foo"
}
view raw !SomeApiMock.kt hosted with ❤ by GitHub

And we can wrap our actual implementation with the mock:

fun wrapSomeApi(actual: SomeApi): SomeApi {
// we only want to mock calls in debug builds!!
return if (BuildConfig.DEBUG) SomeApiMock(actual) else actual
}
view raw !wrapSomeApi.kt hosted with ❤ by GitHub

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.

Controlling mock behaviour with Java Dynamic Proxy

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

fun <T : Any> create(
clazz: Class<T>,
actual: T,
mock: T,
controller: MockProxyController,
): T {
return Proxy.newProxyInstance(
clazz.classLoader,
arrayOf(clazz),
MockProxyInvocationHandler(actual, mock, controller)
) as T
}

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

internal class MockProxyInvocationHandler<T : Any>(
private val actual: T,
private val mock: T,
private val controller: MockProxyController,
) : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array<out Any>?): Any? {
// handle method invocation here!
}
}

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 behaviour of our mocks.

interface MockProxyController {
val areMocksEnabled: Boolean
val suspendFunctionsDelay: Long
}

Note that we can use the MockProxyController instance to alter the behaviour 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:

object MockDisabledException : Exception()
Implementing MockProxyInvocationHandler

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:

private val targetObject: T
get() = if (controller.areMocksEnabled) mock else actual

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

override fun invoke(proxy: Any, method: Method, args: Array<out Any>?): Any? {
val safeArgs = args ?: emptyArray()
return method.invoke(targetObject, *safeArgs)
}
view raw !invokeBasic.kt hosted with ❤ by GitHub

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:

override fun invoke(proxy: Any, method: Method, args: Array<out Any>?): Any? {
val safeArgs = args ?: emptyArray()
val continuation = args?.firstNotNullOfOrNull { it as? Continuation<Any?> }
return if (continuation == null) {
invokeMethod(method, targetObject, safeArgs)
} else {
invokeSuspendingFunction(method, targetObject, safeArgs, continuation)
COROUTINE_SUSPENDED
}
}
private fun invokeMethod(...): Any? { ... }
private fun invokeSuspendingFunction(...): Unit { ... }
view raw !invoke.kt hosted with ❤ by GitHub

Job Offers

Job Offers


    Android App Developer

    sipgate GmbH
    Düsseldorf, Remote
    • Full Time
    apply now

    Senior Mobile Systems SDK Engineer

    Sauce Labs
    Remote
    • Full Time
    apply now

    Android Build Engineer

    Pinterest
    San Francisco, CA | Seattle, WA
    • Full Time
    apply now
Load more listings

OUR VIDEO RECOMMENDATION

Jobs

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.

Now, let’s see how the invokeMethod is implemented:

private fun invokeMethod(method: Method, obj: Any, safeArgs: Array<out Any>): Any? {
return try {
method.invoke(obj, *safeArgs)
} catch (exception: Exception) {
when (val actualException = exception.unwrap()) {
is MockDisabledException -> invokeMethod(method, actual, safeArgs)
else -> throw actualException
}
}
}
private fun Throwable.unwrap(): Throwable {
return (this as? InvocationTargetException)?.cause ?: this
}

First, we try to call the method for the passed object (that we got from targetObject). If we get any exception, we try to unwrap if from the InvocationTargetException and then if it’s the MockDisabledException we talked about earlier — call the same method again, but use the actual instance this time. If it’s some other exception, then we can just re-throw it and let the caller handle it.

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

private fun invokeSuspendingFunction(
method: Method, obj: Any, safeArgs: Array<out Any>, continuation: Continuation<Any?>
) {
// Get the delay duration that we'll use (only if the target is the mock implementation)
val delayDuration = controller.suspendFunctionsDelay.takeIf { obj == mock } ?: 0L
CoroutineScope(continuation.context).launch {
try {
coroutineScope {
launch {
val result = method.invoke(obj, *safeArgs)
if (result != COROUTINE_SUSPENDED) {
// the invoked function might return COROUTINE_SUSPEND,
// but since we already returned it from the InvocationHandler.invoke method
// we need to ignore it this time!!!
continuation.resumeWithAfterDelay(Result.success(result), delayDuration)
}
}
}
} catch (exception: Exception) {
when (val actualException = exception.unwrap()) {
is MockDisabledException -> invokeSuspendingFunction(method, actual, safeArgs, continuation)
is CancellationException -> continuation.resumeWith(Result.failure(actualException))
else -> continuation.resumeWithAfterDelay(Result.failure(actualException), delayDuration)
}
}
}
}
// Delay before resuming the coroutine execution
private suspend fun Continuation<Any?>.resumeWithAfterDelay(result: Result<Any?>, timeMillis: Long) {
delay(timeMillis)
resumeWith(result)
}

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 cancelled 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 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.

After adding the functions to handle regular and suspending functions to our InvocationHandler, we’re ready to use our MockProxy. You can find the full code here.

After copying into your project, all you need to do is implement MockProxyController (or use the default one — DefaultMockProxyController), create a mock implementation for the api you want to mock and create a proxy with it:

class SomeApiMockController : MockProxyController {
override val areMocksEnabled: Boolean
get() = mockFoo || mockBaz
override var suspendFunctionsDelay: Long = 3000L
var mockFoo = true
var mockBaz = false
}
class SomeApiMock(
private val actual: SomeApi,
private val controller: SomeApiMockController,
) : SomeApi by actual {
private inline fun <T> getResult(
isMockEnabled: Boolean = true,
method: () -> T,
): T {
if (isMockEnabled) return method() else throw MockDisabledException
}
override suspend fun foo() = getResult(controller.mockFoo) { "Mocked foo" }
override suspend fun baz() = getResult(controller.mockBaz) { "Mocked baz" }
}
suspend fun example() {
val actual = SomeApiImpl()
val controller = SomeApiMockController()
val mock = SomeApiMock(actual, controller)
val proxy = MockProxy.create(actual, mock, controller)
proxy.foo() // returns "Mocked foo"
proxy.bar() // returns "Actual bar"
proxy.baz() // returns "Actual baz", because the mock is disabled and MockDisabledException is thrown
}
A few things to remember

While the provided MockProxy has all the advantages that we need and it lets us create and change mocks very fast, the use of reflection does add some overhead and it should be used only in debug builds to avoid impacting performance. Another important thing is that no OkHttp Interceptors are called for the mocked methods because the invocation never actually reaches Retrofit. This will probably be fine for most cases, but for some others — you might need to use one of the other options described in the intro.

P.s. A special thank you to Mamedov Eldar, for helping me test MockProxy and finding a quite nasty bug in the process.

Got any thoughts or ideas about the article? Maybe a suggestion to improve the MockProxy implementation? Please share your feedback in the comments below.

Links
  1. Full MockProxy code with example
  2. Kotlin coroutines under the hood (Kt.Academy)
  3. Kotlin coroutines in Android summary
  4. Mocking Retrofit API calls with Interceptor
  5. Creating Java Dynamic Proxies with Kotlin coroutines

 

This article was originally published on proandroiddev.com on April 28, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
This tutorial is the second part of the series. It’ll be focussed on developing…
READ MORE
blog
One of the main functions of a mobile phone was to store contacts information.…
READ MORE
blog
Splash screen is the initial screen you’ll see when you open your app. It…
READ MORE
blog
AppGallery is Huawei’s application distribution platform. Many developers are integrating their apps into the…
READ MORE

Leave a Reply

Your email address will not be published.

Fill out this field
Fill out this field
Please enter a valid email address.

Menu