NoBeanDefFoundException with Mock ViewModel, testing with Koin, Espresso

Issue

I have been trying to get a simple Espresso unit test work with Koin as DI tool. Here are the dependencies that I am using in build.gradle

    // testing with Koin
    // because of this
    // https://github.com/InsertKoinIO/koin/pull/604/commits/69391bc378bbb9007b9d82c46537e7d753be7ea3
    androidTestImplementation 'org.mockito:mockito-android:3.1.0'
    androidTestImplementation ("org.koin:koin-test:$koin_version") {
        exclude group: 'org.mockito'
    }

    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    // stuff like ActivityTestRule
    androidTestImplementation 'androidx.test:rules:1.2.0'
    // AndroidJUnit4
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    // test runner
    androidTestImplementation 'androidx.test:runner:1.2.0'

my ViewModel declaration

open class LoginViewModel(private val apiService: MockApiService) : ViewModel() {
..
..
}

here is how its injected in Activity

private val loginViewModel: LoginViewModel by viewModel()

my custom TestRunner in order to have custom TestApplication instantiated

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

TestApplication class. I have verified that this test class gets initialised when test is invoked

class TestApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        startKoin {
            androidLogger()
            androidContext(this@TestApplication)
            modules(emptyList())
        }
    }
}

Here is my actual androidTest. This fails as soon as activity is started with NoBeanDefFoundException

No definition found for ‘com.abhishek.mvvmdemo.onboarding.LoginViewModel’ has been found.

@RunWith(AndroidJUnit4::class)
@LargeTest
class LoginActivityTest : KoinTest {
    private lateinit var loginViewModel: LoginViewModel

    @get:Rule
    val activityRule  ActivityTestRule(LoginActivity::class.java)

    @Before
    fun beforeTest() {
        loginViewModel  declareMock()
        loadKoinModules(
            module {
//                single { ApiModule.providesApiService() }
                viewModel { loginViewModel }
            }
        )
    }

    @Test
    fun testProgress() {
        activityRule.launchActivity(null)
        onView(withId(R.id.emailEt))
            .perform(ViewActions.typeText("abhishek"))
    }

    @After
    fun afterTest() {
        stopKoin()
    }
}

I have tried a lot of permutation and combinations but got no luck. I also happen to have following configuration in my gradle

testOptions {
        animationsDisabled  true
    }

    packagingOptions {
        pickFirst 'mockito-extensions/org.mockito.plugins.MockMaker'
    }

and

testInstrumentationRunner "com.abhishek.mvvmdemo.MyTestRunner"

TL;DR

Here is a github sample that reproduces the issue

Solution

What’s happening here is that the ActivityTestRule launches the activity before your @Before method, so the mock has no chance to be initialized.

From the official documentation,

This rule provides functional testing of a single Activity. When launchActivity is set to true in the constructor, the Activity under test will be launched before each test annotated with Test and before methods annotated with Before, and it will be terminated after the test is completed and methods annotated with After are finished.

You should instead specify that you do not want to launch the activity automatically, by using this constructor

ActivityTestRule (Class<T> activityClass, 
                boolean initialTouchMode, 
                boolean launchActivity)

Then in your test method you can launch your activity manually by

activityRule.launchActivity(null)

Also, you may want to check out https://mockk.io/ for mocking. You will not have to declare your classes as open.

Answered By – Anuj

Leave a Comment