Testing with Koin

One of the great advantages in using dependency injection is ease of testing. It allows us to replace any dependency with a test double. Some DI frameworks are better here than others, Dagger is on the “not so good” side. Happily, Koin did a better job here. I’ll show you how.
Let’s assume we have an API interface that is implemented by Retrofit. So we probably have a Koin definition like this:
applicationContext {
bean<Api> { RetrofitApi(get()) }
...
}
( bean
defines a singleton in Koin opposed to factory
)
Overriding
Overriding this with Koin is very easy, just load a new binding in another module and override the existing value
val apiMock = mock<Api>()
...startKoin(
listOf(
myModule,
applicationContext {
bean { apiMock }
}
)
}
Now whenever our implementation is injecting an API, probably with something like this:
class SearchFragment : Fragment() {
val api: API by inject()
it will get your mocked version. That’s it!
That was easy. right? :)
Late overriding
If the graph is already created (startKoin()
called somewhere in the code we test) we may need to override a dependency at a later point in time.
Luckily this is not a problem at all. We can use the loadKoinModules()
method, which is meant for libraries and other use cases where at application start the graph is not complete.
loadKoinModules(
applicationContext {
bean { apiMock }
})
This is nice and easy. But if we override more than just one binding, this will become boilerplate. Also it would be nice if our tests are less cluttered with Koin code.
Keep the test clean
The toothpick dependency injection framework brings it’s own Junit TestRule
that automatically binds mocks into the current scope.
Let’s build something similar. Our projects at sporttotal are all on Junit5 where TestRules don’t exist anymore. Their replacement are test extensions, similar but much easier to write.
Let’s start with writing an extension that will replace the setup part
class InjectionExtension() : BeforeEachCallback {
override fun beforeEach(context: ExtensionContext?) {
// TODO
}
}
Now we’ll add the module(s) in the constructor that will contain our overrides:
class InjectionExtension(vararg val modules: Module)
: BeforeEachCallback
Before every test we will load the module:
class InjectionExtension(vararg val modules: Module = emptyArray())
: BeforeEachCallback {
override fun beforeEach(context: ExtensionContext?) {
loadKoinModules(*modules)
}
}
That’s it.
How would you use that in our test?
@JvmField
@RegisterExtension
val extension = KoinExtension(
applicationContext {
bean { apiMock }
}
)
As you can see we still have a lot of Koin boilerplate code in our tests. The goal was to have less Koin code there.
The problem here is we cannot just pass all the objects to the extension’s constructor to do the bindings there.
The whole magic of Koin is based on inline
/ reified
generics.
We need to keep converting the object to binding in our tests.
But we can still make this a bit nicer:
inline fun <reified T : Any> T.bean(): Module =
applicationContext {
bean { this@bind }
}
We created an extension function that can be called on Any
object, that binds itself into a bean. As this method is inline
, it can determine the type of the generic T
and use all of Koin’s powers.
This is how can use this now:
@JvmField
@RegisterExtension
val extension = KoinExtension(apiMock.bean())
And as you might remember, we created the constructor with a vararg
, so we can pass unlimited number of overrides:
@JvmField
@RegisterExtension
val extension = KoinExtension(
apiMock.bean(),
mock<Resources>().bean()
)
Scoping
How would we handle scopes?
At sporttotal we use a more complex version of this method to also support that. Let me show you.
For the scope identifier we use class names so it’s very easy to build Activity scopes. So we need added a parameter to the method:
inline fun <reified T : Any> T.bean(scope: KClass<out Any>? = null):
If we pass a scope we need to wrap the binding in a context, else keep it as before. That is easy in Kotlin:
inline fun <reified T : Any> T.bean(scope: KClass<out Any>? = null): Module =
applicationContext {
scope?.let {
context(scope.java.scopeName) {
bean{ this@bind }
}
} ?: bean { this@bean }
}
And then pass the scope name (in our case the class) like this:
@RegisterExtension
@JvmField
val extension = KoinExtension(
videoTracker.bean(PlayerActivity::class))
Cleaning up
We need to close the graph after your tests, which we can easily add to the Extension:
class InjectionExtension(vararg val modules: Module = emptyArray())
: BeforeEachCallback, AfterEachCallback { override fun beforeEach(context: ExtensionContext?) {
loadKoinModules(*modules)
override fun afterEach(context: ExtensionContext?)
= closeKoin()
}
Injecting not overriding
Overriding dependencies becomes super easy with Koin.
But maybe you want the opposite. Instead of injecting classes from test to tested code, maybe you want to get the same classes that your class under test gets injected? So you want to inject fields into the test itself!
All you need is adding the koin test dependency:
dependencies {
testImplementation ‘org.koin:koin-test:0.9.3‘
}
and make your tests Koin aware to be able to inject everything from your Koin graph
class SomeImportantTest: KoinTest
Now you can inject whatever you need, the same way you always do (as long as startKoing was called)
class SomeImportantTest: KoinTest {
` val preferences: Persistence by inject()
Of course you can also use this for creating test only graphs or modules, for example to be able to create complex utils tools by keeping the tests clean. Dependency injection is not only useful in production code.
The end
What left to say: Testing with Koin is easy and that’s the best thing you can have for tests