Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 785
Erik Avatar asked Jan 09 '19 17:01

Erik


People also ask

How do you implement a ViewModel?

There are three steps to setting up and using a ViewModel: Separate out your data from your UI controller by creating a class that extends ViewModel. Set up communications between your ViewModel and your UI controller. Use your ViewModel in your UI controller.

How does a ViewModel survive?

A ViewModel stores and manages UI-related data in a lifecycle-conscious manner. Simply put, it allows data to survive configuration changes. A ViewModel remains in the memory until, the Lifecycle it is scoped to, goes away completely.

How does ViewModel retain data?

ViewModel overview Part of Android Jetpack. Stay organized with collections Save and categorize content based on your preferences. The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations.


3 Answers

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

class MyViewModel: ViewModel() {
    public override fun onCleared() {
        ///...
    }
}
like image 109
miguel Avatar answered Sep 21 '22 11:09

miguel


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
}
like image 33
Владислав Стариков Avatar answered Sep 20 '22 11:09

Владислав Стариков


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() {}
}
like image 40
Erik Avatar answered Sep 21 '22 11:09

Erik