Issue#41 Copy full example into separate module with Hilt Integration
This commit is contained in:
parent
69e76dc0da
commit
52a99a82fc
229 changed files with 8416 additions and 11 deletions
1
hilt/hilt-core/.gitignore
vendored
Normal file
1
hilt/hilt-core/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
35
hilt/hilt-core/build.gradle
Normal file
35
hilt/hilt-core/build.gradle
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
plugins {
|
||||
id 'java-library'
|
||||
id 'kotlin'
|
||||
id 'kotlin-kapt'
|
||||
id 'java-test-fixtures'
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
|
||||
api project(":model")
|
||||
implementation project(":hilt:hilt-network")
|
||||
|
||||
applyCoreTestDependenciesTo(this)
|
||||
|
||||
// hilt
|
||||
implementation "com.google.dagger:hilt-core:$hilt_version"
|
||||
kapt "com.google.dagger:hilt-compiler:$hilt_version"
|
||||
def reloadable_module_version = "0.1.0"
|
||||
implementation "org.fnives.library.reloadable.module:annotation:$reloadable_module_version"
|
||||
kapt "org.fnives.library.reloadable.module:annotation-processor:$reloadable_module_version"
|
||||
|
||||
testImplementation project(':mockserver')
|
||||
testFixturesApi testFixtures(project(":hilt:hilt-network"))
|
||||
kaptTest "com.google.dagger:dagger-compiler:$hilt_version"
|
||||
}
|
||||
0
hilt/hilt-core/consumer-rules.pro
Normal file
0
hilt/hilt-core/consumer-rules.pro
Normal file
21
hilt/hilt-core/proguard-rules.pro
vendored
Normal file
21
hilt/hilt-core/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package org.fnives.test.showcase.hilt.core.session
|
||||
|
||||
interface SessionExpirationListener {
|
||||
fun onSessionExpired()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package org.fnives.test.showcase.hilt.core.shared
|
||||
|
||||
internal class Optional<T : Any>(val item: T?)
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
mock-maker-inline
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue