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

In kotlin you can override the protected visibility using public and then call it from a test.

class MyViewModel: ViewModel() {
    public override fun onCleared() {
        ///...
    }
}

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() {}
}

I've just created this extension to ViewModel:

/**
 * Will create new [ViewModelStore], add view model into it using [ViewModelProvider]
 * and then call [ViewModelStore.clear], that will cause [ViewModel.onCleared] to be called
 */
fun ViewModel.callOnCleared() {
    val viewModelStore = ViewModelStore()
    val viewModelProvider = ViewModelProvider(viewModelStore, object : ViewModelProvider.Factory {

        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel?> create(modelClass: Class<T>): T = this@callOnCleared as T
    })
    viewModelProvider.get(this@callOnCleared::class.java)

    //Run 2
    viewModelStore.clear()//To call clear() in ViewModel
}