androidtestingandroid-espressodagger-2dagger-hilt

Hilt viewmodel injection into instrumentation tests


I was searching quite a lot for how to inject ViewModel into tests so I can test it. Lets say the viewmodel have a constructor injection with some business logic interactor. I can inject it into fragments easily but no success in tests.

@HiltAndroidTest
class ViewModelTest

 val randomViewmodel: RandomViewmodel// now what ? since by viewModels() is not accessible in tests

    @Test
    fun viewModelTet() {
        randomViewmodel.triggerAction()
        assertEquals(RandomVIewState(1), randomViewmodel.getState())
    }

I tried to implement byViewModels() in test class and could inject viewmodel without constructor arguments but no success with them.

class RandomViewmodel @ViewModelInject constructor(
     private val randomInteractor: RandomInteractor
) : ViewModel
Caused by: java.lang.InstantiationException: class app.RandomViewModel has no zero argument constructor

Reason: I want to be able to fully test my screen logic since the viewModel will handle the dependencies on interactors etc. there might be quite a lot of logic behind with various data flowing around. Testing the fragment would be most likely possible but way slower in a larger poject with a lot of tests.

I already read the https://developer.android.com/jetpack/guide#test-components , which is suggesting doing JUnit tests and mocking the dependencies in viewModel but then you have to create tests for each dependency separatelly and cant really test the logic for the whole screen


Solution

  • The @HiltViewModel annotation, generates binding Modules that you would have otherwise written.

    One of those being a module called BindsModule. This class is declared inside of a wrapper class that contains that multi-binding module as well as one for the key.

    For example, let's say you have created a ViewModel called MyViewModel

    Edit:

    As of Dagger 2.51.x, ViewModel Bindings now use @LazyClassKey instead of @StringKey

    package com.mypackage
    
    @HiltViewModel
    class MyViewModel @Inject constructor(
        private val someDependency: MyType
    ) : ViewModel()
    

    Then the generated module would look something like this:

    @Generated("dagger.hilt.android.processor.internal.viewmodel.ViewModelProcessor")
    @OriginatingElement(
        topLevelClass = MyViewModel.class
    )
    public final class MyViewModel_HiltModules {
      private MyViewModel_HiltModules() {
      }
    
      @Module
      @InstallIn(ViewModelComponent.class)
      public abstract static class BindsModule {
        private BindsModule() {
        }
    
        @Binds
        @IntoMap
        @LazyClassKey(MyViewModel.class)
        @HiltViewModelMap
        public abstract ViewModel binds(MyViewModel vm);
      }
    
      @Module
      @InstallIn(ActivityRetainedComponent.class)
      public static final class KeyModule {
        private KeyModule() {
        }
    
        @Provides
        @IntoMap
        @LazyClassKey(MyViewModel.class)
        @HiltViewModelMap.KeySet
        public static boolean provide() {
          return true;
        }
      }
    }
    
    

    Therefore your ViewModel can replace that @Binds contract by simply using the @BindValue annotation on a property in your test class that matches the implementation type, in this case, it would be MyViewModel.

    No need to uninstall any modules related to the ViewModel.

    @HiltAndroidTest
    class MyFragmentInstrumentedUnitTest {
        @get:Rule val hiltRule = HiltAndroidRule(this)
    
        // either a subclass or a mock, as long as the types match
        // it will provide this instance as the implementation of the abstract binding 
        // `public abstract ViewModel binds(MyViewModel vm);`
        @BindValue
        val mockMyViewModel= mock<MyViewModel>()
    
        @Before
        fun init() {
            hiltRule.inject()
        }
    }