InsertKoinIO / koin

Koin - a pragmatic lightweight dependency injection framework for Kotlin & Kotlin Multiplatform
https://insert-koin.io
Apache License 2.0
9.12k stars 722 forks source link

No root scoped initialized for a ViewModel Unit Test #770

Closed ahinchman1 closed 4 years ago

ahinchman1 commented 4 years ago

Hi! It's likely this is not a bug, but I'm perplexed about why my ViewModel unit test keeps asking for a root scope to be initialized when it wasn't necessary for weaving Koin in to the actual project.

Scoping didn't appear to be relevant for the actual implementation, so why does it matter for testing? In other examples I've viewed, there's appears nothing needed regarding scope in other examples I've seen.

Error message:

java.lang.IllegalStateException: No root scoped initialized

    at org.koin.core.registry.ScopeRegistry.getRootScope(ScopeRegistry.kt:48)
    at com.fortysevendeg.android.scaladays.MainActivityViewModelTest$$special$$inlined$inject$1.invoke(KoinTest.kt:50)
    at kotlin.UnsafeLazyImpl.getValue(Lazy.kt:81)
    at com.fortysevendeg.android.scaladays.MainActivityViewModelTest.getContext(MainActivityViewModelTest.kt)
    at com.fortysevendeg.android.scaladays.MainActivityViewModelTest.access$getContext$p(MainActivityViewModelTest.kt:26)

I've tried several different combinations all the with the same result:

class MainActivityViewModelKoinTest : KoinTest {
    @get: Rule
    val rule = InstantTaskExecutorRule()

    @get:Rule
    val koinTestRule = KoinTestRule.create {
        printLogger(Level.DEBUG)
        koinApplication {
                App.modules(context) +
                DataSource.modules(TWITTER_API_KEY, TWITTER_API_SECRET) +
                Feature.modules +
                Repository.modules +
                Service.modules(BASE_URL)
        }
    }

    @After
    fun close() {
        stopKoin()
    }

    @get:Rule
    val mockProvider = MockProviderRule.create { clazz ->
        Mockito.mock(clazz.java)
    }

    private val context: Context by inject()
    private val navigator: Navigator by inject()
    private val viewModel: MainActivityViewModel by inject()

    @Test
    fun `Load conferences successfully`() {
        val success = Result.Success(20201)
        getCurrentConferenceId(success)

        viewModel.loadConferences(false)
        verify(navigator, never()).navigateToError(from = Screen.SCHEDULE)
    }

    @Test
    fun `Load conferences failure navigates to Error screen`() {
        val error = Result.Failure(Error("Unable to load conferences."))
        getCurrentConferenceId(error)

        viewModel.loadConferences(false)
        verify(navigator).navigateToError(from = Screen.SCHEDULE)
    }

    private fun getCurrentConferenceId(result: Result<Int>) =
        runBlocking {
            declareMock<ConferenceRepository> {
                given(getCurrentConferenceId()).will { result }
            }
        }

    companion object {
        const val TWITTER_API_KEY = "TWITTER_API_KEY"
        const val TWITTER_API_SECRET = "TWITTER_API_SECRET"
        const val BASE_URL = "BASE_URL"
    }
}

I also moved the Koin module to individual tests to see if that gave me anything different (it removed the context already initialized issue, but I am still getting the same error

    @Test
    fun `Load conferences successfully`() {
        startKoin {
            module {
                App.modules(context) + DataSource.modules(TWITTER_API_KEY, TWITTER_API_SECRET) +
                        Feature.modules + Repository.modules +  Service.modules(BASE_URL)
            }
        }

        val success = Result.Success(20201)
        getCurrentConferenceId(success)

        viewModel.loadConferences(false)
        verify(navigator, never()).navigateToError(from = Screen.SCHEDULE)
    }

Even when I attempted to add the scope, I was still getting the same error message:

    @Test
    fun `Load conferences successfully`() {
        startKoin {
            module {
                App.modules(context) + DataSource.modules +
                        Feature.modules + Repository.modules +  Service.modules
                scope<MainActivity> {
                    scoped { MainActivityViewModel }
                }
            }
        }
        val success = Result.Success(20201)
        getCurrentConferenceId(success)
        viewModel.loadConferences(false)
        verify(navigator, never()).navigateToError(from = Screen.SCHEDULE)
    }

Any pointers would be greatly appreciated! Thanks!

arnaudgiuliani commented 4 years ago

java.lang.IllegalStateException: No root scoped initialised

is typical of something that tries to run on Koin, but it has not been started. ViewModel will follow lifecycle, try to not use scopes as much as you can.

arnaudgiuliani commented 4 years ago

Should perhaps update your start:

@get:Rule
    val koinTestRule = KoinTestRule.create {
        printLogger(Level.DEBUG)
        modules( // modules here)
        }
    }