Issue#41 Copy full example into separate module with Hilt Integration

This commit is contained in:
Gergely Hegedus 2022-09-27 17:16:05 +03:00
parent 69e76dc0da
commit 52a99a82fc
229 changed files with 8416 additions and 11 deletions

View file

@ -0,0 +1,13 @@
package org.fnives.test.showcase.hilt.core.content
import org.fnives.test.showcase.hilt.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.ContentId
import javax.inject.Inject
class AddContentToFavouriteUseCase @Inject internal constructor(
private val favouriteContentLocalStorage: FavouriteContentLocalStorage,
) {
suspend fun invoke(contentId: ContentId) =
favouriteContentLocalStorage.markAsFavourite(contentId)
}

View file

@ -0,0 +1,41 @@
package org.fnives.test.showcase.hilt.core.content
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import org.fnives.test.showcase.hilt.core.di.LoggedInModuleInject
import org.fnives.test.showcase.hilt.core.shared.Optional
import org.fnives.test.showcase.hilt.core.shared.mapIntoResource
import org.fnives.test.showcase.hilt.core.shared.wrapIntoAnswer
import org.fnives.test.showcase.hilt.network.content.ContentRemoteSource
import org.fnives.test.showcase.model.content.Content
import org.fnives.test.showcase.model.shared.Resource
internal class ContentRepository @LoggedInModuleInject internal constructor(
private val contentRemoteSource: ContentRemoteSource,
) {
private val mutableContentFlow = MutableStateFlow(Optional<List<Content>>(null))
private val requestFlow: Flow<Resource<List<Content>>> = flow {
emit(Resource.Loading())
val response = wrapIntoAnswer { contentRemoteSource.get() }.mapIntoResource()
if (response is Resource.Success) {
mutableContentFlow.value = Optional(response.data)
}
emit(response)
}
@OptIn(ExperimentalCoroutinesApi::class)
val contents: Flow<Resource<List<Content>>> = mutableContentFlow.flatMapLatest {
if (it.item != null) flowOf(Resource.Success(it.item)) else requestFlow
}
.distinctUntilChanged()
fun fetch() {
mutableContentFlow.value = Optional(null)
}
}

View file

@ -0,0 +1,8 @@
package org.fnives.test.showcase.hilt.core.content
import javax.inject.Inject
class FetchContentUseCase @Inject internal constructor(private val contentRepository: ContentRepository) {
fun invoke() = contentRepository.fetch()
}

View file

@ -0,0 +1,47 @@
package org.fnives.test.showcase.hilt.core.content
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import org.fnives.test.showcase.hilt.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.shared.Resource
import javax.inject.Inject
class GetAllContentUseCase @Inject internal constructor(
private val contentRepository: ContentRepository,
private val favouriteContentLocalStorage: FavouriteContentLocalStorage,
) {
fun get(): Flow<Resource<List<FavouriteContent>>> =
contentRepository.contents.combine(
favouriteContentLocalStorage.observeFavourites(),
::combineContentWithFavourites
)
.distinctUntilChanged()
companion object {
private fun combineContentWithFavourites(
contentResource: Resource<List<Content>>,
favouriteContents: List<ContentId>,
): Resource<List<FavouriteContent>> =
when (contentResource) {
is Resource.Error -> Resource.Error(contentResource.error)
is Resource.Loading -> Resource.Loading()
is Resource.Success ->
Resource.Success(
combineContentWithFavourites(contentResource.data, favouriteContents)
)
}
private fun combineContentWithFavourites(
content: List<Content>,
favourite: List<ContentId>,
): List<FavouriteContent> =
content.map {
FavouriteContent(content = it, isFavourite = favourite.contains(it.id))
}
}
}

View file

@ -0,0 +1,14 @@
package org.fnives.test.showcase.hilt.core.content
import org.fnives.test.showcase.hilt.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.ContentId
import javax.inject.Inject
class RemoveContentFromFavouritesUseCase @Inject internal constructor(
private val favouriteContentLocalStorage: FavouriteContentLocalStorage,
) {
suspend fun invoke(contentId: ContentId) {
favouriteContentLocalStorage.deleteAsFavourite(contentId)
}
}

View file

@ -0,0 +1,33 @@
package org.fnives.test.showcase.hilt.core.di
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.fnives.test.showcase.hilt.core.login.LogoutUseCase
import org.fnives.test.showcase.hilt.core.session.SessionExpirationAdapter
import org.fnives.test.showcase.hilt.core.storage.NetworkSessionLocalStorageAdapter
import org.fnives.test.showcase.hilt.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.hilt.network.session.NetworkSessionExpirationListener
import org.fnives.test.showcase.hilt.network.session.NetworkSessionLocalStorage
@InstallIn(SingletonComponent::class)
@Module
object CoreModule {
@Provides
internal fun bindNetworkSessionLocalStorageAdapter(
networkSessionLocalStorageAdapter: NetworkSessionLocalStorageAdapter
): NetworkSessionLocalStorage = networkSessionLocalStorageAdapter
@Provides
internal fun bindNetworkSessionExpirationListener(
sessionExpirationAdapter: SessionExpirationAdapter
): NetworkSessionExpirationListener = sessionExpirationAdapter
@Provides
fun provideLogoutUseCase(
storage: UserDataLocalStorage,
reloadLoggedInModuleInjectModule: ReloadLoggedInModuleInjectModule
): LogoutUseCase = LogoutUseCase(storage, reloadLoggedInModuleInjectModule)
}

View file

@ -0,0 +1,8 @@
package org.fnives.test.showcase.hilt.core.di
import org.fnives.library.reloadable.module.annotation.ReloadableModule
@ReloadableModule
@Target(AnnotationTarget.CONSTRUCTOR)
@Retention(AnnotationRetention.SOURCE)
annotation class LoggedInModuleInject

View file

@ -0,0 +1,11 @@
package org.fnives.test.showcase.hilt.core.login
import org.fnives.test.showcase.hilt.core.storage.UserDataLocalStorage
import javax.inject.Inject
class IsUserLoggedInUseCase @Inject internal constructor(
private val userDataLocalStorage: UserDataLocalStorage,
) {
fun invoke(): Boolean = userDataLocalStorage.session != null
}

View file

@ -0,0 +1,31 @@
package org.fnives.test.showcase.hilt.core.login
import org.fnives.test.showcase.hilt.core.shared.wrapIntoAnswer
import org.fnives.test.showcase.hilt.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.hilt.network.auth.LoginRemoteSource
import org.fnives.test.showcase.hilt.network.auth.model.LoginStatusResponses
import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.model.auth.LoginStatus
import org.fnives.test.showcase.model.shared.Answer
import javax.inject.Inject
class LoginUseCase @Inject internal constructor(
private val loginRemoteSource: LoginRemoteSource,
private val userDataLocalStorage: UserDataLocalStorage,
) {
suspend fun invoke(credentials: LoginCredentials): Answer<LoginStatus> {
if (credentials.username.isBlank()) return Answer.Success(LoginStatus.INVALID_USERNAME)
if (credentials.password.isBlank()) return Answer.Success(LoginStatus.INVALID_PASSWORD)
return wrapIntoAnswer {
when (val response = loginRemoteSource.login(credentials)) {
LoginStatusResponses.InvalidCredentials -> LoginStatus.INVALID_CREDENTIALS
is LoginStatusResponses.Success -> {
userDataLocalStorage.session = response.session
LoginStatus.SUCCESS
}
}
}
}
}

View file

@ -0,0 +1,15 @@
package org.fnives.test.showcase.hilt.core.login
import org.fnives.test.showcase.hilt.core.di.ReloadLoggedInModuleInjectModule
import org.fnives.test.showcase.hilt.core.storage.UserDataLocalStorage
class LogoutUseCase(
private val storage: UserDataLocalStorage,
private val reloadLoggedInModuleInjectModule: ReloadLoggedInModuleInjectModule,
) {
suspend fun invoke() {
reloadLoggedInModuleInjectModule.reload()
storage.session = null
}
}

View file

@ -0,0 +1,11 @@
package org.fnives.test.showcase.hilt.core.session
import org.fnives.test.showcase.hilt.network.session.NetworkSessionExpirationListener
import javax.inject.Inject
internal class SessionExpirationAdapter @Inject constructor(
private val sessionExpirationListener: SessionExpirationListener
) : NetworkSessionExpirationListener {
override fun onSessionExpired() = sessionExpirationListener.onSessionExpired()
}

View file

@ -0,0 +1,5 @@
package org.fnives.test.showcase.hilt.core.session
interface SessionExpirationListener {
fun onSessionExpired()
}

View file

@ -0,0 +1,26 @@
package org.fnives.test.showcase.hilt.core.shared
import kotlinx.coroutines.CancellationException
import org.fnives.test.showcase.hilt.network.shared.exceptions.NetworkException
import org.fnives.test.showcase.hilt.network.shared.exceptions.ParsingException
import org.fnives.test.showcase.model.shared.Answer
import org.fnives.test.showcase.model.shared.Resource
@Suppress("RethrowCaughtException")
internal suspend fun <T> wrapIntoAnswer(callback: suspend () -> T): Answer<T> =
try {
Answer.Success(callback())
} catch (networkException: NetworkException) {
Answer.Error(networkException)
} catch (parsingException: ParsingException) {
Answer.Error(parsingException)
} catch (cancellationException: CancellationException) {
throw cancellationException
} catch (throwable: Throwable) {
Answer.Error(UnexpectedException(throwable))
}
internal fun <T> Answer<T>.mapIntoResource() = when (this) {
is Answer.Error -> Resource.Error(error)
is Answer.Success -> Resource.Success(data)
}

View file

@ -0,0 +1,3 @@
package org.fnives.test.showcase.hilt.core.shared
internal class Optional<T : Any>(val item: T?)

View file

@ -0,0 +1,11 @@
package org.fnives.test.showcase.hilt.core.shared
class UnexpectedException(cause: Throwable) : RuntimeException(cause.message, cause) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return this.cause == (other as UnexpectedException).cause
}
override fun hashCode(): Int = super.hashCode() + cause.hashCode()
}

View file

@ -0,0 +1,16 @@
package org.fnives.test.showcase.hilt.core.storage
import org.fnives.test.showcase.hilt.network.session.NetworkSessionLocalStorage
import org.fnives.test.showcase.model.session.Session
import javax.inject.Inject
internal class NetworkSessionLocalStorageAdapter @Inject constructor(
private val userDataLocalStorage: UserDataLocalStorage,
) : NetworkSessionLocalStorage {
override var session: Session?
get() = userDataLocalStorage.session
set(value) {
userDataLocalStorage.session = value
}
}

View file

@ -0,0 +1,7 @@
package org.fnives.test.showcase.hilt.core.storage
import org.fnives.test.showcase.model.session.Session
interface UserDataLocalStorage {
var session: Session?
}

View file

@ -0,0 +1,13 @@
package org.fnives.test.showcase.hilt.core.storage.content
import kotlinx.coroutines.flow.Flow
import org.fnives.test.showcase.model.content.ContentId
interface FavouriteContentLocalStorage {
fun observeFavourites(): Flow<List<ContentId>>
suspend fun markAsFavourite(contentId: ContentId)
suspend fun deleteAsFavourite(contentId: ContentId)
}

View file

@ -0,0 +1,59 @@
package org.fnives.test.showcase.hilt.core.content
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.fnives.test.showcase.hilt.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.ContentId
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
@Suppress("TestFunctionName")
@OptIn(ExperimentalCoroutinesApi::class)
internal class AddContentToFavouriteUseCaseTest {
private lateinit var sut: AddContentToFavouriteUseCase
private lateinit var mockFavouriteContentLocalStorage: FavouriteContentLocalStorage
@BeforeEach
fun setUp() {
mockFavouriteContentLocalStorage = mock()
sut = AddContentToFavouriteUseCase(mockFavouriteContentLocalStorage)
}
@DisplayName("WHEN nothing happens THEN the storage is not touched")
@Test
fun initializationDoesntAffectStorage() {
verifyNoInteractions(mockFavouriteContentLocalStorage)
}
@DisplayName("GIVEN contentId WHEN called THEN storage is called")
@Test
fun contentIdIsDelegatedToStorage() = runTest {
sut.invoke(ContentId("a"))
verify(mockFavouriteContentLocalStorage, times(1)).markAsFavourite(ContentId("a"))
verifyNoMoreInteractions(mockFavouriteContentLocalStorage)
}
@DisplayName("GIVEN throwing local storage WHEN thrown THEN its propagated")
@Test
fun storageThrowingIsPropagated() = runTest {
whenever(mockFavouriteContentLocalStorage.markAsFavourite(ContentId("a"))).doThrow(
RuntimeException()
)
assertThrows(RuntimeException::class.java) {
runBlocking { sut.invoke(ContentId("a")) }
}
}
}

View file

@ -0,0 +1,152 @@
package org.fnives.test.showcase.hilt.core.content
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.fnives.test.showcase.hilt.core.shared.UnexpectedException
import org.fnives.test.showcase.hilt.network.content.ContentRemoteSource
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.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
@Suppress("TestFunctionName")
@OptIn(ExperimentalCoroutinesApi::class)
internal class ContentRepositoryTest {
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("")))
)
val actual = sut.contents.take(2).toList()
Assertions.assertEquals(expected, actual)
}
@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)
val actual = sut.contents.take(2).toList()
Assertions.assertEquals(expected, actual)
}
@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()
val actual = sut.contents.take(1).toList()
verify(mockContentRemoteSource, times(1)).get()
Assertions.assertEquals(expected, actual)
}
@DisplayName("GIVEN no response from remote source WHEN content observed THEN loading is returned")
@Test
fun loadingIsShownBeforeTheRequestIsReturned() = runTest {
val expected = Resource.Loading<List<Content>>()
val suspendedRequest = CompletableDeferred<Unit>()
whenever(mockContentRemoteSource.get()).doSuspendableAnswer {
suspendedRequest.await()
emptyList()
}
val actual = sut.contents.take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
suspendedRequest.complete(Unit)
}
@DisplayName("GIVEN content response THEN error WHEN fetched THEN returned states are loading data loading error")
@Test
fun whenFetchingRequestIsCalledAgain() = 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
}
val actual = async {
sut.contents.take(4).toList()
}
advanceUntilIdle()
sut.fetch()
advanceUntilIdle()
Assertions.assertEquals(expected, actual.getCompleted())
}
@DisplayName("GIVEN content response THEN error WHEN fetched THEN only 4 items are emitted")
@Test
fun noAdditionalItemsEmitted() = runTest {
val exception = RuntimeException()
var first = true
whenever(mockContentRemoteSource.get()).doAnswer {
if (first) emptyList<Content>().also { first = false } else throw exception
}
val actual = async(coroutineContext) { sut.contents.take(5).toList() }
advanceUntilIdle()
sut.fetch()
advanceUntilIdle()
Assertions.assertFalse(actual.isCompleted)
actual.cancel()
}
}

View file

@ -0,0 +1,55 @@
package org.fnives.test.showcase.hilt.core.content
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
@Suppress("TestFunctionName")
@OptIn(ExperimentalCoroutinesApi::class)
internal class FetchContentUseCaseTest {
private lateinit var sut: FetchContentUseCase
private lateinit var mockContentRepository: ContentRepository
@BeforeEach
fun setUp() {
mockContentRepository = mock()
sut = FetchContentUseCase(mockContentRepository)
}
@DisplayName("WHEN nothing happens THEN the storage is not touched")
@Test
fun initializationDoesntAffectRepository() {
verifyNoInteractions(mockContentRepository)
}
@DisplayName("WHEN called THEN repository is called")
@Test
fun whenCalledRepositoryIsFetched() = runTest {
sut.invoke()
verify(mockContentRepository, times(1)).fetch()
verifyNoMoreInteractions(mockContentRepository)
}
@DisplayName("GIVEN throwing local storage WHEN thrown THEN its thrown")
@Test
fun whenRepositoryThrowsUseCaseAlsoThrows() = runTest {
whenever(mockContentRepository.fetch()).doThrow(RuntimeException())
assertThrows(RuntimeException::class.java) {
runBlocking { sut.invoke() }
}
}
}

View file

@ -0,0 +1,222 @@
package org.fnives.test.showcase.hilt.core.content
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.fnives.test.showcase.hilt.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
@Suppress("TestFunctionName")
@OptIn(ExperimentalCoroutinesApi::class)
internal class GetAllContentUseCaseTest {
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 = Resource.Loading<List<FavouriteContent>>()
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
@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 = Resource.Loading<List<FavouriteContent>>()
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
@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 = Resource.Error<List<FavouriteContent>>(exception)
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
@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 = Resource.Error<List<FavouriteContent>>(exception)
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
@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 = Resource.Success(items)
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
@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 = Resource.Success(items)
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
@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 = Resource.Success(items)
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
@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)))
)
val actual = async(coroutineContext) {
sut.get().take(3).toList()
}
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")
@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)))
)
val actual = async(coroutineContext) {
sut.get().take(3).toList()
}
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")
@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()
)
val actual = async(coroutineContext) {
sut.get().take(3).toList()
}
advanceUntilIdle()
contentFlow.value = Resource.Success(listOf(content))
advanceUntilIdle()
contentFlow.value = Resource.Loading()
advanceUntilIdle()
Assertions.assertEquals(expected, actual.getCompleted())
}
}

View file

@ -0,0 +1,57 @@
package org.fnives.test.showcase.hilt.core.content
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.fnives.test.showcase.hilt.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.ContentId
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.doThrow
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
@Suppress("TestFunctionName")
@OptIn(ExperimentalCoroutinesApi::class)
internal class RemoveContentFromFavouritesUseCaseTest {
private lateinit var sut: RemoveContentFromFavouritesUseCase
private lateinit var mockFavouriteContentLocalStorage: FavouriteContentLocalStorage
@BeforeEach
fun setUp() {
mockFavouriteContentLocalStorage = mock()
sut = RemoveContentFromFavouritesUseCase(mockFavouriteContentLocalStorage)
}
@DisplayName("WHEN nothing happens THEN the storage is not touched")
@Test
fun initializationDoesntAffectStorage() {
verifyNoInteractions(mockFavouriteContentLocalStorage)
}
@DisplayName("GIVEN contentId WHEN called THEN storage is called")
@Test
fun givenContentIdCallsStorage() = runTest {
sut.invoke(ContentId("a"))
verify(mockFavouriteContentLocalStorage, times(1)).deleteAsFavourite(ContentId("a"))
verifyNoMoreInteractions(mockFavouriteContentLocalStorage)
}
@DisplayName("GIVEN throwing local storage WHEN thrown THEN its propogated")
@Test
fun storageExceptionThrowingIsPropogated() = runTest {
whenever(mockFavouriteContentLocalStorage.deleteAsFavourite(ContentId("a"))).doThrow(RuntimeException())
Assertions.assertThrows(RuntimeException::class.java) {
runBlocking { sut.invoke(ContentId("a")) }
}
}
}

View file

@ -0,0 +1,167 @@
package org.fnives.test.showcase.hilt.core.content
import app.cash.turbine.test
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
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.hilt.core.shared.UnexpectedException
import org.fnives.test.showcase.hilt.network.content.ContentRemoteSource
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.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
@OptIn(ExperimentalCoroutinesApi::class)
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,230 @@
package org.fnives.test.showcase.hilt.core.content
import app.cash.turbine.test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.fnives.test.showcase.hilt.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
@OptIn(ExperimentalCoroutinesApi::class)
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())
}
}
}

View file

@ -0,0 +1,35 @@
package org.fnives.test.showcase.hilt.core.di
import dagger.BindsInstance
import dagger.Component
import org.fnives.test.showcase.hilt.core.login.LogoutUseCaseTest
import org.fnives.test.showcase.hilt.core.session.SessionExpirationListener
import org.fnives.test.showcase.hilt.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.hilt.network.di.BindsBaseOkHttpClient
import org.fnives.test.showcase.hilt.network.di.HiltNetworkModule
import javax.inject.Singleton
@Singleton
@Component(modules = [CoreModule::class, HiltNetworkModule::class, ReloadLoggedInModuleInjectModuleImpl::class, BindsBaseOkHttpClient::class])
internal interface TestCoreComponent {
@Component.Builder
interface Builder {
@BindsInstance
fun setBaseUrl(baseUrl: String): Builder
@BindsInstance
fun setEnableLogging(enableLogging: Boolean): Builder
@BindsInstance
fun setSessionExpirationListener(listener: SessionExpirationListener): Builder
@BindsInstance
fun setUserDataLocalStorage(storage: UserDataLocalStorage): Builder
fun build(): TestCoreComponent
}
fun inject(logoutUseCaseTest: LogoutUseCaseTest)
}

View file

@ -0,0 +1,66 @@
package org.fnives.test.showcase.hilt.core.login
import org.fnives.test.showcase.hilt.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.model.session.Session
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.verifyNoInteractions
import org.mockito.kotlin.whenever
@Suppress("TestFunctionName")
internal class IsUserLoggedInUseCaseTest {
private lateinit var sut: IsUserLoggedInUseCase
private lateinit var mockUserDataLocalStorage: UserDataLocalStorage
@BeforeEach
fun setUp() {
mockUserDataLocalStorage = mock()
sut = IsUserLoggedInUseCase(mockUserDataLocalStorage)
}
@DisplayName("WHEN nothing is called THEN storage is not called")
@Test
fun creatingDoesntAffectStorage() {
verifyNoInteractions(mockUserDataLocalStorage)
}
@DisplayName("GIVEN session data saved WHEN is user logged in checked THEN true is returned")
@Test
fun sessionInStorageResultsInLoggedIn() {
whenever(mockUserDataLocalStorage.session).doReturn(Session("a", "b"))
val actual = sut.invoke()
Assertions.assertEquals(true, actual)
}
@DisplayName("GIVEN no session data saved WHEN is user logged in checked THEN false is returned")
@Test
fun noSessionInStorageResultsInLoggedOut() {
whenever(mockUserDataLocalStorage.session).doReturn(null)
val actual = sut.invoke()
Assertions.assertEquals(false, actual)
}
@DisplayName("GIVEN no session THEN session THEN no session WHEN is user logged in checked over again THEN every return is correct")
@Test
fun multipleSessionSettingsResultsInCorrectResponses() {
whenever(mockUserDataLocalStorage.session).doReturn(null)
val actual1 = sut.invoke()
whenever(mockUserDataLocalStorage.session).doReturn(Session("", ""))
val actual2 = sut.invoke()
whenever(mockUserDataLocalStorage.session).doReturn(null)
val actual3 = sut.invoke()
Assertions.assertEquals(false, actual1)
Assertions.assertEquals(true, actual2)
Assertions.assertEquals(false, actual3)
}
}

View file

@ -0,0 +1,105 @@
package org.fnives.test.showcase.hilt.core.login
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.fnives.test.showcase.hilt.core.shared.UnexpectedException
import org.fnives.test.showcase.hilt.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.hilt.network.auth.LoginRemoteSource
import org.fnives.test.showcase.hilt.network.auth.model.LoginStatusResponses
import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.model.auth.LoginStatus
import org.fnives.test.showcase.model.session.Session
import org.fnives.test.showcase.model.shared.Answer
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.doThrow
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
@Suppress("TestFunctionName")
@OptIn(ExperimentalCoroutinesApi::class)
internal class LoginUseCaseTest {
private lateinit var sut: LoginUseCase
private lateinit var mockLoginRemoteSource: LoginRemoteSource
private lateinit var mockUserDataLocalStorage: UserDataLocalStorage
@BeforeEach
fun setUp() {
mockLoginRemoteSource = mock()
mockUserDataLocalStorage = mock()
sut = LoginUseCase(mockLoginRemoteSource, mockUserDataLocalStorage)
}
@DisplayName("GIVEN empty username WHEN trying to login THEN invalid username is returned")
@Test
fun emptyUserNameReturnsLoginStatusError() = runTest {
val expected = Answer.Success(LoginStatus.INVALID_USERNAME)
val actual = sut.invoke(LoginCredentials("", "a"))
Assertions.assertEquals(expected, actual)
verifyNoInteractions(mockLoginRemoteSource)
verifyNoInteractions(mockUserDataLocalStorage)
}
@DisplayName("GIVEN empty password WHEN trying to login THEN invalid password is returned")
@Test
fun emptyPasswordNameReturnsLoginStatusError() = runTest {
val expected = Answer.Success(LoginStatus.INVALID_PASSWORD)
val actual = sut.invoke(LoginCredentials("a", ""))
Assertions.assertEquals(expected, actual)
verifyNoInteractions(mockLoginRemoteSource)
verifyNoInteractions(mockUserDataLocalStorage)
}
@DisplayName("GIVEN invalid credentials response WHEN trying to login THEN invalid credentials is returned ")
@Test
fun invalidLoginResponseReturnInvalidCredentials() = runTest {
val expected = Answer.Success(LoginStatus.INVALID_CREDENTIALS)
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b")))
.doReturn(LoginStatusResponses.InvalidCredentials)
val actual = sut.invoke(LoginCredentials("a", "b"))
Assertions.assertEquals(expected, actual)
verifyNoInteractions(mockUserDataLocalStorage)
}
@DisplayName("GIVEN success response WHEN trying to login THEN session is saved and success is returned")
@Test
fun validResponseResultsInSavingSessionAndSuccessReturned() = runTest {
val expected = Answer.Success(LoginStatus.SUCCESS)
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b")))
.doReturn(LoginStatusResponses.Success(Session("c", "d")))
val actual = sut.invoke(LoginCredentials("a", "b"))
Assertions.assertEquals(expected, actual)
verify(mockUserDataLocalStorage, times(1)).session = Session("c", "d")
verifyNoMoreInteractions(mockUserDataLocalStorage)
}
@DisplayName("GIVEN error response WHEN trying to login THEN session is not touched and error is returned")
@Test
fun invalidResponseResultsInErrorReturned() = runTest {
val exception = RuntimeException()
val expected = Answer.Error<LoginStatus>(UnexpectedException(exception))
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b")))
.doThrow(exception)
val actual = sut.invoke(LoginCredentials("a", "b"))
Assertions.assertEquals(expected, actual)
verifyNoInteractions(mockUserDataLocalStorage)
}
}

View file

@ -0,0 +1,71 @@
package org.fnives.test.showcase.hilt.core.login
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.fnives.test.showcase.hilt.core.content.ContentRepository
import org.fnives.test.showcase.hilt.core.di.DaggerTestCoreComponent
import org.fnives.test.showcase.hilt.core.di.TestCoreComponent
import org.fnives.test.showcase.hilt.core.storage.UserDataLocalStorage
import org.junit.jupiter.api.AfterEach
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.koin.core.context.stopKoin
import org.koin.test.KoinTest
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.verifyNoMoreInteractions
import javax.inject.Inject
@Suppress("TestFunctionName")
@OptIn(ExperimentalCoroutinesApi::class)
internal class LogoutUseCaseTest : KoinTest {
@Inject
lateinit var sut: LogoutUseCase
private lateinit var mockUserDataLocalStorage: UserDataLocalStorage
private lateinit var testCoreComponent: TestCoreComponent
@Inject
lateinit var contentRepository: ContentRepository
@BeforeEach
fun setUp() {
mockUserDataLocalStorage = mock()
testCoreComponent = DaggerTestCoreComponent.builder()
.setBaseUrl("https://a.b.com")
.setEnableLogging(true)
.setSessionExpirationListener(mock())
.setUserDataLocalStorage(mockUserDataLocalStorage)
.build()
testCoreComponent.inject(this)
}
@AfterEach
fun tearDown() {
stopKoin()
}
@DisplayName("WHEN no call THEN storage is not interacted")
@Test
fun initializedDoesntAffectStorage() {
verifyNoInteractions(mockUserDataLocalStorage)
}
@DisplayName("WHEN logout invoked THEN storage is cleared")
@Test
fun logoutResultsInStorageCleaning() = runTest {
val repositoryBefore = contentRepository
sut.invoke()
testCoreComponent.inject(this@LogoutUseCaseTest)
val repositoryAfter = contentRepository
verify(mockUserDataLocalStorage, times(1)).session = null
verifyNoMoreInteractions(mockUserDataLocalStorage)
Assertions.assertNotSame(repositoryBefore, repositoryAfter)
}
}

View file

@ -0,0 +1,38 @@
package org.fnives.test.showcase.hilt.core.session
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.verifyNoMoreInteractions
@Suppress("TestFunctionName")
internal class SessionExpirationAdapterTest {
private lateinit var sut: SessionExpirationAdapter
private lateinit var mockSessionExpirationListener: SessionExpirationListener
@BeforeEach
fun setUp() {
mockSessionExpirationListener = mock()
sut = SessionExpirationAdapter(mockSessionExpirationListener)
}
@DisplayName("WHEN nothing is changed THEN delegate is not touched")
@Test
fun verifyNoInteractionsIfNoInvocations() {
verifyNoInteractions(mockSessionExpirationListener)
}
@DisplayName("WHEN onSessionExpired is called THEN delegated is also called")
@Test
fun verifyOnSessionExpirationIsDelegated() {
sut.onSessionExpired()
verify(mockSessionExpirationListener, times(1)).onSessionExpired()
verifyNoMoreInteractions(mockSessionExpirationListener)
}
}

View file

@ -0,0 +1,90 @@
package org.fnives.test.showcase.hilt.core.shared
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.fnives.test.showcase.hilt.network.shared.exceptions.NetworkException
import org.fnives.test.showcase.hilt.network.shared.exceptions.ParsingException
import org.fnives.test.showcase.model.shared.Answer
import org.fnives.test.showcase.model.shared.Resource
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
@Suppress("TestFunctionName")
@OptIn(ExperimentalCoroutinesApi::class)
internal class AnswerUtilsKtTest {
@DisplayName("GIVEN network exception thrown WHEN wrapped into answer THEN answer error is returned")
@Test
fun networkExceptionThrownResultsInError() = runTest {
val exception = NetworkException(Throwable())
val expected = Answer.Error<Unit>(exception)
val actual = wrapIntoAnswer<Unit> { throw exception }
Assertions.assertEquals(expected, actual)
}
@DisplayName("GIVEN parsing exception thrown WHEN wrapped into answer THEN answer error is returned")
@Test
fun parsingExceptionThrownResultsInError() = runTest {
val exception = ParsingException(Throwable())
val expected = Answer.Error<Unit>(exception)
val actual = wrapIntoAnswer<Unit> { throw exception }
Assertions.assertEquals(expected, actual)
}
@DisplayName("GIVEN unexpected throwable thrown WHEN wrapped into answer THEN answer error is returned")
@Test
fun unexpectedExceptionThrownResultsInError() = runTest {
val exception = Throwable()
val expected = Answer.Error<Unit>(UnexpectedException(exception))
val actual = wrapIntoAnswer<Unit> { throw exception }
Assertions.assertEquals(expected, actual)
}
@DisplayName("GIVEN string WHEN wrapped into answer THEN string answer is returned")
@Test
fun stringIsReturnedWrappedIntoSuccess() = runTest {
val expected = Answer.Success("banan")
val actual = wrapIntoAnswer { "banan" }
Assertions.assertEquals(expected, actual)
}
@DisplayName("GIVEN cancellation exception WHEN wrapped into answer THEN cancellation exception is thrown")
@Test
fun cancellationExceptionResultsInThrowingIt() {
Assertions.assertThrows(CancellationException::class.java) {
runBlocking { wrapIntoAnswer { throw CancellationException() } }
}
}
@DisplayName("GIVEN success answer WHEN converted into resource THEN Resource success is returned")
@Test
fun successAnswerConvertsToSuccessResource() {
val expected = Resource.Success("alma")
val actual = Answer.Success("alma").mapIntoResource()
Assertions.assertEquals(expected, actual)
}
@DisplayName("GIVEN error answer WHEN converted into resource THEN Resource error is returned")
@Test
fun errorAnswerConvertsToErrorResource() {
val exception = Throwable()
val expected = Resource.Error<Unit>(exception)
val actual = Answer.Error<Unit>(exception).mapIntoResource()
Assertions.assertEquals(expected, actual)
}
}

View file

@ -0,0 +1,59 @@
package org.fnives.test.showcase.hilt.core.storage
import org.fnives.test.showcase.model.session.Session
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.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
@Suppress("TestFunctionName")
internal class NetworkSessionLocalStorageAdapterTest {
private lateinit var sut: NetworkSessionLocalStorageAdapter
private lateinit var mockUserDataLocalStorage: UserDataLocalStorage
@BeforeEach
fun setUp() {
mockUserDataLocalStorage = mock()
sut = NetworkSessionLocalStorageAdapter(mockUserDataLocalStorage)
}
@DisplayName("GIVEN null as session WHEN saved THEN its delegated")
@Test
fun settingNullSessionIsDelegated() {
sut.session = null
verify(mockUserDataLocalStorage, times(1)).session = null
verifyNoMoreInteractions(mockUserDataLocalStorage)
}
@DisplayName("GIVEN session WHEN saved THEN its delegated")
@Test
fun settingDataAsSessionIsDelegated() {
val expected = Session("a", "b")
sut.session = Session("a", "b")
verify(mockUserDataLocalStorage, times(1)).session = expected
verifyNoMoreInteractions(mockUserDataLocalStorage)
}
@DisplayName("WHEN session requested THEN its returned from delegated")
@Test
fun gettingSessionReturnsFromDelegate() {
val expected = Session("a", "b")
whenever(mockUserDataLocalStorage.session).doReturn(expected)
val actual = sut.session
Assertions.assertSame(expected, actual)
verify(mockUserDataLocalStorage, times(1)).session
verifyNoMoreInteractions(mockUserDataLocalStorage)
}
}

View file

@ -0,0 +1,24 @@
package org.fnives.test.showcase.hilt.core.testutil
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
class AwaitElementEmitCount(private var counter: Int) {
private val completableDeferred = CompletableDeferred<Unit>()
init {
assert(counter > 0)
}
fun <T> attach(flow: Flow<T>): Flow<T> =
flow.onEach {
counter--
if (counter == 0) {
completableDeferred.complete(Unit)
}
}
suspend fun await() = completableDeferred.await()
}

View file

@ -0,0 +1 @@
mock-maker-inline

View file

@ -0,0 +1,30 @@
package org.fnives.test.showcase.hilt.core.integration.fake
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import org.fnives.test.showcase.hilt.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.ContentId
class FakeFavouriteContentLocalStorage : FavouriteContentLocalStorage {
private val dataFlow = MutableSharedFlow<List<ContentId>>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
init {
dataFlow.tryEmit(emptyList())
}
override fun observeFavourites(): Flow<List<ContentId>> = dataFlow.asSharedFlow()
override suspend fun markAsFavourite(contentId: ContentId) {
dataFlow.emit(dataFlow.replayCache.first().plus(contentId))
}
override suspend fun deleteAsFavourite(contentId: ContentId) {
dataFlow.emit(dataFlow.replayCache.first().minus(contentId))
}
}

View file

@ -0,0 +1,6 @@
package org.fnives.test.showcase.hilt.core.integration.fake
import org.fnives.test.showcase.hilt.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.model.session.Session
class FakeUserDataLocalStorage(override var session: Session? = null) : UserDataLocalStorage