18

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?

Erik
  • 4,305
  • 3
  • 36
  • 54
  • You could `public override fun onCleared()`, but that exposes the method which is not good, as the method should only be called by the Android system. – Erik Jan 09 '19 at 19:14

4 Answers4

18

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

class MyViewModel: ViewModel() {
    public override fun onCleared() {
        ///...
    }
}
miguel
  • 16,205
  • 4
  • 53
  • 64
  • 13
    A little ``@RestrictTo(RestrictTo.Scope.TESTS)`` on top of `onCleared()` and it's all good – Charly Lafon Jul 06 '20 at 09:53
  • 2
    with androidx.annotation: ```@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) public override fun onCleared() {...``` – tzanke Oct 19 '22 at 12:27
  • 1
    Note: this won't call .close() for tags and closables registered with the ViewModel. So you might be testing behavior that never happens in real life. (e.g. using a closable that would be closed passes in tests, but crashes in app) – TWiStErRob Jun 20 '23 at 13:39
13

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
}
  • This depends on `ViewModelStore#clear` to call `ViewModel#onCleared`, and therefore that call is somewhat hidden among these lines of code. However, this solution is quite nice due to its brevity, no need for reflection nor Robolectric / Android tests, and the fact that on Android normally it's a `ViewModelStore` instance that performs the call. Thanks for your answer! – Erik Aug 02 '19 at 16:14
  • Results in "CreationExtras must have a value by `SAVED_STATE_REGISTRY_OWNER_KEY`" – John Glen Jan 01 '23 at 00:28
  • This is genius! Perfectly simulates what happens in real life, ensuring `Closeable`s are also `close()`d before calling `onCleared`, so it exhibits the same behavior as if it was running in the app. – TWiStErRob Jun 20 '23 at 13:48
5

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() {}
}
Erik
  • 4,305
  • 3
  • 36
  • 54
0

For Java, if you create your test class in the same package (within the test directory) as of the ViewModel class (here, MyViewModel), then you can call onCleared method from the test class; since protected methods are also package private.

Asdinesh
  • 183
  • 1
  • 6
  • Yes, it works like that for Java. But I'm not using Java. – Erik Oct 12 '21 at 14:08
  • 1
    Yes, I know, it for users who are looking for a solution in Java; since the title does not specifically mention 'in Kotlin'. – Asdinesh Oct 12 '21 at 14:17
  • FYI, this also works in Kotlin. You can change the package within the file, for example, **package androidx.lifecycle**, and then that will let you access package-private stuff. – Trevor Aug 16 '22 at 02:24
  • Tip: don't create the test class in the same package, that's confusing. Just create a `ViewModelAccessor` class with a static `clear(ViewModel)` method. It's a bit more reusable. And it's callable from Kotlin and Java easily. – TWiStErRob Jun 20 '23 at 13:46