Search code examples
androidunit-testingandroid-testingandroid-architecture-componentsandroid-viewmodel

How to ensure ViewModel#onCleared is called in an Android unit test?


This is my MWE test class, which depends on AndroidX, JUnit 4 and MockK 1.9:

class ViewModelOnClearedTest {
    @Test
    fun `MyViewModel#onCleared calls Object#function`() = mockkObject(Object) {
        MyViewModel::class.members
            .single { it.name == "onCleared" }
            .apply { isAccessible = true }
            .call(MyViewModel())

        verify { Object.function() }
    }
}

class MyViewModel : ViewModel() {
    override fun onCleared() = Object.function()
}

object Object {
    fun function() {}
}

Note: the method is protected in superclass ViewModel.

I want to verify that MyViewModel#onCleared calls Object#function. The above code accomplished this through reflection. My question is: can I somehow run or mock the Android system so that the onCleared method is called, so that I don't need reflection?

From the onCleared JavaDoc:

This method will be called when this ViewModel is no longer used and will be destroyed.

So, in other words, how do I create this situation so that I know onCleared is called and I can verify its behaviour?


Solution

  • TL;DR

    In this answer, Robolectric is used to have the Android framework invoke onCleared on your ViewModel. This way of testing is slower than using reflection (like in the question) and depends on both Robolectric and the Android framework. That trade-off is up to you.


    Looking at Android's source...

    ...you can see that ViewModel#onCleared is only called in ViewModelStore (for your own ViewModels). This is a storage class for view models and is owned by ViewModelStoreOwner classes, e.g. FragmentActivity. So, when does ViewModelStore invoke onCleared on your ViewModel?

    It has to store your ViewModel, then the store has to be cleared (which you cannot do yourself).

    Your view model is stored by the ViewModelProvider when you get your ViewModel using ViewModelProviders.of(FragmentActivity activity).get(Class<T> modelClass), where T is your view model class. It stores it in the ViewModelStore of the FragmentActivity.

    The store is clear for example when your fragment activity is destroyed. It's a bunch of chained calls that go all over the place, but basically it is:

    1. Have a FragmentActivity.
    2. Get its ViewModelProvider using ViewModelProviders#of.
    3. Get your ViewModel using ViewModelProvider#get.
    4. Destroy your activity.

    Now, onCleared should be invoked on your view model. Let's test it using Robolectric 4, JUnit 4, MockK 1.9:

    1. Add @RunWith(RobolectricTestRunner::class) to your test class.
    2. Create an activity controller using Robolectric.buildActivity(FragmentActivity::class.java)
    3. Initialise the activity using setup on the controller, this allows it to be destroyed.
    4. Get the activity with the controller's get method.
    5. Get your view model with the steps described above.
    6. Destroy the activity using destroy on the controller.
    7. Verify the behaviour of onCleared.

    Full example class...

    ...based on the question's example:

    @RunWith(RobolectricTestRunner::class)
    class ViewModelOnClearedTest {
        @Test
        fun `MyViewModel#onCleared calls Object#function`() = mockkObject(Object) {
            val controller = Robolectric.buildActivity(FragmentActivity::class.java).setup()
    
            ViewModelProviders.of(controller.get()).get(MyViewModel::class.java)
    
            controller.destroy()
    
            verify { Object.function() }
        }
    }
    
    class MyViewModel : ViewModel() {
        override fun onCleared() = Object.function()
    }
    
    object Object {
        fun function() {}
    }