How to inject mocked ViewModel into Fragment during Instrumented tests with Android Hilt?

1.4k views Asked by At

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)

0

There are 0 answers