JJaviMS
JJaviMS

Reputation: 422

Android test Fragmen with mock ViewModel using Hilt

I´m developing an app using Hilt, all works fine but when I try to run some Espresso test on a device running on below Android P I have encountered an issue.

The problem comes when I try to mock (using Mockk) the ViewModel so I can unit test my Fragment. When the Fragment will try to instanciate te ViewModel I got a NullPointerException when the ViewModel is being created. The NPE is thrown on the method setTagIfAbsent. The problem is that this method is package private as you can see on ViewModel source code, so it can not be mocked on Android < P.

I have tried by using the Kotlin All-Open plugin, it has helped on mocking the ViewModel and stubing it public methods. I try to stub the setTagIfAbsent by using the mockk private stubbing, like this:

every{
    myViewModelMock["setTagIfAbsent"](any<String>,any())
} answers {secondArg()}

But when setTagIfAbsent is called, the real method is invoked, throwing the NPE because the ViewModel.mBagOfTags is null because the class is a mock.

The rest of the code is the following:

ViewModel:

@OpenForTesting
@HiltViewModel
class MyViewModel @Inject constructor MyViewModel(private val dependency: Dependency): ViewModel(){
    //Rest of the code
}

Fragment:

@AndroidEntryPoint
class MyFragment: Fragment(){
    private val viewModel: MyViewModel by viewModels()
    //Rest of the code
}

Test class:

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class MyFragmentTest {

    @Bind
    @MockK
    lateinit var viewModel: MyViewModel

    @get:Rule
    var hiltRule = HiltAndroidRule(this)

    @Before
    fun prepareTest(){
        MockkAnnotations.init(this)
        hiltRule.inject()
        launchFragmentInHiltContainer<MyFragment>()
    }

    @Test
    fun testThatWillMakeAViewModelInvokation(){
         onView(withId(R.id.button)).perform(click())
         //Assume that button will make the ViewModel be called and created by the delegate
         //When this happens the NPE is thrown
    }
}

The method launchFragmentInHiltContainer comes from here (Hilt sample app).

If you look at the Mockk Android documentation it is said that < Android P the private methods can not be mocked (it is also said for finals, but the OpenClass plugin fix that problem).

Does anyone have an idea of how can I workaround this or how to fix the test?

Thanks in advance.

Upvotes: 4

Views: 4389

Answers (2)

Diego Vidal
Diego Vidal

Reputation: 51

If you use mockk(relaxed = true) this problem is solved

Upvotes: 5

Petras Labutis
Petras Labutis

Reputation: 31

Instead of mocking setTagIfAbsent you can mock mBagOfTags using reflection on already mocked instance of ViewModel.

setInternalFieldValue(mockedViewModel, "mBagOfTags", HashMap<String, Any>())
fun setInternalFieldValue(target: Any, fieldName: String, value: Any, javaClass: Class<in Any> = target.javaClass) {
    try {
        val field = javaClass.getDeclaredField(fieldName)
        field.isAccessible = true
        field.set(target, value)
    } catch (exception: NoSuchFieldException) {
        val superClass = javaClass.superclass
        if (superClass != null) {
            setInternalFieldValue(target, fieldName, value, superClass)
        } else {
            throw RuntimeException("Field $fieldName is not declared in a hierarchy of this class")
        }
    }
}

Upvotes: 3

Related Questions