15

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

Alex
  • 327
  • 3
  • 11

1 Answers1

11

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

package com.mypackage

@HiltViewModel
class MyViewModel @Inject constructor(
    private val someDependency: MyType
) : ViewModel()

Then the generated module would look something like this:

@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
    @StringKey("com.mypackage.MyViewModel")
    @HiltViewModelMap
    public abstract ViewModel binds(MyViewModel vm);
  }

  @Module
  @InstallIn(ActivityRetainedComponent.class)
  public static final class KeyModule {
    private KeyModule() {
    }

    @Provides
    @IntoSet
    @HiltViewModelMap.KeySet
    public static String provide() {
      return "com.mypackage.MyViewModel";
    }
  }
}

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()
    }
}
mvbrenes
  • 517
  • 5
  • 9
  • Where does "mock" come from? – Andrew Aug 28 '21 at 16:04
  • 2
    that's a mockito extension, from the [mockito-kotlin library](https://github.com/mockito/mockito-kotlin), not part of jetpack. `mockMyViewModel` can be any mock class or a "fake" made from a child class of `MyViewModel`, as long as the type is the same it will replace the binding. – mvbrenes Aug 30 '21 at 21:14
  • 18
    I think this answer has misunderstood the question. The question is how to inject the 'real' ViewModel into a test, not how to replace it with a mock on your dependency graph – Jahnold Oct 25 '21 at 16:31
  • I am getting "java.lang.NullPointerException at androidx.lifecycle.ViewModel.setTagIfAbsent" error when using @BindValue and mock. Have you encountered this problem? – Berkay Kireçci Mar 14 '22 at 14:13
  • In my case I am using mockk so I had to add "relaxed" parameter to true to make it work. This is: mockk(relaxed = true) – Rubén Viguera May 23 '23 at 12:05