Reputation: 183
I'm excited to use the new Android Architecture Components ViewModel system, which nicely separates the Activity/Fragment/Layout rendering concerns from the ViewModel logic. I've successfully unit-tested the ViewModel in isolation, and now would like to try out some screenshot testing by providing mocked ViewModels to the Activity/Fragment for various state scenarios.
I've successfully configured my androidTests to be able to use Mockito in my device tests, that part works great.
However, the officially recommended way of calling ViewModelProvider or delegating by viewModels<>
does not seem to offer a way to inject mocked ViewModels. I'd rather not add an entire DI framework just to work around this omission in the documentation, so I wonder if anyone has any successful examples of providing mocked ViewModels with the official Android Architecture Components without the extra dependencies of Dagger or Hilt.
The only related answer from 1 year ago suggests using ActivityTestRule
and manually controlling the activity lifecycle, but that Rule is deprecated in favor of activityScenarioRule
which does not provide this control.
Upvotes: 7
Views: 3826
Reputation: 183
I decided to rewrite the by viewModels
delegate to check for instances in a map of mock ViewModels, so my activities can use the normal delegate pattern and provide their own factories if the ViewModel isn't found.
val mockedViewModels = HashMap<Class<*>, ViewModel>()
@MainThread
inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
): Lazy<VM> {
// the production producer
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}
return createMockedViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}
/// ... and similar for the fragment-ktx delegates
/**
* Wraps the default factoryPromise with one that looks in the mockedViewModels map
*/
fun <VM : ViewModel> createMockedViewModelLazy(
viewModelClass: KClass<VM>,
storeProducer: () -> ViewModelStore,
factoryPromise: () -> ViewModelProvider.Factory
): Lazy<VM> {
// the mock producer
val mockedFactoryPromise: () -> ViewModelProvider.Factory = {
// if there are any mocked ViewModels, return a Factory that fetches them
if (mockedViewModels.isNotEmpty()) {
object: ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return mockedViewModels[modelClass] as T
?: factoryPromise().create(modelClass) // return the normal one if no mock found
}
}
} else {
// if no mocks, call the normal factoryPromise directly
factoryPromise()
}
}
return ViewModelLazy(viewModelClass, storeProducer, mockedFactoryPromise)
}
Upvotes: 1
Reputation: 1139
You can use a ViewModelProvider
, so you can replace the ViewModelProvider.Factory
in the tests with a mock. For example by using:
viewModel = ViewModelProvider(this, ViewModelFactoryOfFactory.INSTANCE)
.get(MyViewModel::class.java)
Where:
object ViewModelFactoryOfFactory {
// The default factory.
var INSTANCE: ViewModelProvider.Factory = MyViewModelFactory()
private set
// To set the factory during tests.
@VisibleForTesting
fun setTestFactory(factory: ViewModelProvider.Factory) {
ViewModelFactoryOfFactory.INSTANCE = factory
}
}
Then in the tests setup one can:
ViewModelFactoryOfFactory.setTestFactory(mockFactory)
One may argue that all this could be replaced by just the factory to get the ViewModel.
Another option could be just make the ViewModelProvider.Factory
a field/property in the Activity or fragment, so it can be also set from tests, also allowing for better memory management.
Upvotes: 2