Custom Koin Test Rule & Instrumented Android Tests in Multi-Module Architectures
Introduction
When working on an Android project with a multi-module architecture, managing dependencies across modules without any issue or causing circular dependencies can be tricky, especially when it comes to instrumented tests. Koin, a lightweight dependency injection framework, is widely used in Android development, besides setting up Koin for instrumented tests seemed straightforward at first, I soon ran into issues that caused my tests to fail when run sequentially.
In this article, I’ll walk you through the problem, how I discovered the root cause, and the custom Koin test rule I created to solve it.
Typical Setup for Koin in Instrumented Tests
In a standard setup, especially in single-module projects, you might define a custom Application
class to initialize Koin for your instrumented tests like this:
class TestApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
modules(productionModule, instrumentedTestModule)
}
}
}
To make this work during instrumented tests, you would typically use a custom test runner:
class InstrumentationTestRunner : AndroidJUnitRunner() {
override fun newApplication(
classLoader: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(classLoader, TestApplication::class.java.name, context)
}
}
However, when you try to apply this approach in a multi-module project, things start to fall apart.
I encountered repeated test failures, specifically a Scope '_root_' is closed
error when running tests sequentially. This error was frustrating because the tests would pass individually but fail when run together.
The Challenge: Multi-Module Testing with Koin
To manage Koin initialization more effectively, especially when using a custom Koin test rule, you should use an “empty” Application
class. This means the Application
class should not start Koin on its own:
class TestApplication : Application()
The reason for this is that the custom test rule will handle Koin initialization, allowing you to avoid the issues caused by Koin being started in the Application
class itself. This setup is particularly important when working in a multi-module project where you need to control the initialization process carefully.
The Issue: Koin Scope Closure
At the time of writing, the Koin documentation suggests a custom test rule that looks like this:
class KoinTestRule(
private val modules: List<Module>
) : TestWatcher() {
override fun starting(description: Description) {
startKoin {
androidContext(InstrumentationRegistry.getInstrumentation().targetContext.applicationContext)
modules(modules)
}
}
override fun finished(description: Description) {
stopKoin()
}
}
However, if you use this rule in a test class with multiple instrumented tests, you might run into a frustrating issue. After the first test passes, any subsequent tests crash, even if they run fine individually. The error message looks something like this:
Scope '_root_' is closed
org.koin.core.error.ClosedScopeException: Scope '_root_' is closed
at org.koin.core.scope.Scope.resolveInstance(Scope.kt:221)
at org.koin.core.scope.Scope.get(Scope.kt:210)
at org.koin.androidx.viewmodel.factory.KoinViewModelFactory.create(KoinViewModelFactory.kt:25)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:187)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:153)
at org.koin.androidx.viewmodel.GetViewModelKt.resolveViewModel(GetViewModel.kt:44)
So, as I was really annoyed(lazy?) by have to run each test individually and without any solution so far, I dig into debugging what was causing this error inside Koin and I realized that the error was on the test rule suggested by the official documentation of Koin and after a series of failed attempts was able to create a workaroud(solution?).
The Solution: Custom Koin Test Rule
As I was really annoyed (lazy?) by having to run each test individually and couldn’t find any solution so far, I dug into Koin’s inner workings and experimented with different solutions. After some trial and error, I came up with a workaround (solution?) that ensures your tests can run sequentially without crashing:
class KoinTestRule(
private val modules: List<Module>
) : TestWatcher() {
override fun starting(description: Description) {
if (getKoinApplicationOrNull() == null) {
startKoin {
androidContext(InstrumentationRegistry.getInstrumentation().targetContext.applicationContext)
modules(modules)
}
} else {
loadKoinModules(modules)
}
}
override fun finished(description: Description) {
unloadKoinModules(modules)
}
}
Why This Solution Works
This custom test rule ensures that Koin is only started once, and additional modules are loaded as needed. This prevents the ClosedScopeException
from occurring when tests are run sequentially.
Conclusion
This challenge was a learning experience, and I hope sharing this solution can help others who might face the same issues when working with Koin in a multi-module Android project. If you’re struggling with Koin-related test failures, give this custom rule a try — it worked wonders for me.
I hope this article can help someone out there! :)
See ya!
Connect with Me
Feel free to connect with me on social media to stay updated on my latest projects and articles!
Let’s stay in touch!