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:
- Have a
FragmentActivity
. - Get its
ViewModelProvider
usingViewModelProviders#of
. - Get your
ViewModel
usingViewModelProvider#get
. - Destroy your activity.
Now, onCleared
should be invoked on your view model. Let's test it using Robolectric 4, JUnit 4, MockK 1.9:
- Add
@RunWith(RobolectricTestRunner::class)
to your test class. - Create an activity controller using
Robolectric.buildActivity(FragmentActivity::class.java)
- Initialise the activity using
setup
on the controller, this allows it to be destroyed. - Get the activity with the controller's
get
method. - Get your view model with the steps described above.
- Destroy the activity using
destroy
on the controller. - 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
}