Skip to content

Instantly share code, notes, and snippets.

@Sloy
Last active July 19, 2018 09:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Sloy/b64226a64c86ece8af5225bf4ff9b0b3 to your computer and use it in GitHub Desktop.
Save Sloy/b64226a64c86ece8af5225bf4ff9b0b3 to your computer and use it in GitHub Desktop.
Reproducing issue with Koin shared state in Android Tests. Inspired by the Dagger 1 version: https://gist.github.com/Sloy/c45dc3291403e603b4b88755e4a67660
open class Counter(context: Context) {
open var count: Int = 0
open fun hit() = count++
}
/**
* In Android Tests, a single application instance is shared between test methods and classes. This means Koin singletons will be shared
* between tests, instead of being recreated each time. That can make tests non deterministic, depending on the execution order.
*
* Another issue exists when a dependency is overridden with [KoinTest.declareMock] or [KoinTest.declare]. Because the Koin state is shared
* the mock will also be injected in other tests that expect the real dependency instead of the mock.
*
* This tests checks that the Koin injections are reset between test methods, avoiding the mentioned issues.
* If that is not the case, the second test executed will fail because it will retain the singleton instance of Counter from the previous one.
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class KoinSharedStateTest : KoinTest {
private val counter by inject<Counter>()
@Test
fun a_fakeCounter() {
declareMock<Counter>()
given(counter.count).willReturn(8)
assertEquals(8, counter.count)
}
@Test
fun b_hitCounter() {
counter.hit()
assertEquals(1, counter.count)
}
}
class MyApplication : Application() {
val mainModule = module {
single { Counter(get()) }
}
override fun onCreate() {
super.onCreate()
startKoin(this, listOf(mainModule))
}
}
@Sloy
Copy link
Author

Sloy commented Jul 19, 2018

One way to fix it:

@Before
fun setUp() {
    closeKoin()
    val app = InstrumentationRegistry.getTargetContext().applicationContext as Application
    app.startKoin(app, listOf(mainModule))
}

@Sloy
Copy link
Author

Sloy commented Jul 19, 2018

An improved version is by extracting the Koin initialization to its own object, and reusing it from the Application and from tests to avoid duplication.

object KoinLoader {

    private val modules = listOf(mainModule)

    fun init(context: Context) {
        val app = context.applicationContext as Application
        app.startKoin(app, modules)
    }

    fun reload(context: Context) {
        closeKoin()
        init(context)
    }
}
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        KoinLoader.init(this)
    }
}
class MyTest {
    @Before
    fun setUp() {
        KoinLoader.reload(getTargetContext())
    }
}

@Sloy
Copy link
Author

Sloy commented Jul 19, 2018

Even more improved: Create a JUnit rule and apply it to all your tests. That way you make sure you never share Koin state between tests.

class ReloadKoinRule : MethodRule {
    override fun apply(statement: Statement, frameworkMethod: FrameworkMethod, testClassInstance: Any): Statement {
        return object : Statement() {
            override fun evaluate() {
                KoinLoader.reload(getTargetContext())
                statement.evaluate()
                KoinLoader.reload(getTargetContext())
            }
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment