alpaca0984.log

Kodein — overwrite for a testing

alpaca0984

Kodein is a simple and easy-to-use dependency injection that works on JVM. I use it in our project’s data layer which takes care of networking. Let’s see a simple example:

data-layer

In the above, a repository object has an API object as a property. The API has a client which makes HTTP requests and receives responses.

Those classes look like this. Please note that the HTTP client implementation is of course just a sample and you probably use Retrofit or Ktor for that.

// src/main/kotlin/animal/AnimalRepository.kt
class AnimalRepository(private val api: AnimalApi) {
 
    suspend fun getAnimals(): HttpResponse = api.getAnimals()
}
 
// src/main/kotlin/animal/AnimalApi.kt
class AnimalApi(private val client: HttpClient) {
 
    suspend fun getAnimals(): HttpResponse = client.get()
}
 
// src/main/kotlin/common/HttpClient.kt
interface HttpClient {
 
    suspend fun get(): HttpResponse
}
 
// src/main/kotlin/common/HttpClientImpl.kt
class HttpClientImpl : HttpClient {
 
    override suspend fun get(): HttpResponse {
        return HttpResponse("Actual API Response")
    }
}

and the Kodein instance looks like this. Below I build Kodein modules for each application module and compose them in an app-level Kodein instance:

// src/main/kotlin/animal/di/AnimalModule.kt
val animalModule = DI.Module("AnimalModule") {
    bind<AnimalApi>() with singleton { AnimalApi(instance()) }
    bind<AnimalRepository>() with singleton { AnimalRepository(instance()) }
}
 
// src/main/kotlin/common/di/CommonModule.kt
val commonModule = DI.Module("CommonModule") {
    bind<HttpClient>() with provider { HttpClientImpl() }
}
 
// src/main/kotlin/AppKodein.kt
val appKodein = DI {
    importAll(
        commonModule,
        animalModule
    )
}

I assume they look good. Next let’s think about adding an integration test to verify the interactions between a repository and API, according to a guideline for medium-sized tests:

In this case, you will instantiate actual instances but maybe mock HTTP responses so that they won’t rely on the backend situation. Let’s look at how we do that.

Replace specific Kodein bindings for testing

First of all, we need to prepare mocked HTTP client like this:

// src/test/kotlin/animal/TestAnimalHttpClient.kt
class TestAnimalHttpClient : HttpClient {
 
    override suspend fun get(): HttpResponse {
        return HttpResponse("Testing API Response")
    }
}

If you use Ktor, its MockEngine should work for you. Next is the main part. Now we create another Kodein module that binds the client:

 
// src/test/kotlin/animal/di/TestAnimalKodein.kt
val testAnimalKodein = DI {
    extend(appKodein, copy = Copy {
        copy the binding<AnimalApi>()
        copy the binding<AnimalRepository>()
    })
 
    bind<HttpClient>(overrides = true) with provider { TestAnimalHttpClient() }
}

There are several things I’d like to mention:

  1. We created a new Kodein instance which extends the original one so that we can keep things as much as possible
  2. It copies access for AnimalApi and AnimalRepository when extending because they’re bound to a singleton. We need to copy the AnimalApi so that it has the TestAnimalHttpClient as its property. Same for AnimalRepository we need to copy it so that it has the AnimalApi that has the test client
  3. It overrides HttpClient to replace the binding with the mocked one

You can find the detailed information in the official doc.

Run the integration test

Since we build a Kodein instance for our integration test, let’s verify how it works. We can get the repository class from the Kodein instance and you will see our HTTP Client is successfully replaced with the mocked one.

// src/test/kotlin/animal/AnimalIntegrationTest.kt
@ExperimentalCoroutinesApi
class AnimalIntegrationTest {
 
    val animalRepository: AnimalRepository by testAnimalKodein.instance()
 
    @Nested
    inner class `getAnimals()` {
 
        @Test
        fun `it fetches data`() = runBlockingTest {
            val response = animalRepository.getAnimals()
            assertThat(response.data).isEqualTo("Testing API Response")
        }
    }
}

I guess you saw how easy for us to arrange a Kodein instance for any test cases! If you want to see the entire codebase above, you can find it here.