core description
This commit is contained in:
parent
392ebc5115
commit
ad2a8fee80
8 changed files with 318 additions and 19 deletions
|
|
@ -14,6 +14,7 @@ class TestMainDispatcher : BeforeEachCallback, AfterEachCallback {
|
||||||
override fun beforeEach(context: ExtensionContext?) {
|
override fun beforeEach(context: ExtensionContext?) {
|
||||||
val testDispatcher = TestCoroutineDispatcher()
|
val testDispatcher = TestCoroutineDispatcher()
|
||||||
privateTestDispatcher = testDispatcher
|
privateTestDispatcher = testDispatcher
|
||||||
|
testDispatcher.pauseDispatcher()
|
||||||
DatabaseInitialization.dispatcher = testDispatcher
|
DatabaseInitialization.dispatcher = testDispatcher
|
||||||
Dispatchers.setMain(testDispatcher)
|
Dispatchers.setMain(testDispatcher)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import org.fnives.test.showcase.testutils.InstantExecutorExtension
|
||||||
import org.fnives.test.showcase.testutils.TestMainDispatcher
|
import org.fnives.test.showcase.testutils.TestMainDispatcher
|
||||||
import org.fnives.test.showcase.ui.shared.Event
|
import org.fnives.test.showcase.ui.shared.Event
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
|
|
@ -35,13 +36,14 @@ internal class AuthViewModelTest {
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
mockLoginUseCase = mock()
|
mockLoginUseCase = mock()
|
||||||
testDispatcher.pauseDispatcher()
|
|
||||||
sut = AuthViewModel(mockLoginUseCase)
|
sut = AuthViewModel(mockLoginUseCase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DisplayName("GIVEN_initialized_viewModel_WHEN_observed_THEN_loading_false_other_fields_are_empty")
|
||||||
@Test
|
@Test
|
||||||
fun GIVEN_initialized_viewModel_WHEN_observed_THEN_loading_false_other_fields_are_empty() {
|
fun initialSetup() {
|
||||||
testDispatcher.resumeDispatcher()
|
testDispatcher.resumeDispatcher()
|
||||||
|
|
||||||
sut.username.test().assertNoValue()
|
sut.username.test().assertNoValue()
|
||||||
sut.password.test().assertNoValue()
|
sut.password.test().assertNoValue()
|
||||||
sut.loading.test().assertValue(false)
|
sut.loading.test().assertValue(false)
|
||||||
|
|
@ -49,8 +51,9 @@ internal class AuthViewModelTest {
|
||||||
sut.navigateToHome.test().assertNoValue()
|
sut.navigateToHome.test().assertNoValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DisplayName("GIVEN_password_text_WHEN_onPasswordChanged_is_called_THEN_password_livedata_is_updated")
|
||||||
@Test
|
@Test
|
||||||
fun GIVEN_password_text_WHEN_onPasswordChanged_is_called_THEN_password_livedata_is_updated() {
|
fun whenPasswordChangedLiveDataIsUpdated() {
|
||||||
testDispatcher.resumeDispatcher()
|
testDispatcher.resumeDispatcher()
|
||||||
val passwordTestObserver = sut.password.test()
|
val passwordTestObserver = sut.password.test()
|
||||||
|
|
||||||
|
|
@ -64,8 +67,9 @@ internal class AuthViewModelTest {
|
||||||
sut.navigateToHome.test().assertNoValue()
|
sut.navigateToHome.test().assertNoValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DisplayName("GIVEN_username_text_WHEN_onUsernameChanged_is_called_THEN_username_livedata_is_updated")
|
||||||
@Test
|
@Test
|
||||||
fun GIVEN_username_text_WHEN_onUsernameChanged_is_called_THEN_username_livedata_is_updated() {
|
fun whenUsernameChangedLiveDataIsUpdated() {
|
||||||
testDispatcher.resumeDispatcher()
|
testDispatcher.resumeDispatcher()
|
||||||
val usernameTestObserver = sut.username.test()
|
val usernameTestObserver = sut.username.test()
|
||||||
|
|
||||||
|
|
@ -79,8 +83,9 @@ internal class AuthViewModelTest {
|
||||||
sut.navigateToHome.test().assertNoValue()
|
sut.navigateToHome.test().assertNoValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DisplayName("GIVEN_no_password_or_username_WHEN_login_is_Called_THEN_empty_credentials_are_used_in_usecase")
|
||||||
@Test
|
@Test
|
||||||
fun GIVEN_no_password_or_username_WHEN_login_is_Called_THEN_empty_credentials_are_used_in_usecase() {
|
fun noPasswordUsesEmptyStringInLoginUseCase() {
|
||||||
val loadingTestObserver = sut.loading.test()
|
val loadingTestObserver = sut.loading.test()
|
||||||
runBlocking {
|
runBlocking {
|
||||||
whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Error(Throwable()))
|
whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Error(Throwable()))
|
||||||
|
|
@ -95,7 +100,7 @@ internal class AuthViewModelTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun WHEN_login_is_Called_twise_THEN_use_case_is_only_called_once() {
|
fun WHEN_login_is_Called_twice_THEN_use_case_is_only_called_once() {
|
||||||
runBlocking { whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Error(Throwable())) }
|
runBlocking { whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Error(Throwable())) }
|
||||||
|
|
||||||
sut.onLogin()
|
sut.onLogin()
|
||||||
|
|
@ -107,7 +112,7 @@ internal class AuthViewModelTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun GIVEN_password_and_username_WHEN_login_is_Called_THEN_empty_credentials_are_used_in_usecase() {
|
fun GIVEN_password_and_username_WHEN_login_is_Called_THEN_not_empty_credentials_are_used_in_usecase() {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Error(Throwable()))
|
whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Error(Throwable()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package org.fnives.test.showcase.ui.auth
|
||||||
|
|
||||||
|
import org.fnives.test.showcase.core.login.LoginUseCase
|
||||||
|
import org.fnives.test.showcase.testutils.InstantExecutorExtension
|
||||||
|
import org.fnives.test.showcase.testutils.TestMainDispatcher
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
|
|
||||||
|
@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class)
|
||||||
|
class CodeKataAuthViewModel {
|
||||||
|
|
||||||
|
private lateinit var sut: AuthViewModel
|
||||||
|
private lateinit var mockLoginUseCase: LoginUseCase
|
||||||
|
private val testDispatcher get() = TestMainDispatcher.testDispatcher
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setUp() {
|
||||||
|
mockLoginUseCase = mock()
|
||||||
|
sut = AuthViewModel(mockLoginUseCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("GIVEN_initialized_viewModel_WHEN_observed_THEN_loading_false_other_fields_are_empty")
|
||||||
|
@Test
|
||||||
|
fun initialSetup() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("GIVEN_password_text_WHEN_onPasswordChanged_is_called_THEN_password_livedata_is_updated")
|
||||||
|
@Test
|
||||||
|
fun whenPasswordChangedLiveDataIsUpdated() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("GIVEN_username_text_WHEN_onUsernameChanged_is_called_THEN_username_livedata_is_updated")
|
||||||
|
@Test
|
||||||
|
fun whenUsernameChangedLiveDataIsUpdated() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("GIVEN_no_password_or_username_WHEN_login_is_Called_THEN_empty_credentials_are_used_in_usecase")
|
||||||
|
@Test
|
||||||
|
fun noPasswordUsesEmptyStringInLoginUseCase() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package org.fnives.test.showcase.ui.splash
|
||||||
|
|
||||||
|
import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
|
||||||
|
import org.fnives.test.showcase.testutils.InstantExecutorExtension
|
||||||
|
import org.fnives.test.showcase.testutils.TestMainDispatcher
|
||||||
|
import org.fnives.test.showcase.ui.shared.Event
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.mockito.kotlin.doReturn
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
|
import org.mockito.kotlin.whenever
|
||||||
|
|
||||||
|
internal class CodeKataSplashViewModelTest {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setUp() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("GIVEN not logged in user WHEN splash started THEN after half a second navigated to authentication")
|
||||||
|
@Test
|
||||||
|
fun loggedOutUserGoesToAuthentication() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("GIVEN logged in user WHEN splash started THEN after half a second navigated to home")
|
||||||
|
@Test
|
||||||
|
fun loggedInUserGoestoHome() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("GIVEN not logged in user WHEN splash started THEN before half a second no event is sent")
|
||||||
|
@Test
|
||||||
|
fun withoutEnoughTimeNoNavigationHappens() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,13 +6,13 @@ import org.fnives.test.showcase.testutils.InstantExecutorExtension
|
||||||
import org.fnives.test.showcase.testutils.TestMainDispatcher
|
import org.fnives.test.showcase.testutils.TestMainDispatcher
|
||||||
import org.fnives.test.showcase.ui.shared.Event
|
import org.fnives.test.showcase.ui.shared.Event
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.mockito.kotlin.doReturn
|
import org.mockito.kotlin.doReturn
|
||||||
import org.mockito.kotlin.mock
|
import org.mockito.kotlin.mock
|
||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
|
|
||||||
@Suppress("TestFunctionName")
|
|
||||||
@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class)
|
@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class)
|
||||||
internal class SplashViewModelTest {
|
internal class SplashViewModelTest {
|
||||||
|
|
||||||
|
|
@ -26,8 +26,9 @@ internal class SplashViewModelTest {
|
||||||
sut = SplashViewModel(mockIsUserLoggedInUseCase)
|
sut = SplashViewModel(mockIsUserLoggedInUseCase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DisplayName("GIVEN not logged in user WHEN splash started THEN after half a second navigated to authentication")
|
||||||
@Test
|
@Test
|
||||||
fun GIVEN_not_logged_in_user_WHEN_splash_started_THEN_after_half_a_second_navigated_to_authentication() {
|
fun loggedOutUserGoesToAuthentication() {
|
||||||
whenever(mockIsUserLoggedInUseCase.invoke()).doReturn(false)
|
whenever(mockIsUserLoggedInUseCase.invoke()).doReturn(false)
|
||||||
|
|
||||||
testCoroutineDispatcher.advanceTimeBy(500)
|
testCoroutineDispatcher.advanceTimeBy(500)
|
||||||
|
|
@ -35,8 +36,9 @@ internal class SplashViewModelTest {
|
||||||
sut.navigateTo.test().assertValue(Event(SplashViewModel.NavigateTo.AUTHENTICATION))
|
sut.navigateTo.test().assertValue(Event(SplashViewModel.NavigateTo.AUTHENTICATION))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DisplayName("GIVEN logged in user WHEN splash started THEN after half a second navigated to home")
|
||||||
@Test
|
@Test
|
||||||
fun GIVEN_logged_in_user_WHEN_splash_started_THEN_after_half_a_second_navigated_to_home() {
|
fun loggedInUserGoestoHome() {
|
||||||
whenever(mockIsUserLoggedInUseCase.invoke()).doReturn(true)
|
whenever(mockIsUserLoggedInUseCase.invoke()).doReturn(true)
|
||||||
|
|
||||||
testCoroutineDispatcher.advanceTimeBy(500)
|
testCoroutineDispatcher.advanceTimeBy(500)
|
||||||
|
|
@ -44,8 +46,9 @@ internal class SplashViewModelTest {
|
||||||
sut.navigateTo.test().assertValue(Event(SplashViewModel.NavigateTo.HOME))
|
sut.navigateTo.test().assertValue(Event(SplashViewModel.NavigateTo.HOME))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DisplayName("GIVEN not logged in user WHEN splash started THEN before half a second no event is sent")
|
||||||
@Test
|
@Test
|
||||||
fun GIVEN_not_logged_in_user_WHEN_splash_started_THEN_before_half_a_second_no_event_is_sent() {
|
fun withoutEnoughTimeNoNavigationHappens() {
|
||||||
whenever(mockIsUserLoggedInUseCase.invoke()).doReturn(false)
|
whenever(mockIsUserLoggedInUseCase.invoke()).doReturn(false)
|
||||||
|
|
||||||
testCoroutineDispatcher.advanceTimeBy(100)
|
testCoroutineDispatcher.advanceTimeBy(100)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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
|
#### Still using mockito
|
||||||
|
|
||||||
To mock such behaviour with mockito is not as straight forward as creating our own.
|
To mock such behaviour with mockito with our current tool set 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.
|
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.
|
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.
|
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.
|
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
|
#### Back to the actual test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
In this testing instruction set you will learn how to write simple tests with retrofit.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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