Reputation: 422
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
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