Update core tests to the coroutines tests 1.6.0

This commit is contained in:
Alex Gabor 2022-01-22 10:55:02 +02:00
parent b35467fcba
commit 3c80744f6d
13 changed files with 272 additions and 240 deletions

View file

@ -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:

View file

@ -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"
} }

View file

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

View file

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

View file

@ -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,8 +108,7 @@ 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(),
@ -131,10 +121,10 @@ 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(4).toList() } val actual = async {
testDispatcher.advanceUntilIdle() sut.contents.take(4).toList()
}
sut.fetch() sut.fetch()
testDispatcher.advanceUntilIdle()
Assertions.assertEquals(expected, actual.await()) Assertions.assertEquals(expected, actual.await())
} }
@ -143,7 +133,7 @@ internal class ContentRepositoryTest {
@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())
}
}
} }

View file

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

View file

@ -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,8 +45,7 @@ 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>>()
@ -61,8 +57,7 @@ internal class GetAllContentUseCaseTest {
@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>>()
@ -74,8 +69,7 @@ internal class GetAllContentUseCaseTest {
@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)
@ -88,8 +82,7 @@ internal class GetAllContentUseCaseTest {
@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)
@ -102,8 +95,7 @@ internal class GetAllContentUseCaseTest {
@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))
@ -119,8 +111,7 @@ internal class GetAllContentUseCaseTest {
@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))
@ -136,8 +127,7 @@ internal class GetAllContentUseCaseTest {
@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))
@ -153,8 +143,7 @@ internal class GetAllContentUseCaseTest {
@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()
@ -164,24 +153,20 @@ internal class GetAllContentUseCaseTest {
Resource.Success(listOf(FavouriteContent(content, true))) Resource.Success(listOf(FavouriteContent(content, true)))
) )
val actual = async(testDispatcher) { sut.get().test {
sut.get().take(3).toList()
}
testDispatcher.advanceUntilIdle()
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()
@ -191,24 +176,20 @@ internal class GetAllContentUseCaseTest {
Resource.Success(listOf(FavouriteContent(content, false))) Resource.Success(listOf(FavouriteContent(content, false)))
) )
val actual = async(testDispatcher) { sut.get().test {
sut.get().take(3).toList()
}
testDispatcher.advanceUntilIdle()
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()
@ -218,17 +199,14 @@ internal class GetAllContentUseCaseTest {
Resource.Loading() Resource.Loading()
) )
val actual = async(testDispatcher) { sut.get().test {
sut.get().take(3).toList()
}
testDispatcher.advanceUntilIdle()
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())
}
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -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" }

View file

@ -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"