Hello! Some time ago I stumbled upon a library inside Square’s brilliant Retrofit repository called retrofit-mock
that allows you to build dummy Retrofit clients for usage in tests without OkHttp’s MockWebServer
or adding mocking library like Mockito or Mockk. Thing is, it’s not documented well, and it was a bit of a challenge to set it up and make it work, but the end result works quite well. So, I decided to write this article with some introduction for the retrofit-mock
usage. I will shortly explain what it is, how to use it, and compare with the other solutions.
What is retrofit-mock
retrofit-mock
is a library for Retrofit that allows you to create Retrofit API interface instances that won’t communicate with the actual API, instead returning the value you specify. It’s similar to OkHttp’s MockWebServer
, but it allows you to simulate varying network conditions and doesn’t require setting up local server and storing JSON responses in your repository and it’s generally on a higher level of abstraction. It requires some setup, and in case of simple API interfaces, you’ll need to make this setup once. The retrofit-mock doesn’t do any response conversion, so you need to set an entity object directly to the mock API instance, called Behavior
. After that, the object you specified in this Behavior will be returned in the every method call of the API interface.
retrofit-mock
can be used to stub actual API during development in the main app, but I used it only in tests, so I will talk about it here only in context of testing.
Set up dependencies
To get started with the retrofit-mock
, you’ll need to add dependencies for Retrofit. I recommend using BOM and version catalog for that. Here’s what you need to add to your libs.versions.toml
file:
[versions]
# ...
retrofit = "2.10.0"
[libraries]
# ...
retrofit = { group = "com.squareup.retrofit2", name = "retrofit" }
retrofit-bom = { group = "com.squareup.retrofit2", name = "retrofit-bom", version.ref = "retrofit" }
retrofit-converter = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization" } # or any other converter of choice
retrofit-mock = { group = "com.squareup.retrofit2", name = "retrofit-mock" }
And in your module’s build.gradle.kts
:
dependencies {
implementation(platform(libs.retrofit.bom))
implementation(libs.retrofit.converter)
implementation(libs.retrofit)
testImplementation(libs.retrofit.mock)
}
Writing test with retrofit-mock
To set up retrofit-mock
, you’ll need to have your Retrofit API interface ready. For example, here’s the API interface for GitLab:
interface GitLabApi {
@GET("/api/v4/projects/{id}/issues")
suspend fun loadIssues(@Path("id") projectId: String): List<Issue>
}
We may have a class that uses this interface, ApiDataSource
:
class ApiDataSource(
private val api: GitLabApi,
private val ioDispatcher: CoroutineDispatcher
) {
fun loadIssues(projectId: String): List<Issue> = withContext(ioDispatcher) {
try {
api.loadIssues(projectId)
} catch (e: HttpException) {
emptyList()
}
}
}
Let’s test this class. We’re starting with the dummy Retrofit
instance:
private val retrofit = Retrofit.Builder()
.baseUrl("http://example.com")
.build()
It doesn’t need to have a real base URL, but it should be present and be a valid one, otherwise it will crash.
Then, we can setup MockRetrofit
instance:
private val mockApi = MockRetrofit.Builder(retrofit)
.build().create<FactCheckApi>()
By default, MockRetrofit
instance will have random network delays, which might make your tests flaky, because sometimes delay can hit more than 10 seconds, and it will fail the request. You can set fixed delay via NetworkBehavior
. You will need to create it beforehand and add it to MockRetrofit
’s builder like this:
private val networkBehavior = NetworkBehavior
.create().apply {
setDelay(100, TimeUnit.MILLISECONDS)
}
private val mockApi = MockRetrofit.Builder(retrofit)
.networkBehavior(networkBehavior)
.build().create<FactCheckApi>()
Delay can be anything, and setting it to zero will disable the delay altogether.
Now, we can proceed with writing tests:
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `empty list on error`() = runTest {
val sut = ApiDataSource(
mockApi.returning(Calls.failure(Exception("test"))),
Executors.newSingleThreadExecutor().asCoroutineDispatcher()
)
val result = sut.load("0")
assertThat(result, isEmpty())
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `successful case`() = runTest {
val expected = listOf(Issue())
val sut = ApiDataSource(
mockApi.returning(Calls.response(expected)),
Executors.newSingleThreadExecutor().asCoroutineDispatcher()
)
val result = sut.load("0")
assertEquals(expected, result)
}
As you can see, it requires a bunch of boilerplate code to setup, but the tests end up concise and simple.
retrofit-mock vs other tools
One of the main tools for testing Retrofit clients will be the aforementioned OkHttp MockWebServer
. It has similar logic behind it: you can configure it to return only one response. It’s possible to add a callback with more sophisticated logic to figure out what response to provide based on request’s path or something like that. However, it operates on the lower lever, so the response you will need to provide should be either string or binary, requiring you to store JSON files in the test resources. And that’s one more thing to maintain, because if the API changes, you will need to update these test JSONs in order to avoid running tests over older API version. However, it’s seemingly the most popular approach in the industry, so you might find more info about it online. This quide describes it well.
You can also mock your Retrofit client via regular mock framework, and specify returned results. But it won’t behave like a real network, because mocked methods usually return values immediately. Also, mock frameworks affect the time of test runs, because they need to set things up on first usage (at least, in case of JUnit 4). It’s not much, 1 to 2 seconds, depending on the usage, but compared to the single test runtime it can be dramatic. For example, single class test run on my machine took 1.7s with Mockk and 60ms without. This kind of difference can accumulate significantly over multiple runs of tests, and that’s one of the reasons why I choose fake objects over mocks. However, mock frameworks can provide more sophisticated tools for state verification, in case you need it.
And since Retrofit uses regular interface classes, you can also create stub implementation of the API interface that will return objects as you need them. It might be simpler, but with this approach you won’t be able to verify if your API interface has all the annotations correctly placed until you will use it with the proper Retrofit setup in the runtime.
Conclusion
Thanks for reading till the end! To sum up, we walked through the retrofit-mock library with the use case of testing, and considered other options we have for testing. Based on that, retrofit-mock seems like a decent option for using it in tests to stub network calls, but it’s not perfect.
Hope it was useful for you. If you have any questions, hit me up on Mastodon. Good luck and have fun!