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?
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.
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.
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.
In kotlin you can override the protected visibility using public
and then call it from a test.
class MyViewModel: ViewModel() {
public override fun onCleared() {
///...
}
}
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
}
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.
...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:
FragmentActivity
.ViewModelProvider
using ViewModelProviders#of
.ViewModel
using ViewModelProvider#get
.Now, onCleared
should be invoked on your view model. Let's test it using Robolectric 4, JUnit 4, MockK 1.9:
@RunWith(RobolectricTestRunner::class)
to your test class.Robolectric.buildActivity(FragmentActivity::class.java)
setup
on the controller, this allows it to be destroyed.get
method.destroy
on the controller.onCleared
....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() {}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With