# 3. Starting of ViewModel testing In this testing instruction set you will learn how to write simple tests for ViewModels. - We will use TestCoroutineDispatcher for time manipulation - Learn how to use TestCorotineDispatcher in ViewModels - And how to test LiveData - how to use extensions - how to parametrize a test ## SplashViewModel test Our system under test will be org.fnives.test.showcase.ui.splash.SplashViewModel What it does is: - waits 500 milliseconds - checks if the user logged in - sends navigated event based on the check ### Setup So let's start with the setup. To properly test LiveData we need to make them instant. To Do this we can use a InstantExecutorExtension. Also to set MainDispatcher as TestCoroutineDispatcher, we can use the TestMainDispatcher Extension. To add this to our TestClass we need to do the following: ```kotlin @ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) class CodeKataSplashViewModelTest { ``` Note you can use @RegisterExtension to register an extension as a field and make it easier to reference. Next let's setup or system under test: ```kotlin private lateinit var mockIsUserLoggedInUseCase: IsUserLoggedInUseCase private lateinit var sut: SplashViewModel private val testCoroutineDispatcher get() = TestMainDispatcher.testDispatcher // just a shortcut @BeforeEach fun setUp() { mockIsUserLoggedInUseCase = mock() sut = SplashViewModel(mockIsUserLoggedInUseCase) } ``` ### 1. `loggedOutUserGoesToAuthentication` So let's setup our mock ```kotlin whenever(mockIsUserLoggedInUseCase.invoke()).doReturn(false) ``` Since the action takes place in the ViewModel constructor, instead of additional calls, we need to simulate that time has elapsed. Note: the Extension we are using is pausing the dispatcher, that's why we the test is linear and not shaky. ```kotlin testCoroutineDispatcher.advanceTimeBy(500) ``` Next verify that we navigated to Authentication ```kotlin sut.navigateTo.test().assertValue(Event(SplashViewModel.NavigateTo.AUTHENTICATION)) ``` Here test() is a LiveData extension. It helps to verify value history, current value and observers the value properly. If a livedata is not observed, it's value may not update (like a livedata that maps) so it's important to have a proper TestObserver set. ### 2. `loggedInUserGoestoHome` This is really similar to `loggedOutUserGoesToAuthentication`, so here is the complete code: ```kotlin whenever(mockIsUserLoggedInUseCase.invoke()).doReturn(true) testCoroutineDispatcher.advanceTimeBy(500) sut.navigateTo.test().assertValue(Event(SplashViewModel.NavigateTo.HOME)) ``` ### 3. `withoutEnoughTimeNoNavigationHappens` Not let's verify that if the time didn't elapse then the event is not sent out: ```kotlin whenever(mockIsUserLoggedInUseCase.invoke()).doReturn(false) testCoroutineDispatcher.advanceTimeBy(100) // we wait only 100ms not 500ms sut.navigateTo.test().assertNoValue() // this is the way to test that no value has been sent out ``` ## AuthViewModelTest Test Our system under test will be org.fnives.test.showcase.ui.auth.AuthViewModel What it does is: - observes input username and password - tries to login with the given data - processes the response and either navigates or shows an error Let's open CodeKataAuthViewModel. The setup is already done because it's almost the same as menitoned above. ### 1. `initialSetup` Let's start with the basics. So first let's verify when the viewModel is setup all LiveData contain the correct data. First we resume the dispatcher, then verify the livedata. ```kotlin testDispatcher.resumeDispatcher() sut.username.test().assertNoValue() sut.password.test().assertNoValue() sut.loading.test().assertValue(false) sut.error.test().assertNoValue() sut.navigateToHome.test().assertNoValue() ``` ### 2. `whenPasswordChangedLiveDataIsUpdated` Here we need to test the livedata updates as we change the password. So first let's add a subscribed to the ViewModel which we plan to verify: ```kotlin testDispatcher.resumeDispatcher() val passwordTestObserver = sut.password.test() ``` Next we do the action and update the password: ```kotlin sut.onPasswordChanged("a") sut.onPasswordChanged("al") ``` And at the end we verify the passwordTestObserver was updated: ```kotlin passwordTestObserver.assertValueHistory("a", "al") sut.username.test().assertNoValue() sut.loading.test().assertValue(false) sut.error.test().assertNoValue() sut.navigateToHome.test().assertNoValue() ``` ### 3. `whenUsernameChangedLiveDataIsUpdated` This is esentially the same as whenPasswordChangedLiveDataIsUpdated, just for the username: ```kotlin testDispatcher.resumeDispatcher() val usernameTestObserver = sut.username.test() sut.onUsernameChanged("a") sut.onUsernameChanged("al") usernameTestObserver.assertValueHistory("a", "al") sut.password.test().assertNoValue() sut.loading.test().assertValue(false) sut.error.test().assertNoValue() sut.navigateToHome.test().assertNoValue() ``` ### 4. `noPasswordUsesEmptyStringInLoginUseCase` Now let's test some actual logic: If we didn't give username and password to the ViewModel when login is clicked we should see loading, empty string passed to the UseCase Let's setup to login ```kotlin val loadingTestObserver = sut.loading.test() runBlocking { whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Error(Throwable())) } ``` Let's do the action ```kotlin sut.onLogin() testDispatcher.advanceUntilIdle() // ensure the coroutine has run ``` verify the loading and the useCase call ```kotlin loadingTestObserver.assertValueHistory(false, true, false) runBlocking { verify(mockLoginUseCase, times(1)).invoke(LoginCredentials("", "")) } verifyNoMoreInteractions(mockLoginUseCase) ```