Issue#11 Create Turbine variant of Flow Tests

This commit is contained in:
Gergely Hegedus 2022-01-23 22:59:07 +02:00
parent b52652ed67
commit a71fa67de2
4 changed files with 428 additions and 50 deletions

View file

@ -1,6 +1,5 @@
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
@ -147,28 +146,4 @@ internal class ContentRepositoryTest {
Assertions.assertFalse(actual.isCompleted) Assertions.assertFalse(actual.isCompleted)
actual.cancel() actual.cancel()
} }
@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,9 +1,10 @@
package org.fnives.test.showcase.core.content package org.fnives.test.showcase.core.content
import app.cash.turbine.test 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.advanceUntilIdle
import kotlinx.coroutines.test.runTest 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
@ -151,15 +152,18 @@ internal class GetAllContentUseCaseTest {
Resource.Success(listOf(FavouriteContent(content, true))) Resource.Success(listOf(FavouriteContent(content, true)))
) )
sut.get().test { val actual = async(coroutineContext) {
contentFlow.value = Resource.Success(listOf(content)) sut.get().take(3).toList()
favouriteContentIdFlow.value = listOf(ContentId("a"))
expected.forEach { expectedItem ->
Assertions.assertEquals(expectedItem, awaitItem())
}
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
} }
advanceUntilIdle()
contentFlow.value = Resource.Success(listOf(content))
advanceUntilIdle()
favouriteContentIdFlow.value = listOf(ContentId("a"))
advanceUntilIdle()
Assertions.assertEquals(expected, actual.getCompleted())
} }
@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")
@ -174,15 +178,18 @@ internal class GetAllContentUseCaseTest {
Resource.Success(listOf(FavouriteContent(content, false))) Resource.Success(listOf(FavouriteContent(content, false)))
) )
sut.get().test { val actual = async(coroutineContext) {
contentFlow.value = Resource.Success(listOf(content)) sut.get().take(3).toList()
favouriteContentIdFlow.value = emptyList()
expected.forEach { expectedItem ->
Assertions.assertEquals(expectedItem, awaitItem())
}
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
} }
advanceUntilIdle()
contentFlow.value = Resource.Success(listOf(content))
advanceUntilIdle()
favouriteContentIdFlow.value = emptyList()
advanceUntilIdle()
Assertions.assertEquals(expected, actual.getCompleted())
} }
@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")
@ -197,14 +204,17 @@ internal class GetAllContentUseCaseTest {
Resource.Loading() Resource.Loading()
) )
sut.get().test { val actual = async(coroutineContext) {
contentFlow.value = Resource.Success(listOf(content)) sut.get().take(3).toList()
contentFlow.value = Resource.Loading()
expected.forEach { expectedItem ->
Assertions.assertEquals(expectedItem, awaitItem())
}
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
} }
advanceUntilIdle()
contentFlow.value = Resource.Success(listOf(content))
advanceUntilIdle()
contentFlow.value = Resource.Loading()
advanceUntilIdle()
Assertions.assertEquals(expected, actual.getCompleted())
} }
} }

View file

@ -0,0 +1,165 @@
package org.fnives.test.showcase.core.content
import app.cash.turbine.test
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.fnives.test.showcase.core.shared.UnexpectedException
import org.fnives.test.showcase.model.content.Content
import org.fnives.test.showcase.model.content.ContentId
import org.fnives.test.showcase.model.content.ImageUrl
import org.fnives.test.showcase.model.shared.Resource
import org.fnives.test.showcase.network.content.ContentRemoteSource
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doSuspendableAnswer
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
class TurbineContentRepositoryTest {
private lateinit var sut: ContentRepository
private lateinit var mockContentRemoteSource: ContentRemoteSource
@BeforeEach
fun setUp() {
mockContentRemoteSource = mock()
sut = ContentRepository(mockContentRemoteSource)
}
@DisplayName("GIVEN no interaction THEN remote source is not called")
@Test
fun fetchingIsLazy() {
verifyNoMoreInteractions(mockContentRemoteSource)
}
@DisplayName("GIVEN content response WHEN content observed THEN loading AND data is returned")
@Test
fun happyFlow() = runTest {
val expected = listOf(
Resource.Loading(),
Resource.Success(listOf(Content(ContentId("a"), "", "", ImageUrl(""))))
)
whenever(mockContentRemoteSource.get()).doReturn(
listOf(Content(ContentId("a"), "", "", ImageUrl("")))
)
sut.contents.test {
expected.forEach { expectedItem ->
Assertions.assertEquals(expectedItem, awaitItem())
}
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
}
}
@DisplayName("GIVEN content error WHEN content observed THEN loading AND data is returned")
@Test
fun errorFlow() = runTest {
val exception = RuntimeException()
val expected = listOf(
Resource.Loading(),
Resource.Error<List<Content>>(UnexpectedException(exception))
)
whenever(mockContentRemoteSource.get()).doThrow(exception)
sut.contents.test {
expected.forEach { expectedItem ->
Assertions.assertEquals(expectedItem, awaitItem())
}
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
}
}
@DisplayName("GIVEN saved cache WHEN collected THEN cache is returned")
@Test
fun verifyCaching() = runTest {
val content = Content(ContentId("1"), "", "", ImageUrl(""))
val expected = listOf(Resource.Success(listOf(content)))
whenever(mockContentRemoteSource.get()).doReturn(listOf(content))
sut.contents.take(2).toList()
sut.contents.test {
expected.forEach { expectedItem ->
Assertions.assertEquals(expectedItem, awaitItem())
}
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
}
verify(mockContentRemoteSource, times(1)).get()
}
@DisplayName("GIVEN no response from remote source WHEN content observed THEN loading is returned")
@Test
fun loadingIsShownBeforeTheRequestIsReturned() = runTest {
val expected = listOf(Resource.Loading<List<Content>>())
val suspendedRequest = CompletableDeferred<Unit>()
whenever(mockContentRemoteSource.get()).doSuspendableAnswer {
suspendedRequest.await()
emptyList()
}
sut.contents.test {
expected.forEach { expectedItem ->
Assertions.assertEquals(expectedItem, awaitItem())
}
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
}
suspendedRequest.complete(Unit)
}
@DisplayName("GIVEN content response THEN error WHEN fetched THEN returned states are loading data loading error")
@Test
fun whenFetchingRequestIsCalledAgain() = runTest(UnconfinedTestDispatcher()) {
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())
}
}
}
@DisplayName("GIVEN content response THEN error WHEN fetched THEN only 4 items are emitted")
@Test
fun noAdditionalItemsEmitted() = 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

@ -0,0 +1,228 @@
package org.fnives.test.showcase.core.content
import app.cash.turbine.test
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.Content
import org.fnives.test.showcase.model.content.ContentId
import org.fnives.test.showcase.model.content.FavouriteContent
import org.fnives.test.showcase.model.content.ImageUrl
import org.fnives.test.showcase.model.shared.Resource
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
class TurbineGetAllContentUseCaseTest {
private lateinit var sut: GetAllContentUseCase
private lateinit var mockContentRepository: ContentRepository
private lateinit var mockFavouriteContentLocalStorage: FavouriteContentLocalStorage
private lateinit var contentFlow: MutableStateFlow<Resource<List<Content>>>
private lateinit var favouriteContentIdFlow: MutableStateFlow<List<ContentId>>
@BeforeEach
fun setUp() {
mockFavouriteContentLocalStorage = mock()
mockContentRepository = mock()
favouriteContentIdFlow = MutableStateFlow(emptyList())
contentFlow = MutableStateFlow(Resource.Loading())
whenever(mockFavouriteContentLocalStorage.observeFavourites()).doReturn(
favouriteContentIdFlow
)
whenever(mockContentRepository.contents).doReturn(contentFlow)
sut = GetAllContentUseCase(mockContentRepository, mockFavouriteContentLocalStorage)
}
@DisplayName("GIVEN loading AND empty favourite WHEN observed THEN loading is shown")
@Test
fun loadingResourceWithNoFavouritesResultsInLoadingResource() = runTest {
favouriteContentIdFlow.value = emptyList()
contentFlow.value = Resource.Loading()
val expected = listOf(Resource.Loading<List<FavouriteContent>>())
sut.get().test {
expected.forEach { expectedItem ->
Assertions.assertEquals(expectedItem, awaitItem())
}
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
}
}
@DisplayName("GIVEN loading AND listOfFavourite WHEN observed THEN loading is shown")
@Test
fun loadingResourceWithFavouritesResultsInLoadingResource() = runTest {
favouriteContentIdFlow.value = listOf(ContentId("a"))
contentFlow.value = Resource.Loading()
val expected = listOf(Resource.Loading<List<FavouriteContent>>())
sut.get().test {
expected.forEach { expectedItem ->
Assertions.assertEquals(expectedItem, awaitItem())
}
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
}
}
@DisplayName("GIVEN error AND empty favourite WHEN observed THEN error is shown")
@Test
fun errorResourceWithNoFavouritesResultsInErrorResource() = runTest {
favouriteContentIdFlow.value = emptyList()
val exception = Throwable()
contentFlow.value = Resource.Error(exception)
val expected = listOf(Resource.Error<List<FavouriteContent>>(exception))
sut.get().test {
expected.forEach { expectedItem ->
Assertions.assertEquals(expectedItem, awaitItem())
}
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
}
}
@DisplayName("GIVEN error AND listOfFavourite WHEN observed THEN error is shown")
@Test
fun errorResourceWithFavouritesResultsInErrorResource() = runTest {
favouriteContentIdFlow.value = listOf(ContentId("b"))
val exception = Throwable()
contentFlow.value = Resource.Error(exception)
val expected = listOf(Resource.Error<List<FavouriteContent>>(exception))
sut.get().test {
expected.forEach { expectedItem ->
Assertions.assertEquals(expectedItem, awaitItem())
}
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
}
}
@DisplayName("GIVEN listOfContent AND empty favourite WHEN observed THEN favourites are returned")
@Test
fun successResourceWithNoFavouritesResultsInNoFavouritedItems() = runTest {
favouriteContentIdFlow.value = emptyList()
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
contentFlow.value = Resource.Success(listOf(content))
val items = listOf(
FavouriteContent(content, false)
)
val expected = listOf(Resource.Success(items))
sut.get().test {
expected.forEach { expectedItem ->
Assertions.assertEquals(expectedItem, awaitItem())
}
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
}
}
@DisplayName("GIVEN listOfContent AND other favourite id WHEN observed THEN favourites are returned")
@Test
fun successResourceWithDifferentFavouritesResultsInNoFavouritedItems() = runTest {
favouriteContentIdFlow.value = listOf(ContentId("x"))
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
contentFlow.value = Resource.Success(listOf(content))
val items = listOf(
FavouriteContent(content, false)
)
val expected = listOf(Resource.Success(items))
sut.get().test {
expected.forEach { expectedItem ->
Assertions.assertEquals(expectedItem, awaitItem())
}
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
}
}
@DisplayName("GIVEN listOfContent AND same favourite id WHEN observed THEN favourites are returned")
@Test
fun successResourceWithSameFavouritesResultsInFavouritedItems() = runTest {
favouriteContentIdFlow.value = listOf(ContentId("a"))
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
contentFlow.value = Resource.Success(listOf(content))
val items = listOf(
FavouriteContent(content, true)
)
val expected = listOf(Resource.Success(items))
sut.get().test {
expected.forEach { expectedItem ->
Assertions.assertEquals(expectedItem, awaitItem())
}
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
}
}
@DisplayName("GIVEN loading then data then added favourite WHEN observed THEN loading then correct favourites are returned")
@Test
fun whileLoadingAndAddingItemsReactsProperly() = runTest {
favouriteContentIdFlow.value = emptyList()
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
contentFlow.value = Resource.Loading()
val expected = listOf(
Resource.Loading(),
Resource.Success(listOf(FavouriteContent(content, false))),
Resource.Success(listOf(FavouriteContent(content, true)))
)
sut.get().test {
contentFlow.value = Resource.Success(listOf(content))
favouriteContentIdFlow.value = listOf(ContentId("a"))
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")
@Test
fun whileLoadingAndRemovingItemsReactsProperly() = runTest {
favouriteContentIdFlow.value = listOf(ContentId("a"))
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
contentFlow.value = Resource.Loading()
val expected = listOf(
Resource.Loading(),
Resource.Success(listOf(FavouriteContent(content, true))),
Resource.Success(listOf(FavouriteContent(content, false)))
)
sut.get().test {
contentFlow.value = Resource.Success(listOf(content))
favouriteContentIdFlow.value = emptyList()
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")
@Test
fun loadingThenDataThenLoadingReactsProperly() = runTest {
favouriteContentIdFlow.value = listOf(ContentId("a"))
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
contentFlow.value = Resource.Loading()
val expected = listOf(
Resource.Loading(),
Resource.Success(listOf(FavouriteContent(content, true))),
Resource.Loading()
)
sut.get().test {
contentFlow.value = Resource.Success(listOf(content))
contentFlow.value = Resource.Loading()
expected.forEach { expectedItem ->
Assertions.assertEquals(expectedItem, awaitItem())
}
Assertions.assertTrue(cancelAndConsumeRemainingEvents().isEmpty())
}
}
}