0

I'm trying to do some Android Tests with Koin and so far, it is not a success.

I want to test a basic Activity with a ViewModel, injected by Koin.

I already read posts like NoBeanDefFoundException with Mock ViewModel, testing with Koin, Espresso but so far I still have the error.


Here is the code relative to the tests configuration

A specific app that start with no module.

class MyTestApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin { emptyList<Module>() }
    }
}

A specific runner that uses the test app

class OccazioTestRunner : AndroidJUnitRunner() {
    override fun newApplication(
        cl: ClassLoader?,
        className: String?,
        context: Context?
    ): Application {
        return super.newApplication(cl, MyTestApplication::class.java.name, context)
    }
}

That is defined in my app build.gradle to be used as runner

android {
    defaultConfig {
       testInstrumentationRunner "fr.dsquad.occazio.occazio.OccazioTestRunner"
    }
}

And now the code I want to test

In my MyActivity

class MyActivity : AppCompatActivity(R.layout.activity_my) {

    private val myViewModel by viewModel<MyViewModel>()

    // Some code
}

And the viewmodel

class MyViewModel(private val useCase: MyUseCase): ViewModel() {
   // Some code
}


And finally, the test itself (in androidTest)

@LargeTest
class MyActivityTest : KoinTest {

    private lateinit var mockUseCase: MyUseCase

    @JvmField
    @Rule
    val activityRule = activityScenarioRule<MyActivity>()

    @Before
    fun setup() {
        mockUseCase = mock(MyUseCase::class.java)

        startKoin {
            modules(module { viewModel { MyViewModel(mockUseCase) } })
        }

        // I've also tried this
        loadKoinModules(
            module { viewModel { MyViewModel(mockUseCase) } }
        )
    }

    @After
    fun cleanUp() {
        stopKoin()
    }

    @Test
    fun someTest() = runBlocking {
        // Mock the usecase response
        `when`(mockUseCase.doSomething()).thenReturn("taratata")

        // Start the scenario
        val scenario = activityRule.scenario

        // Verify we call the getUserId
        // Activity is supposed to call the view model that will call the method doSomethingAdterThat.
        verify(mockUseCase, times(1)).doSomethingAfterThat()

        return@runBlocking
    }
}

And so far, everytime I run this code I have this error

org.koin.core.error.NoBeanDefFoundException: 
No definition found for 'mypackage.MyViewModel' has been found. Check your module definitions.

What is interesting is that, when

  1. I change the rule activityScenarioRule by the old deprecated ActivityTestRule(SplashScreenActivity::class.java, true, false)
  2. I change val scenario = activityRule.scenario by val scenario = activityRule.launchActivity(null)
  3. I use loadKoinModules and not startKoin in setUp

Two things happen

  1. When my test is started alone (via Android Studio): it passes.
  2. When my test is started with other tests (by the class or with connectedAndroidTest), only one of them passes and old the others are KO.

So I have two questions in fact here.

  1. How can I make this test work with activityScenarioRule ?
  2. How can I make them "all" work (and not start them one by one to make them work) ?
Quentin Klein
  • 1,263
  • 10
  • 27

1 Answers1

0

Ok, don't ask me how it works but I figured it out.

First of all, as I needed config I followed this https://medium.com/stepstone-tech/better-tests-with-androidxs-activityscenario-in-kotlin-part-1-6a6376b713ea .

I've done 3 things

First, I needed to configure koin before startup, to do that, I needed to use ActivityScenario.launch() with an intent that I defined earlier

private val intent = Intent(ApplicationProvider.getApplicationContext(), MyActivity::class.java)
var activityRule : ActivityScenario<MyActivity>? = null

// And then I can start my activity calling
activityRule = ActivityScenario.launch(intent)

Then "KoinApp was not started"... I just replaced the loadKoinModules block with the startKoin one in setUp

startKoin { modules(module { viewModel { MyViewModel(mockUseCase) } }) }

Finally, it worked for 1 test, but the others were failing because "KoinAppAlreadyStartedException" like the stopKoin() was not called. So I found out that I should extend AutoCloseKoinTest instead of KoinTest.. But no success. In the end, I've put a stopKoin() before the startKoin and now, everything works like a charm.

Here is my complete code that works

@LargeTest
class MyActivityTest : KoinTest() {

    private val intent = Intent(ApplicationProvider.getApplicationContext(), MyActivity::class.java)
    var activityRule : ActivityScenario<MyActivity>? = null

    private lateinit var mockUseCase: MyUseCase

    @Before
    fun setup() {
        mockUseCase = mock(MyUseCase::class.java)
        stopKoin()
        startKoin {
            modules(module { viewModel { MyViewModel(mockUseCase) } })
        }
    }

    @After
    fun cleanUp() {
        activityRule?.close()
    }

    @Test
    fun someTest() = runBlocking {
        // Mock the usecase response
        `when`(mockUseCase.doSomething()).thenReturn("taratata")

        // Start the rule
        val activityRule = ActivityScenario.launch(intent)

        // Verify we call the getUserId
        // Activity is supposed to call the view model that will call the method doSomethingAdterThat.
        verify(mockUseCase, times(1)).doSomethingAfterThat()

        return@runBlocking
    }

}

Ho, I've also added this code to my two Applications

override fun onTerminate() {
    super.onTerminate()
    stopKoin()
}

Just to be sure !

Quentin Klein
  • 1,263
  • 10
  • 27