hufman
hufman

Reputation: 183

Testing Android ViewModelProvider with a mock ViewModel

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

Answers (2)

hufman
hufman

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

Eddie Lopez
Eddie Lopez

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

Related Questions