core description
This commit is contained in:
parent
392ebc5115
commit
ad2a8fee80
8 changed files with 318 additions and 19 deletions
|
|
@ -1 +1,202 @@
|
|||
TODO
|
||||
# 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)
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue