Update core tests to the coroutines tests 1.6.0
This commit is contained in:
parent
b35467fcba
commit
3c80744f6d
13 changed files with 272 additions and 240 deletions
|
|
@ -133,12 +133,12 @@ verifyZeroInteractions(mockUserDataLocalStorage) // assert we didn't modify our
|
||||||
|
|
||||||
But something is wrong, the invoke method cannot be executed since it's a suspending function.
|
But something is wrong, the invoke method cannot be executed since it's a suspending function.
|
||||||
|
|
||||||
To test coroutines we will use `runBlockingTest`, this creates a blocking coroutine for us to test suspend functions, together it will look like:
|
To test coroutines we will use `runTest`, this creates a blocking coroutine for us to test suspend functions, together it will look like:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
@DisplayName("GIVEN empty username WHEN trying to login THEN invalid username is returned")
|
@DisplayName("GIVEN empty username WHEN trying to login THEN invalid username is returned")
|
||||||
@Test
|
@Test
|
||||||
fun emptyUserNameReturnsLoginStatusError() = runBlockingTest {
|
fun emptyUserNameReturnsLoginStatusError() = runTest {
|
||||||
val expected = Answer.Success(LoginStatus.INVALID_USERNAME)
|
val expected = Answer.Success(LoginStatus.INVALID_USERNAME)
|
||||||
|
|
||||||
val actual = sut.invoke(LoginCredentials("", "a"))
|
val actual = sut.invoke(LoginCredentials("", "a"))
|
||||||
|
|
@ -160,7 +160,7 @@ This is really similar, so try to write it on your own, but for progress the cod
|
||||||
```kotlin
|
```kotlin
|
||||||
@DisplayName("GIVEN empty password WHEN trying to login THEN invalid password is returned")
|
@DisplayName("GIVEN empty password WHEN trying to login THEN invalid password is returned")
|
||||||
@Test
|
@Test
|
||||||
fun emptyPasswordNameReturnsLoginStatusError() = runBlockingTest {
|
fun emptyPasswordNameReturnsLoginStatusError() = runTest {
|
||||||
val expected = Answer.Success(LoginStatus.INVALID_PASSWORD)
|
val expected = Answer.Success(LoginStatus.INVALID_PASSWORD)
|
||||||
|
|
||||||
val actual = sut.invoke(LoginCredentials("a", ""))
|
val actual = sut.invoke(LoginCredentials("a", ""))
|
||||||
|
|
@ -212,7 +212,7 @@ Together:
|
||||||
```kotlin
|
```kotlin
|
||||||
@DisplayName("GIVEN invalid credentials response WHEN trying to login THEN invalid credentials is returned")
|
@DisplayName("GIVEN invalid credentials response WHEN trying to login THEN invalid credentials is returned")
|
||||||
@Test
|
@Test
|
||||||
fun invalidLoginResponseReturnInvalidCredentials() = runBlockingTest {
|
fun invalidLoginResponseReturnInvalidCredentials() = runTest {
|
||||||
val expected = Answer.Success(LoginStatus.INVALID_CREDENTIALS)
|
val expected = Answer.Success(LoginStatus.INVALID_CREDENTIALS)
|
||||||
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b")))
|
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b")))
|
||||||
.doReturn(LoginStatusResponses.InvalidCredentials)
|
.doReturn(LoginStatusResponses.InvalidCredentials)
|
||||||
|
|
@ -240,7 +240,7 @@ The full code:
|
||||||
```kotlin
|
```kotlin
|
||||||
@DisplayName("GIVEN success response WHEN trying to login THEN session is saved and success is returned")
|
@DisplayName("GIVEN success response WHEN trying to login THEN session is saved and success is returned")
|
||||||
@Test
|
@Test
|
||||||
fun validResponseResultsInSavingSessionAndSuccessReturned() = runBlockingTest {
|
fun validResponseResultsInSavingSessionAndSuccessReturned() = runTest {
|
||||||
val expected = Answer.Success(LoginStatus.SUCCESS)
|
val expected = Answer.Success(LoginStatus.SUCCESS)
|
||||||
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b")))
|
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b")))
|
||||||
.doReturn(LoginStatusResponses.Success(Session("c", "d")))
|
.doReturn(LoginStatusResponses.Success(Session("c", "d")))
|
||||||
|
|
@ -297,7 +297,7 @@ together:
|
||||||
```kotlin
|
```kotlin
|
||||||
@DisplayName("GIVEN error resposne WHEN trying to login THEN session is not touched and error is returned")
|
@DisplayName("GIVEN error resposne WHEN trying to login THEN session is not touched and error is returned")
|
||||||
@Test
|
@Test
|
||||||
fun invalidResponseResultsInErrorReturned() = runBlockingTest {
|
fun invalidResponseResultsInErrorReturned() = runTest {
|
||||||
val exception = RuntimeException()
|
val exception = RuntimeException()
|
||||||
val expected = Answer.Error<LoginStatus>(UnexpectedException(exception))
|
val expected = Answer.Error<LoginStatus>(UnexpectedException(exception))
|
||||||
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b")))
|
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b")))
|
||||||
|
|
@ -500,34 +500,9 @@ suspendedRequest.complete(Unit)
|
||||||
|
|
||||||
### 6. `whenFetchingRequestIsCalledAgain`
|
### 6. `whenFetchingRequestIsCalledAgain`
|
||||||
|
|
||||||
We still didn't even touch the fetch method so let's test the that behaviour next:
|
We still didn't even touch the fetch method so let's test the behaviour next:
|
||||||
|
|
||||||
However the main issue here is, when to call fetch. If we call after `take()` we will never reach it, but if we call it before then it doesn't test the right behaviour.
|
We want to get the first result triggered by the subscription to the flow, and the again another loading and result after a call to `fetch`, so the setup would be:
|
||||||
We need to do it async, but async means it's not linear, thus our request could become shaky. For this we will use TestCoroutineDispatcher.
|
|
||||||
|
|
||||||
Let's add this to our setup:
|
|
||||||
```kotlin
|
|
||||||
private lateinit var sut: ContentRepository
|
|
||||||
private lateinit var mockContentRemoteSource: ContentRemoteSource
|
|
||||||
private lateinit var testDispatcher: TestCoroutineDispatcher
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setUp() {
|
|
||||||
testDispatcher = TestCoroutineDispatcher()
|
|
||||||
testDispatcher.pauseDispatcher() // we pause the dispatcher so we have full control over it
|
|
||||||
mockContentRemoteSource = mock()
|
|
||||||
sut = ContentRepository(mockContentRemoteSource)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Next we should use the same dispatcher in our test so:
|
|
||||||
```kotlin
|
|
||||||
fun whenFetchingRequestIsCalledAgain() = runBlockingTest(testDispatcher) {
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Okay with this we should write our setup:
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val exception = RuntimeException()
|
val exception = RuntimeException()
|
||||||
val expected = listOf(
|
val expected = listOf(
|
||||||
|
|
@ -542,19 +517,42 @@ whenever(mockContentRemoteSource.get()).doAnswer {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Our action will need to use async and advance to coroutines so we can are testing the correct behaviour:
|
However the main issue here is, when to call fetch. If we call after `take()` we will never reach it, but if we call it before then it doesn't test the right behaviour.
|
||||||
|
We need to do it async:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val actual = async(testDispatcher) { sut.contents.take(4).toList() }
|
val actual = async { sut.contents.take(4).toList() }
|
||||||
testDispatcher.advanceUntilIdle() // we ensure the async is progressing as much as it can (thus receiving the first to values)
|
|
||||||
sut.fetch()
|
sut.fetch()
|
||||||
testDispatcher.advanceUntilIdle() // ensure the async progresses further now, since we give it additional action to take.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Our verification as usual is really simple
|
And the verification as usual is really simple
|
||||||
```kotlin
|
```kotlin
|
||||||
Assertions.assertEquals(expected, actual.await())
|
Assertions.assertEquals(expected, actual.await())
|
||||||
```
|
```
|
||||||
|
|
||||||
|
However this test will hang. This is because `runTest` uses by default `StandardTestDispatcher` which doesn't enter child coroutines immediately and the async block will only be executed after the call to fetch.
|
||||||
|
This is a good thing because it gives us more control over the order of execution and as a result our test are not shaky.
|
||||||
|
To make sure that `fetch` is called only when `take` suspends, we can call `advanceUntilIdle` which will give the opportunity of the async block to execute.
|
||||||
|
So our test becomes:
|
||||||
|
```kotlin
|
||||||
|
val actual = async { sut.contents.take(4).toList() }
|
||||||
|
advanceUntilIdle()
|
||||||
|
sut.fetch()
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively we can make `runTest` use `UnconfinedTestDispatcher` which will enter child coroutines eagerly, so our async will be executed until it suspends and only after the main execution path will continue with the call to `fetch` and we don't need `advanceUntilIdle` anymore.
|
||||||
|
```kotlin
|
||||||
|
@Test
|
||||||
|
fun whenFetchingRequestIsCalledAgain() = runTest(UnconfinedTestDispatcher()) {
|
||||||
|
... // setup here
|
||||||
|
|
||||||
|
val actual = async { sut.contents.take(4).toList() }
|
||||||
|
sut.fetch()
|
||||||
|
|
||||||
|
Assertions.assertEquals(expected, actual.await())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Now we can test even complicated interactions between methods and classes with TestCoroutineDispatcher.
|
Now we can test even complicated interactions between methods and classes with TestCoroutineDispatcher.
|
||||||
|
|
||||||
### 7. `noAdditionalItemsEmitted`
|
### 7. `noAdditionalItemsEmitted`
|
||||||
|
|
@ -564,6 +562,7 @@ So we also need to test that this assumption is correct.
|
||||||
|
|
||||||
I think the best place to start from is our most complicated test `whenFetchingRequestIsCalledAgain` since this is the one most likely add additional unexpected values.
|
I think the best place to start from is our most complicated test `whenFetchingRequestIsCalledAgain` since this is the one most likely add additional unexpected values.
|
||||||
|
|
||||||
|
# TODO this doesn't apply to `runTest`
|
||||||
Luckily `runBlockingTest` is helpful here: if a coroutine didn't finish properly it will throw an IllegalStateException.
|
Luckily `runBlockingTest` is helpful here: if a coroutine didn't finish properly it will throw an IllegalStateException.
|
||||||
|
|
||||||
So all we need to do is to request more than elements it should send out and expect an IllegalStateException from runBlocking.
|
So all we need to do is to request more than elements it should send out and expect an IllegalStateException from runBlocking.
|
||||||
|
|
@ -594,6 +593,40 @@ Assertions.assertThrows(IllegalStateException::class.java) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 8. `noAdditionalItemsEmittedWithTurbine`
|
||||||
|
|
||||||
|
Turbine is library that provides some testing utilities for Flow.
|
||||||
|
The entrypoint is the `test` extension which collects the flow and gives you the opportunity to
|
||||||
|
assert the collected events.
|
||||||
|
|
||||||
|
To receive a new item from the flow we call `awaitItem()`, and to verify that no more items are
|
||||||
|
emitted we expect the result of `cancelAndConsumeRemainingEvents()` to be an empty list.
|
||||||
|
|
||||||
|
Keeping the same setup as before we can use turbine to test `contents` as follows:
|
||||||
|
```kotlin
|
||||||
|
sut.contents.test {
|
||||||
|
Assertions.assertEquals(expected[0], awaitItem())
|
||||||
|
Assertions.assertEquals(expected[1], awaitItem())
|
||||||
|
sut.fetch()
|
||||||
|
Assertions.assertEquals(expected[2], awaitItem())
|
||||||
|
Assertions.assertEquals(expected[3], awaitItem())
|
||||||
|
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We can reorganize a bit the code. We can move the `fetch` before the first `awaitItem`,
|
||||||
|
because `test` will immediately collect and buffer the first Loading and Success, so we can
|
||||||
|
assert the items in a for loop like this:
|
||||||
|
```kotlin
|
||||||
|
sut.contents.test {
|
||||||
|
sut.fetch()
|
||||||
|
expected.forEach { expectedItem ->
|
||||||
|
Assertions.assertEquals(expectedItem, awaitItem())
|
||||||
|
}
|
||||||
|
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Conclusion
|
## Conclusion
|
||||||
|
|
||||||
Here we went over most common cases when you need to test simple java / kotlin files with no reference to networking or android:
|
Here we went over most common cases when you need to test simple java / kotlin files with no reference to networking or android:
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ java {
|
||||||
|
|
||||||
compileKotlin {
|
compileKotlin {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
|
freeCompilerArgs += ['-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,4 +38,5 @@ dependencies {
|
||||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version"
|
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version"
|
||||||
kaptTest "com.google.dagger:dagger-compiler:$hilt_version"
|
kaptTest "com.google.dagger:dagger-compiler:$hilt_version"
|
||||||
testImplementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
testImplementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||||
|
testImplementation "app.cash.turbine:turbine:$turbine_version"
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
package org.fnives.test.showcase.core.content
|
package org.fnives.test.showcase.core.content
|
||||||
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.test.runBlockingTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
|
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
|
||||||
import org.fnives.test.showcase.model.content.ContentId
|
import org.fnives.test.showcase.model.content.ContentId
|
||||||
|
import org.junit.jupiter.api.Assertions
|
||||||
import org.junit.jupiter.api.Assertions.assertThrows
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
|
@ -36,7 +37,7 @@ internal class AddContentToFavouriteUseCaseTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN contentId WHEN called THEN storage is called")
|
@DisplayName("GIVEN contentId WHEN called THEN storage is called")
|
||||||
@Test
|
@Test
|
||||||
fun contentIdIsDelegatedToStorage() = runBlockingTest {
|
fun contentIdIsDelegatedToStorage() = runTest {
|
||||||
sut.invoke(ContentId("a"))
|
sut.invoke(ContentId("a"))
|
||||||
|
|
||||||
verify(mockFavouriteContentLocalStorage, times(1)).markAsFavourite(ContentId("a"))
|
verify(mockFavouriteContentLocalStorage, times(1)).markAsFavourite(ContentId("a"))
|
||||||
|
|
@ -45,7 +46,7 @@ internal class AddContentToFavouriteUseCaseTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN throwing local storage WHEN thrown THEN its propagated")
|
@DisplayName("GIVEN throwing local storage WHEN thrown THEN its propagated")
|
||||||
@Test
|
@Test
|
||||||
fun storageThrowingIsPropagated() = runBlockingTest {
|
fun storageThrowingIsPropagated() = runTest {
|
||||||
whenever(mockFavouriteContentLocalStorage.markAsFavourite(ContentId("a"))).doThrow(
|
whenever(mockFavouriteContentLocalStorage.markAsFavourite(ContentId("a"))).doThrow(
|
||||||
RuntimeException()
|
RuntimeException()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package org.fnives.test.showcase.core.content
|
package org.fnives.test.showcase.core.content
|
||||||
|
|
||||||
import kotlinx.coroutines.test.runBlockingTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Disabled
|
import org.junit.jupiter.api.Disabled
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
|
@ -20,27 +20,27 @@ class CodeKataContentRepositoryTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN content response WHEN content observed THEN loading AND data is returned")
|
@DisplayName("GIVEN content response WHEN content observed THEN loading AND data is returned")
|
||||||
@Test
|
@Test
|
||||||
fun happyFlow() = runBlockingTest {
|
fun happyFlow() = runTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@DisplayName("GIVEN content error WHEN content observed THEN loading AND data is returned")
|
@DisplayName("GIVEN content error WHEN content observed THEN loading AND data is returned")
|
||||||
@Test
|
@Test
|
||||||
fun errorFlow() = runBlockingTest {
|
fun errorFlow() = runTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@DisplayName("GIVEN saved cache WHEN collected THEN cache is returned")
|
@DisplayName("GIVEN saved cache WHEN collected THEN cache is returned")
|
||||||
@Test
|
@Test
|
||||||
fun verifyCaching() = runBlockingTest {
|
fun verifyCaching() = runTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@DisplayName("GIVEN no response from remote source WHEN content observed THEN loading is returned")
|
@DisplayName("GIVEN no response from remote source WHEN content observed THEN loading is returned")
|
||||||
@Test
|
@Test
|
||||||
fun loadingIsShownBeforeTheRequestIsReturned() = runBlockingTest {
|
fun loadingIsShownBeforeTheRequestIsReturned() = runTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@DisplayName("GIVEN content response THEN error WHEN fetched THEN returned states are loading data loading error")
|
@DisplayName("GIVEN content response THEN error WHEN fetched THEN returned states are loading data loading error")
|
||||||
@Test
|
@Test
|
||||||
fun whenFetchingRequestIsCalledAgain() = runBlockingTest {
|
fun whenFetchingRequestIsCalledAgain() = runTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@DisplayName("GIVEN content response THEN error WHEN fetched THEN only 4 items are emitted")
|
@DisplayName("GIVEN content response THEN error WHEN fetched THEN only 4 items are emitted")
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
package org.fnives.test.showcase.core.content
|
package org.fnives.test.showcase.core.content
|
||||||
|
|
||||||
|
import app.cash.turbine.test
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.take
|
import kotlinx.coroutines.flow.take
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
import kotlinx.coroutines.test.runBlockingTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.fnives.test.showcase.core.shared.UnexpectedException
|
import org.fnives.test.showcase.core.shared.UnexpectedException
|
||||||
import org.fnives.test.showcase.model.content.Content
|
import org.fnives.test.showcase.model.content.Content
|
||||||
import org.fnives.test.showcase.model.content.ContentId
|
import org.fnives.test.showcase.model.content.ContentId
|
||||||
|
|
@ -31,12 +32,9 @@ internal class ContentRepositoryTest {
|
||||||
|
|
||||||
private lateinit var sut: ContentRepository
|
private lateinit var sut: ContentRepository
|
||||||
private lateinit var mockContentRemoteSource: ContentRemoteSource
|
private lateinit var mockContentRemoteSource: ContentRemoteSource
|
||||||
private lateinit var testDispatcher: TestCoroutineDispatcher
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
testDispatcher = TestCoroutineDispatcher()
|
|
||||||
testDispatcher.pauseDispatcher()
|
|
||||||
mockContentRemoteSource = mock()
|
mockContentRemoteSource = mock()
|
||||||
sut = ContentRepository(mockContentRemoteSource)
|
sut = ContentRepository(mockContentRemoteSource)
|
||||||
}
|
}
|
||||||
|
|
@ -49,20 +47,13 @@ internal class ContentRepositoryTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN content response WHEN content observed THEN loading AND data is returned")
|
@DisplayName("GIVEN content response WHEN content observed THEN loading AND data is returned")
|
||||||
@Test
|
@Test
|
||||||
fun happyFlow() = runBlockingTest {
|
fun happyFlow() = runTest {
|
||||||
val expected = listOf(
|
val expected = listOf(
|
||||||
Resource.Loading(),
|
Resource.Loading(),
|
||||||
Resource.Success(listOf(Content(ContentId("a"), "", "", ImageUrl(""))))
|
Resource.Success(listOf(Content(ContentId("a"), "", "", ImageUrl(""))))
|
||||||
)
|
)
|
||||||
whenever(mockContentRemoteSource.get()).doReturn(
|
whenever(mockContentRemoteSource.get()).doReturn(
|
||||||
listOf(
|
listOf(Content(ContentId("a"), "", "", ImageUrl("")))
|
||||||
Content(
|
|
||||||
ContentId("a"),
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
ImageUrl("")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val actual = sut.contents.take(2).toList()
|
val actual = sut.contents.take(2).toList()
|
||||||
|
|
@ -72,7 +63,7 @@ internal class ContentRepositoryTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN content error WHEN content observed THEN loading AND data is returned")
|
@DisplayName("GIVEN content error WHEN content observed THEN loading AND data is returned")
|
||||||
@Test
|
@Test
|
||||||
fun errorFlow() = runBlockingTest {
|
fun errorFlow() = runTest {
|
||||||
val exception = RuntimeException()
|
val exception = RuntimeException()
|
||||||
val expected = listOf(
|
val expected = listOf(
|
||||||
Resource.Loading(),
|
Resource.Loading(),
|
||||||
|
|
@ -87,7 +78,7 @@ internal class ContentRepositoryTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN saved cache WHEN collected THEN cache is returned")
|
@DisplayName("GIVEN saved cache WHEN collected THEN cache is returned")
|
||||||
@Test
|
@Test
|
||||||
fun verifyCaching() = runBlockingTest {
|
fun verifyCaching() = runTest {
|
||||||
val content = Content(ContentId("1"), "", "", ImageUrl(""))
|
val content = Content(ContentId("1"), "", "", ImageUrl(""))
|
||||||
val expected = listOf(Resource.Success(listOf(content)))
|
val expected = listOf(Resource.Success(listOf(content)))
|
||||||
whenever(mockContentRemoteSource.get()).doReturn(listOf(content))
|
whenever(mockContentRemoteSource.get()).doReturn(listOf(content))
|
||||||
|
|
@ -101,7 +92,7 @@ internal class ContentRepositoryTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN no response from remote source WHEN content observed THEN loading is returned")
|
@DisplayName("GIVEN no response from remote source WHEN content observed THEN loading is returned")
|
||||||
@Test
|
@Test
|
||||||
fun loadingIsShownBeforeTheRequestIsReturned() = runBlockingTest {
|
fun loadingIsShownBeforeTheRequestIsReturned() = runTest {
|
||||||
val expected = Resource.Loading<List<Content>>()
|
val expected = Resource.Loading<List<Content>>()
|
||||||
val suspendedRequest = CompletableDeferred<Unit>()
|
val suspendedRequest = CompletableDeferred<Unit>()
|
||||||
whenever(mockContentRemoteSource.get()).doSuspendableAnswer {
|
whenever(mockContentRemoteSource.get()).doSuspendableAnswer {
|
||||||
|
|
@ -117,33 +108,32 @@ internal class ContentRepositoryTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN content response THEN error WHEN fetched THEN returned states are loading data loading error")
|
@DisplayName("GIVEN content response THEN error WHEN fetched THEN returned states are loading data loading error")
|
||||||
@Test
|
@Test
|
||||||
fun whenFetchingRequestIsCalledAgain() =
|
fun whenFetchingRequestIsCalledAgain() = runTest(UnconfinedTestDispatcher()) {
|
||||||
runBlockingTest(testDispatcher) {
|
val exception = RuntimeException()
|
||||||
val exception = RuntimeException()
|
val expected = listOf(
|
||||||
val expected = listOf(
|
Resource.Loading(),
|
||||||
Resource.Loading(),
|
Resource.Success(emptyList()),
|
||||||
Resource.Success(emptyList()),
|
Resource.Loading(),
|
||||||
Resource.Loading(),
|
Resource.Error<List<Content>>(UnexpectedException(exception))
|
||||||
Resource.Error<List<Content>>(UnexpectedException(exception))
|
)
|
||||||
)
|
var first = true
|
||||||
var first = true
|
whenever(mockContentRemoteSource.get()).doAnswer {
|
||||||
whenever(mockContentRemoteSource.get()).doAnswer {
|
if (first) emptyList<Content>().also { first = false } else throw exception
|
||||||
if (first) emptyList<Content>().also { first = false } else throw exception
|
|
||||||
}
|
|
||||||
|
|
||||||
val actual = async(testDispatcher) { sut.contents.take(4).toList() }
|
|
||||||
testDispatcher.advanceUntilIdle()
|
|
||||||
sut.fetch()
|
|
||||||
testDispatcher.advanceUntilIdle()
|
|
||||||
|
|
||||||
Assertions.assertEquals(expected, actual.await())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val actual = async {
|
||||||
|
sut.contents.take(4).toList()
|
||||||
|
}
|
||||||
|
sut.fetch()
|
||||||
|
|
||||||
|
Assertions.assertEquals(expected, actual.await())
|
||||||
|
}
|
||||||
|
|
||||||
@DisplayName("GIVEN content response THEN error WHEN fetched THEN only 4 items are emitted")
|
@DisplayName("GIVEN content response THEN error WHEN fetched THEN only 4 items are emitted")
|
||||||
@Test
|
@Test
|
||||||
fun noAdditionalItemsEmitted() {
|
fun noAdditionalItemsEmitted() {
|
||||||
Assertions.assertThrows(IllegalStateException::class.java) {
|
Assertions.assertThrows(IllegalStateException::class.java) {
|
||||||
runBlockingTest(testDispatcher) {
|
runTest(UnconfinedTestDispatcher()) {
|
||||||
val exception = RuntimeException()
|
val exception = RuntimeException()
|
||||||
val expected = listOf(
|
val expected = listOf(
|
||||||
Resource.Loading(),
|
Resource.Loading(),
|
||||||
|
|
@ -156,13 +146,37 @@ internal class ContentRepositoryTest {
|
||||||
if (first) emptyList<Content>().also { first = false } else throw exception
|
if (first) emptyList<Content>().also { first = false } else throw exception
|
||||||
}
|
}
|
||||||
|
|
||||||
val actual = async(testDispatcher) { sut.contents.take(5).toList() }
|
val actual = async {
|
||||||
testDispatcher.advanceUntilIdle()
|
sut.contents.take(5).toList()
|
||||||
|
}
|
||||||
sut.fetch()
|
sut.fetch()
|
||||||
testDispatcher.advanceUntilIdle()
|
|
||||||
|
|
||||||
Assertions.assertEquals(expected, actual.await())
|
Assertions.assertEquals(expected, actual.await())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DisplayName("GIVEN content response THEN error WHEN fetched THEN only 4 items are emitted")
|
||||||
|
@Test
|
||||||
|
fun noAdditionalItemsEmittedWithTurbine() = runTest {
|
||||||
|
val exception = RuntimeException()
|
||||||
|
val expected = listOf(
|
||||||
|
Resource.Loading(),
|
||||||
|
Resource.Success(emptyList()),
|
||||||
|
Resource.Loading(),
|
||||||
|
Resource.Error<List<Content>>(UnexpectedException(exception))
|
||||||
|
)
|
||||||
|
var first = true
|
||||||
|
whenever(mockContentRemoteSource.get()).doAnswer {
|
||||||
|
if (first) emptyList<Content>().also { first = false } else throw exception
|
||||||
|
}
|
||||||
|
|
||||||
|
sut.contents.test {
|
||||||
|
sut.fetch()
|
||||||
|
expected.forEach { expectedItem ->
|
||||||
|
Assertions.assertEquals(expectedItem, awaitItem())
|
||||||
|
}
|
||||||
|
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package org.fnives.test.showcase.core.content
|
package org.fnives.test.showcase.core.content
|
||||||
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.test.runBlockingTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.Assertions.assertThrows
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
|
@ -34,7 +34,7 @@ internal class FetchContentUseCaseTest {
|
||||||
|
|
||||||
@DisplayName("WHEN called THEN repository is called")
|
@DisplayName("WHEN called THEN repository is called")
|
||||||
@Test
|
@Test
|
||||||
fun whenCalledRepositoryIsFetched() = runBlockingTest {
|
fun whenCalledRepositoryIsFetched() = runTest {
|
||||||
sut.invoke()
|
sut.invoke()
|
||||||
|
|
||||||
verify(mockContentRepository, times(1)).fetch()
|
verify(mockContentRepository, times(1)).fetch()
|
||||||
|
|
@ -43,7 +43,7 @@ internal class FetchContentUseCaseTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN throwing local storage WHEN thrown THEN its thrown")
|
@DisplayName("GIVEN throwing local storage WHEN thrown THEN its thrown")
|
||||||
@Test
|
@Test
|
||||||
fun whenRepositoryThrowsUseCaseAlsoThrows() = runBlockingTest {
|
fun whenRepositoryThrowsUseCaseAlsoThrows() = runTest {
|
||||||
whenever(mockContentRepository.fetch()).doThrow(RuntimeException())
|
whenever(mockContentRepository.fetch()).doThrow(RuntimeException())
|
||||||
|
|
||||||
assertThrows(RuntimeException::class.java) {
|
assertThrows(RuntimeException::class.java) {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
package org.fnives.test.showcase.core.content
|
package org.fnives.test.showcase.core.content
|
||||||
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import app.cash.turbine.test
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.take
|
import kotlinx.coroutines.flow.take
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.test.runBlockingTest
|
import kotlinx.coroutines.test.runBlockingTest
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
|
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
|
||||||
import org.fnives.test.showcase.model.content.Content
|
import org.fnives.test.showcase.model.content.Content
|
||||||
import org.fnives.test.showcase.model.content.ContentId
|
import org.fnives.test.showcase.model.content.ContentId
|
||||||
|
|
@ -22,7 +22,6 @@ import org.mockito.kotlin.mock
|
||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
|
|
||||||
@Suppress("TestFunctionName")
|
@Suppress("TestFunctionName")
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
internal class GetAllContentUseCaseTest {
|
internal class GetAllContentUseCaseTest {
|
||||||
|
|
||||||
private lateinit var sut: GetAllContentUseCase
|
private lateinit var sut: GetAllContentUseCase
|
||||||
|
|
@ -30,11 +29,9 @@ internal class GetAllContentUseCaseTest {
|
||||||
private lateinit var mockFavouriteContentLocalStorage: FavouriteContentLocalStorage
|
private lateinit var mockFavouriteContentLocalStorage: FavouriteContentLocalStorage
|
||||||
private lateinit var contentFlow: MutableStateFlow<Resource<List<Content>>>
|
private lateinit var contentFlow: MutableStateFlow<Resource<List<Content>>>
|
||||||
private lateinit var favouriteContentIdFlow: MutableStateFlow<List<ContentId>>
|
private lateinit var favouriteContentIdFlow: MutableStateFlow<List<ContentId>>
|
||||||
private lateinit var testDispatcher: TestCoroutineDispatcher
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
testDispatcher = TestCoroutineDispatcher()
|
|
||||||
mockFavouriteContentLocalStorage = mock()
|
mockFavouriteContentLocalStorage = mock()
|
||||||
mockContentRepository = mock()
|
mockContentRepository = mock()
|
||||||
favouriteContentIdFlow = MutableStateFlow(emptyList())
|
favouriteContentIdFlow = MutableStateFlow(emptyList())
|
||||||
|
|
@ -48,187 +45,168 @@ internal class GetAllContentUseCaseTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN loading AND empty favourite WHEN observed THEN loading is shown")
|
@DisplayName("GIVEN loading AND empty favourite WHEN observed THEN loading is shown")
|
||||||
@Test
|
@Test
|
||||||
fun loadingResourceWithNoFavouritesResultsInLoadingResource() =
|
fun loadingResourceWithNoFavouritesResultsInLoadingResource() = runTest {
|
||||||
runBlockingTest(testDispatcher) {
|
favouriteContentIdFlow.value = emptyList()
|
||||||
favouriteContentIdFlow.value = emptyList()
|
contentFlow.value = Resource.Loading()
|
||||||
contentFlow.value = Resource.Loading()
|
val expected = Resource.Loading<List<FavouriteContent>>()
|
||||||
val expected = Resource.Loading<List<FavouriteContent>>()
|
|
||||||
|
|
||||||
val actual = sut.get().take(1).toList()
|
val actual = sut.get().take(1).toList()
|
||||||
|
|
||||||
Assertions.assertEquals(listOf(expected), actual)
|
Assertions.assertEquals(listOf(expected), actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DisplayName("GIVEN loading AND listOfFavourite WHEN observed THEN loading is shown")
|
@DisplayName("GIVEN loading AND listOfFavourite WHEN observed THEN loading is shown")
|
||||||
@Test
|
@Test
|
||||||
fun loadingResourceWithFavouritesResultsInLoadingResource() =
|
fun loadingResourceWithFavouritesResultsInLoadingResource() = runTest {
|
||||||
runBlockingTest(testDispatcher) {
|
favouriteContentIdFlow.value = listOf(ContentId("a"))
|
||||||
favouriteContentIdFlow.value = listOf(ContentId("a"))
|
contentFlow.value = Resource.Loading()
|
||||||
contentFlow.value = Resource.Loading()
|
val expected = Resource.Loading<List<FavouriteContent>>()
|
||||||
val expected = Resource.Loading<List<FavouriteContent>>()
|
|
||||||
|
|
||||||
val actual = sut.get().take(1).toList()
|
val actual = sut.get().take(1).toList()
|
||||||
|
|
||||||
Assertions.assertEquals(listOf(expected), actual)
|
Assertions.assertEquals(listOf(expected), actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DisplayName("GIVEN error AND empty favourite WHEN observed THEN error is shown")
|
@DisplayName("GIVEN error AND empty favourite WHEN observed THEN error is shown")
|
||||||
@Test
|
@Test
|
||||||
fun errorResourceWithNoFavouritesResultsInErrorResource() =
|
fun errorResourceWithNoFavouritesResultsInErrorResource() = runTest {
|
||||||
runBlockingTest(testDispatcher) {
|
favouriteContentIdFlow.value = emptyList()
|
||||||
favouriteContentIdFlow.value = emptyList()
|
val exception = Throwable()
|
||||||
val exception = Throwable()
|
contentFlow.value = Resource.Error(exception)
|
||||||
contentFlow.value = Resource.Error(exception)
|
val expected = Resource.Error<List<FavouriteContent>>(exception)
|
||||||
val expected = Resource.Error<List<FavouriteContent>>(exception)
|
|
||||||
|
|
||||||
val actual = sut.get().take(1).toList()
|
val actual = sut.get().take(1).toList()
|
||||||
|
|
||||||
Assertions.assertEquals(listOf(expected), actual)
|
Assertions.assertEquals(listOf(expected), actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DisplayName("GIVEN error AND listOfFavourite WHEN observed THEN error is shown")
|
@DisplayName("GIVEN error AND listOfFavourite WHEN observed THEN error is shown")
|
||||||
@Test
|
@Test
|
||||||
fun errorResourceWithFavouritesResultsInErrorResource() =
|
fun errorResourceWithFavouritesResultsInErrorResource() = runTest {
|
||||||
runBlockingTest(testDispatcher) {
|
favouriteContentIdFlow.value = listOf(ContentId("b"))
|
||||||
favouriteContentIdFlow.value = listOf(ContentId("b"))
|
val exception = Throwable()
|
||||||
val exception = Throwable()
|
contentFlow.value = Resource.Error(exception)
|
||||||
contentFlow.value = Resource.Error(exception)
|
val expected = Resource.Error<List<FavouriteContent>>(exception)
|
||||||
val expected = Resource.Error<List<FavouriteContent>>(exception)
|
|
||||||
|
|
||||||
val actual = sut.get().take(1).toList()
|
val actual = sut.get().take(1).toList()
|
||||||
|
|
||||||
Assertions.assertEquals(listOf(expected), actual)
|
Assertions.assertEquals(listOf(expected), actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DisplayName("GIVEN listOfContent AND empty favourite WHEN observed THEN favourites are returned")
|
@DisplayName("GIVEN listOfContent AND empty favourite WHEN observed THEN favourites are returned")
|
||||||
@Test
|
@Test
|
||||||
fun successResourceWithNoFavouritesResultsInNoFavouritedItems() =
|
fun successResourceWithNoFavouritesResultsInNoFavouritedItems() = runTest {
|
||||||
runBlockingTest(testDispatcher) {
|
favouriteContentIdFlow.value = emptyList()
|
||||||
favouriteContentIdFlow.value = emptyList()
|
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
|
||||||
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
|
contentFlow.value = Resource.Success(listOf(content))
|
||||||
contentFlow.value = Resource.Success(listOf(content))
|
val items = listOf(
|
||||||
val items = listOf(
|
FavouriteContent(content, false)
|
||||||
FavouriteContent(content, false)
|
)
|
||||||
)
|
val expected = Resource.Success(items)
|
||||||
val expected = Resource.Success(items)
|
|
||||||
|
|
||||||
val actual = sut.get().take(1).toList()
|
val actual = sut.get().take(1).toList()
|
||||||
|
|
||||||
Assertions.assertEquals(listOf(expected), actual)
|
Assertions.assertEquals(listOf(expected), actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DisplayName("GIVEN listOfContent AND other favourite id WHEN observed THEN favourites are returned")
|
@DisplayName("GIVEN listOfContent AND other favourite id WHEN observed THEN favourites are returned")
|
||||||
@Test
|
@Test
|
||||||
fun successResourceWithDifferentFavouritesResultsInNoFavouritedItems() =
|
fun successResourceWithDifferentFavouritesResultsInNoFavouritedItems() = runTest {
|
||||||
runBlockingTest(testDispatcher) {
|
favouriteContentIdFlow.value = listOf(ContentId("x"))
|
||||||
favouriteContentIdFlow.value = listOf(ContentId("x"))
|
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
|
||||||
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
|
contentFlow.value = Resource.Success(listOf(content))
|
||||||
contentFlow.value = Resource.Success(listOf(content))
|
val items = listOf(
|
||||||
val items = listOf(
|
FavouriteContent(content, false)
|
||||||
FavouriteContent(content, false)
|
)
|
||||||
)
|
val expected = Resource.Success(items)
|
||||||
val expected = Resource.Success(items)
|
|
||||||
|
|
||||||
val actual = sut.get().take(1).toList()
|
val actual = sut.get().take(1).toList()
|
||||||
|
|
||||||
Assertions.assertEquals(listOf(expected), actual)
|
Assertions.assertEquals(listOf(expected), actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DisplayName("GIVEN listOfContent AND same favourite id WHEN observed THEN favourites are returned")
|
@DisplayName("GIVEN listOfContent AND same favourite id WHEN observed THEN favourites are returned")
|
||||||
@Test
|
@Test
|
||||||
fun successResourceWithSameFavouritesResultsInFavouritedItems() =
|
fun successResourceWithSameFavouritesResultsInFavouritedItems() = runTest {
|
||||||
runBlockingTest(testDispatcher) {
|
favouriteContentIdFlow.value = listOf(ContentId("a"))
|
||||||
favouriteContentIdFlow.value = listOf(ContentId("a"))
|
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
|
||||||
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
|
contentFlow.value = Resource.Success(listOf(content))
|
||||||
contentFlow.value = Resource.Success(listOf(content))
|
val items = listOf(
|
||||||
val items = listOf(
|
FavouriteContent(content, true)
|
||||||
FavouriteContent(content, true)
|
)
|
||||||
)
|
val expected = Resource.Success(items)
|
||||||
val expected = Resource.Success(items)
|
|
||||||
|
|
||||||
val actual = sut.get().take(1).toList()
|
val actual = sut.get().take(1).toList()
|
||||||
|
|
||||||
Assertions.assertEquals(listOf(expected), actual)
|
Assertions.assertEquals(listOf(expected), actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DisplayName("GIVEN loading then data then added favourite WHEN observed THEN loading then correct favourites are returned")
|
@DisplayName("GIVEN loading then data then added favourite WHEN observed THEN loading then correct favourites are returned")
|
||||||
@Test
|
@Test
|
||||||
fun whileLoadingAndAddingItemsReactsProperly() =
|
fun whileLoadingAndAddingItemsReactsProperly() = runTest {
|
||||||
runBlockingTest(testDispatcher) {
|
favouriteContentIdFlow.value = emptyList()
|
||||||
favouriteContentIdFlow.value = emptyList()
|
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
|
||||||
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
|
contentFlow.value = Resource.Loading()
|
||||||
contentFlow.value = Resource.Loading()
|
val expected = listOf(
|
||||||
val expected = listOf(
|
Resource.Loading(),
|
||||||
Resource.Loading(),
|
Resource.Success(listOf(FavouriteContent(content, false))),
|
||||||
Resource.Success(listOf(FavouriteContent(content, false))),
|
Resource.Success(listOf(FavouriteContent(content, true)))
|
||||||
Resource.Success(listOf(FavouriteContent(content, true)))
|
)
|
||||||
)
|
|
||||||
|
|
||||||
val actual = async(testDispatcher) {
|
|
||||||
sut.get().take(3).toList()
|
|
||||||
}
|
|
||||||
testDispatcher.advanceUntilIdle()
|
|
||||||
|
|
||||||
|
sut.get().test {
|
||||||
contentFlow.value = Resource.Success(listOf(content))
|
contentFlow.value = Resource.Success(listOf(content))
|
||||||
testDispatcher.advanceUntilIdle()
|
|
||||||
|
|
||||||
favouriteContentIdFlow.value = listOf(ContentId("a"))
|
favouriteContentIdFlow.value = listOf(ContentId("a"))
|
||||||
testDispatcher.advanceUntilIdle()
|
|
||||||
|
|
||||||
Assertions.assertEquals(expected, actual.await())
|
expected.forEach { expectedItem ->
|
||||||
|
Assertions.assertEquals(expectedItem, awaitItem())
|
||||||
|
}
|
||||||
|
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@DisplayName("GIVEN loading then data then removed favourite WHEN observed THEN loading then correct favourites are returned")
|
@DisplayName("GIVEN loading then data then removed favourite WHEN observed THEN loading then correct favourites are returned")
|
||||||
@Test
|
@Test
|
||||||
fun whileLoadingAndRemovingItemsReactsProperly() =
|
fun whileLoadingAndRemovingItemsReactsProperly() = runTest {
|
||||||
runBlockingTest(testDispatcher) {
|
favouriteContentIdFlow.value = listOf(ContentId("a"))
|
||||||
favouriteContentIdFlow.value = listOf(ContentId("a"))
|
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
|
||||||
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
|
contentFlow.value = Resource.Loading()
|
||||||
contentFlow.value = Resource.Loading()
|
val expected = listOf(
|
||||||
val expected = listOf(
|
Resource.Loading(),
|
||||||
Resource.Loading(),
|
Resource.Success(listOf(FavouriteContent(content, true))),
|
||||||
Resource.Success(listOf(FavouriteContent(content, true))),
|
Resource.Success(listOf(FavouriteContent(content, false)))
|
||||||
Resource.Success(listOf(FavouriteContent(content, false)))
|
)
|
||||||
)
|
|
||||||
|
|
||||||
val actual = async(testDispatcher) {
|
|
||||||
sut.get().take(3).toList()
|
|
||||||
}
|
|
||||||
testDispatcher.advanceUntilIdle()
|
|
||||||
|
|
||||||
|
sut.get().test {
|
||||||
contentFlow.value = Resource.Success(listOf(content))
|
contentFlow.value = Resource.Success(listOf(content))
|
||||||
testDispatcher.advanceUntilIdle()
|
|
||||||
|
|
||||||
favouriteContentIdFlow.value = emptyList()
|
favouriteContentIdFlow.value = emptyList()
|
||||||
testDispatcher.advanceUntilIdle()
|
|
||||||
|
|
||||||
Assertions.assertEquals(expected, actual.await())
|
expected.forEach { expectedItem ->
|
||||||
|
Assertions.assertEquals(expectedItem, awaitItem())
|
||||||
|
}
|
||||||
|
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@DisplayName("GIVEN loading then data then loading WHEN observed THEN loading then correct favourites then loading are returned")
|
@DisplayName("GIVEN loading then data then loading WHEN observed THEN loading then correct favourites then loading are returned")
|
||||||
@Test
|
@Test
|
||||||
fun loadingThenDataThenLoadingReactsProperly() =
|
fun loadingThenDataThenLoadingReactsProperly() = runTest {
|
||||||
runBlockingTest(testDispatcher) {
|
favouriteContentIdFlow.value = listOf(ContentId("a"))
|
||||||
favouriteContentIdFlow.value = listOf(ContentId("a"))
|
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
|
||||||
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
|
contentFlow.value = Resource.Loading()
|
||||||
contentFlow.value = Resource.Loading()
|
val expected = listOf(
|
||||||
val expected = listOf(
|
Resource.Loading(),
|
||||||
Resource.Loading(),
|
Resource.Success(listOf(FavouriteContent(content, true))),
|
||||||
Resource.Success(listOf(FavouriteContent(content, true))),
|
Resource.Loading()
|
||||||
Resource.Loading()
|
)
|
||||||
)
|
|
||||||
|
|
||||||
val actual = async(testDispatcher) {
|
|
||||||
sut.get().take(3).toList()
|
|
||||||
}
|
|
||||||
testDispatcher.advanceUntilIdle()
|
|
||||||
|
|
||||||
|
sut.get().test {
|
||||||
contentFlow.value = Resource.Success(listOf(content))
|
contentFlow.value = Resource.Success(listOf(content))
|
||||||
testDispatcher.advanceUntilIdle()
|
|
||||||
|
|
||||||
contentFlow.value = Resource.Loading()
|
contentFlow.value = Resource.Loading()
|
||||||
testDispatcher.advanceUntilIdle()
|
|
||||||
|
|
||||||
Assertions.assertEquals(expected, actual.await())
|
expected.forEach { expectedItem ->
|
||||||
|
Assertions.assertEquals(expectedItem, awaitItem())
|
||||||
|
}
|
||||||
|
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package org.fnives.test.showcase.core.content
|
||||||
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.test.runBlockingTest
|
import kotlinx.coroutines.test.runBlockingTest
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
|
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
|
||||||
import org.fnives.test.showcase.model.content.ContentId
|
import org.fnives.test.showcase.model.content.ContentId
|
||||||
import org.junit.jupiter.api.Assertions
|
import org.junit.jupiter.api.Assertions
|
||||||
|
|
@ -36,7 +37,7 @@ internal class RemoveContentFromFavouritesUseCaseTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN contentId WHEN called THEN storage is called")
|
@DisplayName("GIVEN contentId WHEN called THEN storage is called")
|
||||||
@Test
|
@Test
|
||||||
fun givenContentIdCallsStorage() = runBlockingTest {
|
fun givenContentIdCallsStorage() = runTest {
|
||||||
sut.invoke(ContentId("a"))
|
sut.invoke(ContentId("a"))
|
||||||
|
|
||||||
verify(mockFavouriteContentLocalStorage, times(1)).deleteAsFavourite(ContentId("a"))
|
verify(mockFavouriteContentLocalStorage, times(1)).deleteAsFavourite(ContentId("a"))
|
||||||
|
|
@ -45,7 +46,7 @@ internal class RemoveContentFromFavouritesUseCaseTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN throwing local storage WHEN thrown THEN its propogated")
|
@DisplayName("GIVEN throwing local storage WHEN thrown THEN its propogated")
|
||||||
@Test
|
@Test
|
||||||
fun storageExceptionThrowingIsPropogated() = runBlockingTest {
|
fun storageExceptionThrowingIsPropogated() = runTest {
|
||||||
whenever(mockFavouriteContentLocalStorage.deleteAsFavourite(ContentId("a"))).doThrow(RuntimeException())
|
whenever(mockFavouriteContentLocalStorage.deleteAsFavourite(ContentId("a"))).doThrow(RuntimeException())
|
||||||
|
|
||||||
Assertions.assertThrows(RuntimeException::class.java) {
|
Assertions.assertThrows(RuntimeException::class.java) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package org.fnives.test.showcase.core.login
|
package org.fnives.test.showcase.core.login
|
||||||
|
|
||||||
import kotlinx.coroutines.test.runBlockingTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.fnives.test.showcase.core.shared.UnexpectedException
|
import org.fnives.test.showcase.core.shared.UnexpectedException
|
||||||
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
|
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
|
||||||
import org.fnives.test.showcase.model.auth.LoginCredentials
|
import org.fnives.test.showcase.model.auth.LoginCredentials
|
||||||
|
|
@ -38,7 +38,7 @@ internal class LoginUseCaseTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN empty username WHEN trying to login THEN invalid username is returned")
|
@DisplayName("GIVEN empty username WHEN trying to login THEN invalid username is returned")
|
||||||
@Test
|
@Test
|
||||||
fun emptyUserNameReturnsLoginStatusError() = runBlockingTest {
|
fun emptyUserNameReturnsLoginStatusError() = runTest {
|
||||||
val expected = Answer.Success(LoginStatus.INVALID_USERNAME)
|
val expected = Answer.Success(LoginStatus.INVALID_USERNAME)
|
||||||
|
|
||||||
val actual = sut.invoke(LoginCredentials("", "a"))
|
val actual = sut.invoke(LoginCredentials("", "a"))
|
||||||
|
|
@ -50,7 +50,7 @@ internal class LoginUseCaseTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN empty password WHEN trying to login THEN invalid password is returned")
|
@DisplayName("GIVEN empty password WHEN trying to login THEN invalid password is returned")
|
||||||
@Test
|
@Test
|
||||||
fun emptyPasswordNameReturnsLoginStatusError() = runBlockingTest {
|
fun emptyPasswordNameReturnsLoginStatusError() = runTest {
|
||||||
val expected = Answer.Success(LoginStatus.INVALID_PASSWORD)
|
val expected = Answer.Success(LoginStatus.INVALID_PASSWORD)
|
||||||
|
|
||||||
val actual = sut.invoke(LoginCredentials("a", ""))
|
val actual = sut.invoke(LoginCredentials("a", ""))
|
||||||
|
|
@ -62,7 +62,7 @@ internal class LoginUseCaseTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN invalid credentials response WHEN trying to login THEN invalid credentials is returned ")
|
@DisplayName("GIVEN invalid credentials response WHEN trying to login THEN invalid credentials is returned ")
|
||||||
@Test
|
@Test
|
||||||
fun invalidLoginResponseReturnInvalidCredentials() = runBlockingTest {
|
fun invalidLoginResponseReturnInvalidCredentials() = runTest {
|
||||||
val expected = Answer.Success(LoginStatus.INVALID_CREDENTIALS)
|
val expected = Answer.Success(LoginStatus.INVALID_CREDENTIALS)
|
||||||
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b")))
|
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b")))
|
||||||
.doReturn(LoginStatusResponses.InvalidCredentials)
|
.doReturn(LoginStatusResponses.InvalidCredentials)
|
||||||
|
|
@ -75,7 +75,7 @@ internal class LoginUseCaseTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN success response WHEN trying to login THEN session is saved and success is returned")
|
@DisplayName("GIVEN success response WHEN trying to login THEN session is saved and success is returned")
|
||||||
@Test
|
@Test
|
||||||
fun validResponseResultsInSavingSessionAndSuccessReturned() = runBlockingTest {
|
fun validResponseResultsInSavingSessionAndSuccessReturned() = runTest {
|
||||||
val expected = Answer.Success(LoginStatus.SUCCESS)
|
val expected = Answer.Success(LoginStatus.SUCCESS)
|
||||||
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b")))
|
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b")))
|
||||||
.doReturn(LoginStatusResponses.Success(Session("c", "d")))
|
.doReturn(LoginStatusResponses.Success(Session("c", "d")))
|
||||||
|
|
@ -89,7 +89,7 @@ internal class LoginUseCaseTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN error resposne WHEN trying to login THEN session is not touched and error is returned")
|
@DisplayName("GIVEN error resposne WHEN trying to login THEN session is not touched and error is returned")
|
||||||
@Test
|
@Test
|
||||||
fun invalidResponseResultsInErrorReturned() = runBlockingTest {
|
fun invalidResponseResultsInErrorReturned() = runTest {
|
||||||
val exception = RuntimeException()
|
val exception = RuntimeException()
|
||||||
val expected = Answer.Error<LoginStatus>(UnexpectedException(exception))
|
val expected = Answer.Error<LoginStatus>(UnexpectedException(exception))
|
||||||
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b")))
|
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b")))
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package org.fnives.test.showcase.core.login.hilt
|
package org.fnives.test.showcase.core.login.hilt
|
||||||
|
|
||||||
import kotlinx.coroutines.test.runBlockingTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.fnives.test.showcase.core.content.ContentRepository
|
import org.fnives.test.showcase.core.content.ContentRepository
|
||||||
import org.fnives.test.showcase.core.login.LogoutUseCase
|
import org.fnives.test.showcase.core.login.LogoutUseCase
|
||||||
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
|
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
|
||||||
|
|
@ -22,6 +22,7 @@ internal class LogoutUseCaseTest {
|
||||||
lateinit var sut: LogoutUseCase
|
lateinit var sut: LogoutUseCase
|
||||||
private lateinit var mockUserDataLocalStorage: UserDataLocalStorage
|
private lateinit var mockUserDataLocalStorage: UserDataLocalStorage
|
||||||
private lateinit var testCoreComponent: TestCoreComponent
|
private lateinit var testCoreComponent: TestCoreComponent
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var contentRepository: ContentRepository
|
lateinit var contentRepository: ContentRepository
|
||||||
|
|
||||||
|
|
@ -45,7 +46,7 @@ internal class LogoutUseCaseTest {
|
||||||
|
|
||||||
@DisplayName("WHEN logout invoked THEN storage is cleared")
|
@DisplayName("WHEN logout invoked THEN storage is cleared")
|
||||||
@Test
|
@Test
|
||||||
fun logoutResultsInStorageCleaning() = runBlockingTest {
|
fun logoutResultsInStorageCleaning() = runTest {
|
||||||
val repositoryBefore = contentRepository
|
val repositoryBefore = contentRepository
|
||||||
|
|
||||||
sut.invoke()
|
sut.invoke()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package org.fnives.test.showcase.core.login.koin
|
package org.fnives.test.showcase.core.login.koin
|
||||||
|
|
||||||
import kotlinx.coroutines.test.runBlockingTest
|
import kotlinx.coroutines.test.runBlockingTest
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.fnives.test.showcase.core.content.ContentRepository
|
import org.fnives.test.showcase.core.content.ContentRepository
|
||||||
import org.fnives.test.showcase.core.di.koin.createCoreModule
|
import org.fnives.test.showcase.core.di.koin.createCoreModule
|
||||||
import org.fnives.test.showcase.core.login.LogoutUseCase
|
import org.fnives.test.showcase.core.login.LogoutUseCase
|
||||||
|
|
@ -56,7 +57,7 @@ internal class LogoutUseCaseTest : KoinTest {
|
||||||
|
|
||||||
@DisplayName("WHEN logout invoked THEN storage is cleared")
|
@DisplayName("WHEN logout invoked THEN storage is cleared")
|
||||||
@Test
|
@Test
|
||||||
fun logoutResultsInStorageCleaning() = runBlockingTest {
|
fun logoutResultsInStorageCleaning() = runTest {
|
||||||
val repositoryBefore = getKoin().get<ContentRepository>()
|
val repositoryBefore = getKoin().get<ContentRepository>()
|
||||||
|
|
||||||
sut.invoke()
|
sut.invoke()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package org.fnives.test.showcase.core.shared
|
||||||
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.fnives.test.showcase.model.shared.Answer
|
import org.fnives.test.showcase.model.shared.Answer
|
||||||
import org.fnives.test.showcase.model.shared.Resource
|
import org.fnives.test.showcase.model.shared.Resource
|
||||||
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
|
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
|
||||||
|
|
@ -15,7 +16,7 @@ internal class AnswerUtilsKtTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN network exception thrown WHEN wrapped into answer THEN answer error is returned")
|
@DisplayName("GIVEN network exception thrown WHEN wrapped into answer THEN answer error is returned")
|
||||||
@Test
|
@Test
|
||||||
fun networkExceptionThrownResultsInError() = runBlocking {
|
fun networkExceptionThrownResultsInError() = runTest {
|
||||||
val exception = NetworkException(Throwable())
|
val exception = NetworkException(Throwable())
|
||||||
val expected = Answer.Error<Unit>(exception)
|
val expected = Answer.Error<Unit>(exception)
|
||||||
|
|
||||||
|
|
@ -26,7 +27,7 @@ internal class AnswerUtilsKtTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN parsing exception thrown WHEN wrapped into answer THEN answer error is returned")
|
@DisplayName("GIVEN parsing exception thrown WHEN wrapped into answer THEN answer error is returned")
|
||||||
@Test
|
@Test
|
||||||
fun parsingExceptionThrownResultsInError() = runBlocking {
|
fun parsingExceptionThrownResultsInError() = runTest {
|
||||||
val exception = ParsingException(Throwable())
|
val exception = ParsingException(Throwable())
|
||||||
val expected = Answer.Error<Unit>(exception)
|
val expected = Answer.Error<Unit>(exception)
|
||||||
|
|
||||||
|
|
@ -37,7 +38,7 @@ internal class AnswerUtilsKtTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN unexpected throwable thrown WHEN wrapped into answer THEN answer error is returned")
|
@DisplayName("GIVEN unexpected throwable thrown WHEN wrapped into answer THEN answer error is returned")
|
||||||
@Test
|
@Test
|
||||||
fun unexpectedExceptionThrownResultsInError() = runBlocking {
|
fun unexpectedExceptionThrownResultsInError() = runTest {
|
||||||
val exception = Throwable()
|
val exception = Throwable()
|
||||||
val expected = Answer.Error<Unit>(UnexpectedException(exception))
|
val expected = Answer.Error<Unit>(UnexpectedException(exception))
|
||||||
|
|
||||||
|
|
@ -48,7 +49,7 @@ internal class AnswerUtilsKtTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN string WHEN wrapped into answer THEN string answer is returned")
|
@DisplayName("GIVEN string WHEN wrapped into answer THEN string answer is returned")
|
||||||
@Test
|
@Test
|
||||||
fun stringIsReturnedWrappedIntoSuccess() = runBlocking {
|
fun stringIsReturnedWrappedIntoSuccess() = runTest {
|
||||||
val expected = Answer.Success("banan")
|
val expected = Answer.Success("banan")
|
||||||
|
|
||||||
val actual = wrapIntoAnswer { "banan" }
|
val actual = wrapIntoAnswer { "banan" }
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ project.ext {
|
||||||
androidx_room_version = "2.4.0"
|
androidx_room_version = "2.4.0"
|
||||||
activity_ktx_version = "1.4.0"
|
activity_ktx_version = "1.4.0"
|
||||||
|
|
||||||
coroutines_version = "1.5.2"
|
coroutines_version = "1.6.0"
|
||||||
|
turbine_version = "0.7.0"
|
||||||
koin_version = "3.1.2"
|
koin_version = "3.1.2"
|
||||||
coil_version = "1.1.1"
|
coil_version = "1.1.1"
|
||||||
retrofit_version = "2.9.0"
|
retrofit_version = "2.9.0"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue