core description

This commit is contained in:
Gergely Hegedus 2021-09-18 11:51:02 +03:00
parent 392ebc5115
commit ad2a8fee80
8 changed files with 318 additions and 19 deletions

View file

@ -1,4 +1,4 @@
# Starting of testing
# 1. Starting of testing
In this testing instruction set you will learn how to write simple tests using mockito.
@ -457,18 +457,20 @@ In this case we should recreate our sut in the test and feed it our own remote s
#### Still using mockito
To mock such behaviour with mockito is not as straight forward as creating our own.
That's because mockito is not aware of the nature of suspend functions, like our code is in the custom mock.
To mock such behaviour with mockito with our current tool set is not as straight forward as creating our own.
That's because how we used mockito so far it is not aware of the nature of suspend functions, like our code is in the custom mock.
However mockito give us the arguments passed into the function.
And since we know the Continuation object is passed as a last argument in suspend functions we can take advantage of that.
This then can be abstracted away and used wherevere without needing to create Custom Mocks for every such case.
This then can be abstracted away and used wherever without needing to create Custom Mocks for every such case.
To get arguments when creating a response for the mock you need to use thenAnswer { } and this lambda will receive InvocationOnMock containing the arguments.
Similarly our suspendable answer is named as such `doSuspendableAnswer`
Luckily this has already be done in "org.mockito.kotlin" and it's called `doSuspendableAnswer`
The point here is not exactly how the doSuspendableAnswer is created, just that we can get arguments while mocking with mockito, and also extend it in a way that helps us in common patterns.
The point here is that we can get arguments while mocking with mockito, and we are able to extend it in a way that helps us in common patterns.
This `doSuspendableAnswer` wasn't available for a while, but we could still create it, if needed.
#### Back to the actual test

View file

@ -1,4 +1,4 @@
# Starting of networking testing
# 2. Starting of networking testing
In this testing instruction set you will learn how to write simple tests with retrofit.

View file

@ -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)
```