I am having a hard time to get my instrumented tests on Android.
Goal: Inject a mocked ViewModel
during a Fragment
Instrumented test.
Context:
My ViewModel
is built using the Hilt Jetpack integrations and the @ViewModelInject
annotation as the following:
class OverviewViewModel @ViewModelInject constructor(
private val coroutineScopeProvider: CoroutineScope?,
private val repository: Repository
): ViewModel() {
private val coroutineScope = getViewModelScope(coroutineScopeProvider)
val isLogged = repository.isLogged
val session = repository.session
fun logout() {
coroutineScope.launch {
repository.logout()
}
}
}
fun ViewModel.getViewModelScope(coroutineScope: CoroutineScope?) =
coroutineScope ?: this.viewModelScope
// Need to do that to be able to test the viewModel
@Module
@InstallIn(ActivityComponent::class)
object CoroutineModel {
@Provides
fun provideViewScopeModel(): CoroutineScope? = null
}
My Fragment uses the ViewModel
as follows:
@AndroidEntryPoint
class OverviewFragment : Fragment() {
private val viewModel: OverviewViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = DataBindingUtil.inflate<FragmentOverviewBinding>(inflater,
R.layout.fragment_overview,container,false)
binding.viewModel = viewModel
binding.lifecycleOwner = viewLifecycleOwner
binding.loginButton.setOnClickListener {
val intent = SessionUtil.getAuthIntent()
startActivity(intent)
}
binding.logoutButton.setOnClickListener {
viewModel.logout()
}
return binding.root
}
}
What I have tried:
I would like to inject a mocked OverviewViewModel
so I can isolate the Fragment
test checking if the button click events are connected correctly with it.
Here is my test so far:
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class OverviewFragmentTest {
val hiltRule = HiltAndroidRule(this)
@get: Rule
val testRules = RuleChain
.outerRule(hiltRule)
.around(ActivityTestRule(MainActivity::class.java))
val mockViewModel = mockkClass(OverviewViewModel::class)
val mockIsLogged = MutableLiveData<Boolean>()
@BindValue @JvmField
val viewModel: OverviewViewModel = mockViewModel
@Before
fun setup () {
clearAllMocks()
hiltRule.inject()
}
@Test
fun Given_nothing_When_clicking_login_button_Then_login_intent_triggers() {
every {viewModel.isLogged} returns mockIsLogged
mockIsLogged.postValue(false)
Intents.init()
every { SessionUtil.getAuthIntent() } returns Intent(Intent.ACTION_VIEW, Uri.parse("https://toto"))
launchFragmentInHiltContainer<OverviewFragment>()
onView(withId(R.id.login_button)).perform(click())
verify {
SessionUtil.getAuthIntent()
}
intended(
hasAction(Intent.ACTION_VIEW)
)
intended(
hasData("https://toto")
)
Intents.release()
}
@Test
fun Given_null_response_When_clicking_logout_button_Then_call_toaster() {
every {viewModel.isLogged} returns mockIsLogged
mockIsLogged.postValue(true)
launchFragmentInHiltContainer<OverviewFragment>()
onView(withId(R.id.logout_button)).perform(click())
verify {
mockViewModel.logout()
}
}
}
Actual: It seems that the fragment still uses the real ViewModel
since even when posting a value (e.g. mockIsLogged.postValue(false)
), the observer inside the Fragment still logs true
(value coming from real model)