diff --git a/app/src/main/kotlin/com/random/user/navigation/RandomUsersNavHost.kt b/app/src/main/kotlin/com/random/user/navigation/RandomUsersNavHost.kt index a023dea..0f6506e 100644 --- a/app/src/main/kotlin/com/random/user/navigation/RandomUsersNavHost.kt +++ b/app/src/main/kotlin/com/random/user/navigation/RandomUsersNavHost.kt @@ -8,7 +8,6 @@ import androidx.navigation.compose.NavHost import androidx.navigation.navigation import com.random.user.navigation.viewmodel.NavigationViewModel import com.random.user.presentation.navigation.BaseNavRoutes -import com.random.user.presentation.ui.theme.RandomUsersTheme @Composable fun BaseProjectApplicationNavHost( diff --git a/core/api/build.gradle.kts b/core/api/build.gradle.kts index 99b1d20..fedf2ea 100644 --- a/core/api/build.gradle.kts +++ b/core/api/build.gradle.kts @@ -39,8 +39,6 @@ dependencies { implementation(libs.retrofit.gson) implementation(libs.okhttp) implementation(libs.okhttp.logging.interceptor) - implementation(libs.hilt.testing) - implementation(libs.mockwebserver) ksp(libs.com.google.dagger.hilt.compiler) } diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index 357174b..2681254 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -35,8 +35,6 @@ android { dependencies { implementation(libs.bundles.layer.data) implementation(libs.room.ktx) - implementation(libs.room.testing) - implementation(libs.hilt.testing) ksp(libs.com.google.dagger.hilt.compiler) ksp(libs.room.compiler) diff --git a/core/preferences/build.gradle.kts b/core/preferences/build.gradle.kts index f643b90..8d89178 100644 --- a/core/preferences/build.gradle.kts +++ b/core/preferences/build.gradle.kts @@ -34,7 +34,6 @@ android { dependencies { implementation(libs.bundles.layer.data) - implementation(libs.hilt.testing) ksp(libs.com.google.dagger.hilt.compiler) } diff --git a/core/presentation/src/main/kotlin/com/random/user/presentation/navigation/BaseNavRoutes.kt b/core/presentation/src/main/kotlin/com/random/user/presentation/navigation/BaseNavRoutes.kt index 441ad98..1a0c022 100644 --- a/core/presentation/src/main/kotlin/com/random/user/presentation/navigation/BaseNavRoutes.kt +++ b/core/presentation/src/main/kotlin/com/random/user/presentation/navigation/BaseNavRoutes.kt @@ -2,10 +2,10 @@ package com.random.user.presentation.navigation import kotlinx.serialization.Serializable -sealed class BaseNavRoutes { +sealed interface BaseNavRoutes { @Serializable - data object MainGraph : BaseNavRoutes() + data object MainGraph : BaseNavRoutes @Serializable - data object Users : BaseNavRoutes() + data object Users : BaseNavRoutes } diff --git a/core/test/.gitignore b/core/test/.gitignore deleted file mode 100644 index 42afabf..0000000 --- a/core/test/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/core/test/build.gradle.kts b/core/test/build.gradle.kts deleted file mode 100644 index c187512..0000000 --- a/core/test/build.gradle.kts +++ /dev/null @@ -1,46 +0,0 @@ -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.com.google.devtools.ksp) - alias(libs.plugins.com.google.dagger.hilt.android) -} - -android { - namespace = "${AppVersions.APPLICATION_ID}.core.test" - compileSdk = AppVersions.COMPILE_SDK - - defaultConfig { - minSdk = AppVersions.MIN_SDK - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro", - ) - } - } - compileOptions { - sourceCompatibility = AppVersions.javaVersion - targetCompatibility = AppVersions.javaVersion - } - kotlinOptions { - jvmTarget = AppVersions.JVM_TARGET - } - testOptions { - unitTests { - isIncludeAndroidResources = true - } - } -} - -dependencies { - implementation(libs.bundles.layer.data) - ksp(libs.com.google.dagger.hilt.compiler) - implementation(libs.bundles.test.compose) - - testImplementation(libs.bundles.test.unit) -} diff --git a/core/test/proguard-rules.pro b/core/test/proguard-rules.pro deleted file mode 100644 index 481bb43..0000000 --- a/core/test/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# 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 \ No newline at end of file diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 65b0081..3362d4d 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -36,9 +36,6 @@ dependencies { implementation(libs.bundles.layer.data) implementation(libs.arrow.core.retrofit) - implementation(libs.hilt.testing) - implementation(libs.test.corutines) - ksp(libs.com.google.dagger.hilt.compiler) implementation(project(":domain")) diff --git a/data/src/test/kotlin/com/random/users/data/mother/RandomUsersResponseMother.kt b/data/src/test/kotlin/com/random/users/data/mother/RandomUsersResponseMother.kt index e9fd638..22b0d32 100644 --- a/data/src/test/kotlin/com/random/users/data/mother/RandomUsersResponseMother.kt +++ b/data/src/test/kotlin/com/random/users/data/mother/RandomUsersResponseMother.kt @@ -13,14 +13,14 @@ import com.random.users.api.model.StreetDto import com.random.users.api.model.TimezoneDto import com.random.users.api.model.UserDto -object RandomUsersResponseMother { +internal object RandomUsersResponseMother { fun createModel( results: List = listOf(UserDtoMother.createModel()), info: ResponseInfoDto = ResponseInfoDtoMother.createModel(), ): RandomUserResponse = RandomUserResponse(results, info) } -object ResponseInfoDtoMother { +internal object ResponseInfoDtoMother { fun createModel( seed: String = "default-seed", results: Int = 10, @@ -35,7 +35,7 @@ object ResponseInfoDtoMother { ) } -object UserDtoMother { +internal object UserDtoMother { fun createModel( gender: String = "male", name: NameDto = NameDtoMother.createModel(), @@ -66,7 +66,7 @@ object UserDtoMother { ) } -object NameDtoMother { +private object NameDtoMother { fun createModel( title: String = "Mr", first: String = "John", @@ -74,7 +74,7 @@ object NameDtoMother { ): NameDto = NameDto(title, first, last) } -object LocationDtoMother { +private object LocationDtoMother { fun createModel( street: StreetDto = StreetDtoMother.createModel(), city: String = "New York", @@ -86,28 +86,28 @@ object LocationDtoMother { ): LocationDto = LocationDto(street, city, state, country, postcode, coordinates, timezone) } -object StreetDtoMother { +private object StreetDtoMother { fun createModel( number: Int = 123, name: String = "Main Street", ): StreetDto = StreetDto(number, name) } -object CoordinatesDtoMother { +private object CoordinatesDtoMother { fun createModel( latitude: String = "40.7128", longitude: String = "-74.0060", ): CoordinatesDto = CoordinatesDto(latitude, longitude) } -object TimezoneDtoMother { +private object TimezoneDtoMother { fun createModel( offset: String = "-05:00", description: String = "Eastern Time (US & Canada)", ): TimezoneDto = TimezoneDto(offset, description) } -object LoginDtoMother { +private object LoginDtoMother { fun createModel( uuid: String = "mock-uuid", username: String = "johndoe", @@ -119,21 +119,21 @@ object LoginDtoMother { ): LoginDto = LoginDto(uuid, username, password, salt, md5, sha1, sha256) } -object DateInfoDtoMother { +private object DateInfoDtoMother { fun createModel( date: String = "2000-01-01T00:00:00Z", age: Int = 21, ): DateInfoDto = DateInfoDto(date, age) } -object IdDtoMother { +private object IdDtoMother { fun createModel( name: String = "SSN", value: String = "123-45-6789", ): IdDto = IdDto(name, value) } -object PictureDtoMother { +private object PictureDtoMother { fun createModel( large: String = "https://example.com/large.jpg", medium: String = "https://example.com/medium.jpg", diff --git a/data/src/test/kotlin/com/random/users/data/repository/UsersRepositoryUnitTest.kt b/data/src/test/kotlin/com/random/users/data/repository/UsersRepositoryUnitTest.kt index 2101987..a3c8f22 100644 --- a/data/src/test/kotlin/com/random/users/data/repository/UsersRepositoryUnitTest.kt +++ b/data/src/test/kotlin/com/random/users/data/repository/UsersRepositoryUnitTest.kt @@ -19,7 +19,7 @@ import org.junit.Assert import org.junit.Before import org.junit.Test -class UsersRepositoryUnitTest { +internal class UsersRepositoryUnitTest { private lateinit var usersRepository: UsersRepository private val usersLocalDataSource: UsersLocalDataSource = mockk() private val usersRemoteDataSource: UsersRemoteDataSource = mockk() diff --git a/domain/src/test/kotlin/com/random/users/domain/mother/UserMother.kt b/domain/src/test/kotlin/com/random/users/domain/mother/UserMother.kt index de74db6..5060f5e 100644 --- a/domain/src/test/kotlin/com/random/users/domain/mother/UserMother.kt +++ b/domain/src/test/kotlin/com/random/users/domain/mother/UserMother.kt @@ -6,7 +6,7 @@ import com.random.users.domain.models.UserName import com.random.users.domain.models.UserPicture import com.random.users.domain.models.UserStreet -object UserMother { +internal object UserMother { fun createModel( uuid: String = "mock-uuid", name: UserName = createUserName(), @@ -26,23 +26,23 @@ object UserMother { picture = picture, ) - fun createUserName( + private fun createUserName( first: String = "John", last: String = "Doe", ): UserName = UserName(first, last) - fun createUserLocation( + private fun createUserLocation( street: UserStreet = createUserStreet(), city: String = "Madrid", state: String = "Madrid", ): UserLocation = UserLocation(street, city, state) - fun createUserStreet( + private fun createUserStreet( number: Int = 123, name: String = "Calle Mayor", ): UserStreet = UserStreet(number, name) - fun createUserPicture( + private fun createUserPicture( medium: String = "https://example.com/medium.jpg", thumbnail: String = "https://example.com/thumbnail.jpg", ): UserPicture = UserPicture(medium, thumbnail) diff --git a/domain/src/test/kotlin/com/random/users/domain/usecase/GetUserListUseCaseUnitTest.kt b/domain/src/test/kotlin/com/random/users/domain/usecase/GetUserListUseCaseUnitTest.kt index 32a1d6a..dab0780 100644 --- a/domain/src/test/kotlin/com/random/users/domain/usecase/GetUserListUseCaseUnitTest.kt +++ b/domain/src/test/kotlin/com/random/users/domain/usecase/GetUserListUseCaseUnitTest.kt @@ -14,7 +14,7 @@ import org.junit.Assert import org.junit.Before import org.junit.Test -class GetUserListUseCaseUnitTest { +internal class GetUserListUseCaseUnitTest { private lateinit var usersRepository: UsersRepository private lateinit var getUserListUseCase: GetUserListUseCase diff --git a/presentation/users/build.gradle.kts b/presentation/users/build.gradle.kts index c686ab0..aff3ebb 100644 --- a/presentation/users/build.gradle.kts +++ b/presentation/users/build.gradle.kts @@ -61,8 +61,17 @@ dependencies { implementation(project(":core:presentation")) implementation(project(":domain")) - testImplementation(project(":core:test")) + kspTest(libs.com.google.dagger.hilt.compiler) testImplementation(project(":data")) + testImplementation(project(":core:preferences")) + testImplementation(project(":core:database")) + testImplementation(project(":core:api")) + testImplementation(libs.arrow.core.retrofit) + testImplementation(libs.retrofit) + testImplementation(libs.retrofit.gson) + testImplementation(libs.okhttp) + testImplementation(libs.okhttp.logging.interceptor) + testImplementation(libs.room.testing) testImplementation(libs.bundles.test.unit) testImplementation(libs.bundles.test.compose) } diff --git a/presentation/users/src/main/kotlin/com/random/users/users/composable/UserList.kt b/presentation/users/src/main/kotlin/com/random/users/users/composable/UserList.kt index 47b3f69..270a2cc 100644 --- a/presentation/users/src/main/kotlin/com/random/users/users/composable/UserList.kt +++ b/presentation/users/src/main/kotlin/com/random/users/users/composable/UserList.kt @@ -37,6 +37,7 @@ import com.random.user.presentation.ui.theme.RandomUsersTheme import com.random.users.users.contract.UserUiState import com.random.users.users.contract.UsersScreenUiState import com.random.users.users.model.UserUiModel +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter @@ -61,7 +62,7 @@ internal fun UserList( snapshotFlow { reachedBottom } .distinctUntilChanged() .filter { it && state.contentState is UsersScreenUiState.ContentState.Idle } - .collect { onLoadUsers() } + .collectLatest { onLoadUsers() } } LaunchedEffect( diff --git a/presentation/users/src/main/kotlin/com/random/users/users/composable/UserSearchView.kt b/presentation/users/src/main/kotlin/com/random/users/users/composable/UserSearchView.kt index cf4b930..18b1522 100644 --- a/presentation/users/src/main/kotlin/com/random/users/users/composable/UserSearchView.kt +++ b/presentation/users/src/main/kotlin/com/random/users/users/composable/UserSearchView.kt @@ -9,13 +9,20 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.PreviewLightDark import com.random.user.presentation.ui.theme.RandomUsersTheme +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +@OptIn(FlowPreview::class) @Composable internal fun UserSearchView( modifier: Modifier = Modifier, @@ -24,6 +31,15 @@ internal fun UserSearchView( ) { val searchState = rememberSaveable { mutableStateOf(search) } + LaunchedEffect( + searchState.value, + ) { + snapshotFlow { searchState.value } + .distinctUntilChanged() + .debounce(200) + .collectLatest { onValueChange(it) } + } + TextField( modifier = modifier.fillMaxWidth().testTag("searchField"), value = searchState.value, diff --git a/presentation/users/src/main/kotlin/com/random/users/users/contract/UsersScreenContract.kt b/presentation/users/src/main/kotlin/com/random/users/users/contract/UsersScreenContract.kt index 33651bd..fa9112d 100644 --- a/presentation/users/src/main/kotlin/com/random/users/users/contract/UsersScreenContract.kt +++ b/presentation/users/src/main/kotlin/com/random/users/users/contract/UsersScreenContract.kt @@ -32,22 +32,22 @@ internal data class UserUiState( } } -internal sealed interface UsersEvent { - data object OnLoadUsers : UsersEvent +internal sealed interface UsersUiEvent { + data object OnLoadUsers : UsersUiEvent data class OnFilterUsers( val filterText: String, - ) : UsersEvent + ) : UsersUiEvent data class OnDeleteUser( val uuid: String, - ) : UsersEvent + ) : UsersUiEvent } -internal sealed class UsersErrorUiEventsState { - data object DeleteError : UsersErrorUiEventsState() +internal sealed interface UsersErrorUiState { + data object DeleteError : UsersErrorUiState - data object LoadUsersError : UsersErrorUiEventsState() + data object LoadUsersError : UsersErrorUiState - data object UnknownError : UsersErrorUiEventsState() + data object UnknownError : UsersErrorUiState } diff --git a/presentation/users/src/main/kotlin/com/random/users/users/mapper/UsersErrorsMapper.kt b/presentation/users/src/main/kotlin/com/random/users/users/mapper/UsersErrorsMapper.kt index 67ef8bd..a32efad 100644 --- a/presentation/users/src/main/kotlin/com/random/users/users/mapper/UsersErrorsMapper.kt +++ b/presentation/users/src/main/kotlin/com/random/users/users/mapper/UsersErrorsMapper.kt @@ -1,13 +1,13 @@ package com.random.users.users.mapper import com.random.users.domain.models.UsersErrors -import com.random.users.users.contract.UsersErrorUiEventsState +import com.random.users.users.contract.UsersErrorUiState internal object UsersErrorsMapper { fun UsersErrors.toUiError() = when (this) { - is UsersErrors.NetworkError -> UsersErrorUiEventsState.LoadUsersError - is UsersErrors.UserError -> UsersErrorUiEventsState.DeleteError - else -> UsersErrorUiEventsState.UnknownError + is UsersErrors.NetworkError -> UsersErrorUiState.LoadUsersError + is UsersErrors.UserError -> UsersErrorUiState.DeleteError + else -> UsersErrorUiState.UnknownError } } diff --git a/presentation/users/src/main/kotlin/com/random/users/users/navigation/UsersRoute.kt b/presentation/users/src/main/kotlin/com/random/users/users/navigation/UsersRoute.kt index 423899f..2e60d5f 100644 --- a/presentation/users/src/main/kotlin/com/random/users/users/navigation/UsersRoute.kt +++ b/presentation/users/src/main/kotlin/com/random/users/users/navigation/UsersRoute.kt @@ -3,12 +3,12 @@ package com.random.users.users.navigation import com.random.users.users.model.UserUiModel import kotlinx.serialization.Serializable -internal sealed class UsersRoute { +internal sealed interface UsersRoute { @Serializable - data object Home : UsersRoute() + data object Home : UsersRoute @Serializable data class UserDetail( val user: UserUiModel, - ) : UsersRoute() + ) : UsersRoute } diff --git a/presentation/users/src/main/kotlin/com/random/users/users/screen/UsersScreen.kt b/presentation/users/src/main/kotlin/com/random/users/users/screen/UsersScreen.kt index 194d3f4..12c2386 100644 --- a/presentation/users/src/main/kotlin/com/random/users/users/screen/UsersScreen.kt +++ b/presentation/users/src/main/kotlin/com/random/users/users/screen/UsersScreen.kt @@ -24,8 +24,8 @@ import com.random.user.presentation.ui.theme.RandomUsersTheme import com.random.users.users.composable.UserList import com.random.users.users.composable.UserSearchView import com.random.users.users.contract.UserUiState -import com.random.users.users.contract.UsersErrorUiEventsState -import com.random.users.users.contract.UsersEvent +import com.random.users.users.contract.UsersErrorUiState +import com.random.users.users.contract.UsersUiEvent import com.random.users.users.contract.UsersScreenUiState import com.random.users.users.model.UserUiModel import com.random.users.users.navigation.UsersRoute @@ -45,9 +45,9 @@ internal fun UsersScreen( UsersContent( modifier = Modifier.padding(innerPadding), state = state, - onDeleteUser = { viewModel.handleEvent(UsersEvent.OnDeleteUser(uuid = it)) }, - onLoadUsers = { viewModel.handleEvent(UsersEvent.OnLoadUsers) }, - onFilterUsers = { viewModel.handleEvent(UsersEvent.OnFilterUsers(filterText = it)) }, + onDeleteUser = { viewModel.handleEvent(UsersUiEvent.OnDeleteUser(uuid = it)) }, + onLoadUsers = { viewModel.handleEvent(UsersUiEvent.OnLoadUsers) }, + onFilterUsers = { viewModel.handleEvent(UsersUiEvent.OnFilterUsers(filterText = it)) }, onUserClick = { navController.navigate(UsersRoute.UserDetail(user = it)) }, ) } @@ -81,7 +81,7 @@ private fun UsersContent( } @Composable -private fun HandleOneTimeEvents(uiEventsState: Flow) { +private fun HandleOneTimeEvents(uiEventsState: Flow) { val lifecycle = LocalLifecycleOwner.current.lifecycle val context = LocalContext.current LaunchedEffect(uiEventsState) { @@ -94,15 +94,15 @@ private fun HandleOneTimeEvents(uiEventsState: Flow) { } private fun showError( - state: UsersErrorUiEventsState, + state: UsersErrorUiState, context: Context, ) { when (state) { - is UsersErrorUiEventsState.DeleteError -> { + is UsersErrorUiState.DeleteError -> { Toast.makeText(context, "Error deleting user", Toast.LENGTH_SHORT).show() } - is UsersErrorUiEventsState.LoadUsersError -> { + is UsersErrorUiState.LoadUsersError -> { Toast.makeText(context, "Error loading users", Toast.LENGTH_SHORT).show() } diff --git a/presentation/users/src/main/kotlin/com/random/users/users/viewmodel/UsersViewModel.kt b/presentation/users/src/main/kotlin/com/random/users/users/viewmodel/UsersViewModel.kt index 3dff250..40f7ff3 100644 --- a/presentation/users/src/main/kotlin/com/random/users/users/viewmodel/UsersViewModel.kt +++ b/presentation/users/src/main/kotlin/com/random/users/users/viewmodel/UsersViewModel.kt @@ -5,8 +5,8 @@ import androidx.lifecycle.viewModelScope import com.random.users.domain.usecase.DeleteUserUseCase import com.random.users.domain.usecase.GetUserListUseCase import com.random.users.users.contract.UserUiState -import com.random.users.users.contract.UsersErrorUiEventsState -import com.random.users.users.contract.UsersEvent +import com.random.users.users.contract.UsersErrorUiState +import com.random.users.users.contract.UsersUiEvent import com.random.users.users.contract.UsersScreenUiState import com.random.users.users.mapper.UsersErrorsMapper.toUiError import com.random.users.users.mapper.toUiState @@ -32,23 +32,23 @@ internal class UsersViewModel UsersScreenUiState(), ) val uiState: StateFlow = _uiState - private val _uiEventsState = Channel(capacity = Channel.CONFLATED) - val uiEventsState: Flow = _uiEventsState.receiveAsFlow() + private val _uiEventsState = Channel(capacity = Channel.CONFLATED) + val uiEventsState: Flow = _uiEventsState.receiveAsFlow() private var currentPage: Int = 0 private var userList: List = emptyList() - fun handleEvent(event: UsersEvent) { + fun handleEvent(event: UsersUiEvent) { when (event) { - is UsersEvent.OnLoadUsers -> { + is UsersUiEvent.OnLoadUsers -> { loadUsers() } - is UsersEvent.OnDeleteUser -> { + is UsersUiEvent.OnDeleteUser -> { deleteUser(uuid = event.uuid) } - is UsersEvent.OnFilterUsers -> { + is UsersUiEvent.OnFilterUsers -> { filterUsers(event.filterText) } } diff --git a/core/api/src/main/kotlin/com/random/users/api/di/TestApiModule.kt b/presentation/users/src/test/kotlin/com/random/users/users/di/TestApiModule.kt similarity index 75% rename from core/api/src/main/kotlin/com/random/users/api/di/TestApiModule.kt rename to presentation/users/src/test/kotlin/com/random/users/users/di/TestApiModule.kt index f2e6ea2..00e6f93 100644 --- a/core/api/src/main/kotlin/com/random/users/api/di/TestApiModule.kt +++ b/presentation/users/src/test/kotlin/com/random/users/users/di/TestApiModule.kt @@ -1,14 +1,14 @@ -package com.random.users.api.di +package com.random.users.users.di import arrow.retrofit.adapter.either.EitherCallAdapterFactory import com.random.users.api.api.UsersApi +import com.random.users.api.di.ApiModule import dagger.Module import dagger.Provides import dagger.hilt.components.SingletonComponent import dagger.hilt.testing.TestInstallIn import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor -import okhttp3.mockwebserver.MockWebServer import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import javax.inject.Singleton @@ -36,21 +36,12 @@ object TestApiModule { @Provides @Singleton - fun provideRetrofit( - okHttpClient: OkHttpClient, - mockWebServer: MockWebServer, - ): Retrofit { - mockWebServer.start() - return Retrofit + fun provideRetrofit(okHttpClient: OkHttpClient) = + Retrofit .Builder() - .baseUrl(mockWebServer.url("/")) + .baseUrl("http://localhost:8080/") .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(EitherCallAdapterFactory.create()) .client(okHttpClient) .build() - } - - @Provides - @Singleton - fun provideMockWebServer(): MockWebServer = MockWebServer() } diff --git a/core/database/src/main/kotlin/com/random/users/database/di/TestDatabaseModule.kt b/presentation/users/src/test/kotlin/com/random/users/users/di/TestDatabaseModule.kt similarity index 90% rename from core/database/src/main/kotlin/com/random/users/database/di/TestDatabaseModule.kt rename to presentation/users/src/test/kotlin/com/random/users/users/di/TestDatabaseModule.kt index fb761ad..1468205 100644 --- a/core/database/src/main/kotlin/com/random/users/database/di/TestDatabaseModule.kt +++ b/presentation/users/src/test/kotlin/com/random/users/users/di/TestDatabaseModule.kt @@ -1,9 +1,10 @@ -package com.random.users.database.di +package com.random.users.users.di import android.content.Context import androidx.room.Room import com.random.users.database.RandomUsersDatabase import com.random.users.database.dao.UserDao +import com.random.users.database.di.DatabaseModule import dagger.Module import dagger.Provides import dagger.hilt.android.qualifiers.ApplicationContext @@ -23,7 +24,6 @@ object TestDatabaseModule { @ApplicationContext appContext: Context, ) = Room .inMemoryDatabaseBuilder(appContext, RandomUsersDatabase::class.java) - .allowMainThreadQueries() .build() @Provides diff --git a/data/src/main/kotlin/com/random/users/data/di/TestDispatchersModule.kt b/presentation/users/src/test/kotlin/com/random/users/users/di/TestDispatchersModule.kt similarity index 86% rename from data/src/main/kotlin/com/random/users/data/di/TestDispatchersModule.kt rename to presentation/users/src/test/kotlin/com/random/users/users/di/TestDispatchersModule.kt index 93ba312..c852705 100644 --- a/data/src/main/kotlin/com/random/users/data/di/TestDispatchersModule.kt +++ b/presentation/users/src/test/kotlin/com/random/users/users/di/TestDispatchersModule.kt @@ -1,5 +1,6 @@ -package com.random.users.data.di +package com.random.users.users.di +import com.random.users.data.di.DispatchersModule import dagger.Module import dagger.Provides import dagger.hilt.components.SingletonComponent diff --git a/core/preferences/src/main/kotlin/com/random/users/preferences/di/TestPreferencesModule.kt b/presentation/users/src/test/kotlin/com/random/users/users/di/TestPreferencesModule.kt similarity index 91% rename from core/preferences/src/main/kotlin/com/random/users/preferences/di/TestPreferencesModule.kt rename to presentation/users/src/test/kotlin/com/random/users/users/di/TestPreferencesModule.kt index 58d4933..4283352 100644 --- a/core/preferences/src/main/kotlin/com/random/users/preferences/di/TestPreferencesModule.kt +++ b/presentation/users/src/test/kotlin/com/random/users/users/di/TestPreferencesModule.kt @@ -1,7 +1,8 @@ -package com.random.users.preferences.di +package com.random.users.users.di import android.content.Context import android.content.SharedPreferences +import com.random.users.preferences.di.PreferencesModule import com.random.users.preferences.manager.PreferencesManager import dagger.Module import dagger.Provides diff --git a/core/test/src/main/kotlin/com/random/users/test/model/getUsersListResponseJson.kt b/presentation/users/src/test/kotlin/com/random/users/users/model/getUsersListResponseJson.kt similarity index 99% rename from core/test/src/main/kotlin/com/random/users/test/model/getUsersListResponseJson.kt rename to presentation/users/src/test/kotlin/com/random/users/users/model/getUsersListResponseJson.kt index 61a2dbc..c2956c1 100644 --- a/core/test/src/main/kotlin/com/random/users/test/model/getUsersListResponseJson.kt +++ b/presentation/users/src/test/kotlin/com/random/users/users/model/getUsersListResponseJson.kt @@ -1,4 +1,4 @@ -package com.random.users.test.model +package com.random.users.users.model val getUserListResponsePage1Json = """ diff --git a/presentation/users/src/test/kotlin/com/random/users/users/mother/UserMother.kt b/presentation/users/src/test/kotlin/com/random/users/users/mother/UserMother.kt index 7be98bc..43762a5 100644 --- a/presentation/users/src/test/kotlin/com/random/users/users/mother/UserMother.kt +++ b/presentation/users/src/test/kotlin/com/random/users/users/mother/UserMother.kt @@ -26,23 +26,23 @@ internal object UserMother { picture = picture, ) - fun createUserName( + private fun createUserName( first: String = "John", last: String = "Doe", ): UserName = UserName(first, last) - fun createUserLocation( + private fun createUserLocation( street: UserStreet = createUserStreet(), city: String = "Madrid", state: String = "Madrid", ): UserLocation = UserLocation(street, city, state) - fun createUserStreet( + private fun createUserStreet( number: Int = 123, name: String = "Calle Mayor", ): UserStreet = UserStreet(number, name) - fun createUserPicture( + private fun createUserPicture( medium: String = "https://example.com/medium.jpg", thumbnail: String = "https://example.com/thumbnail.jpg", ): UserPicture = UserPicture(medium, thumbnail) diff --git a/core/test/src/main/kotlin/com/random/users/test/rules/DispatcherRules.kt b/presentation/users/src/test/kotlin/com/random/users/users/rules/DispatcherRules.kt similarity index 95% rename from core/test/src/main/kotlin/com/random/users/test/rules/DispatcherRules.kt rename to presentation/users/src/test/kotlin/com/random/users/users/rules/DispatcherRules.kt index a79dd0f..dc13154 100644 --- a/core/test/src/main/kotlin/com/random/users/test/rules/DispatcherRules.kt +++ b/presentation/users/src/test/kotlin/com/random/users/users/rules/DispatcherRules.kt @@ -1,4 +1,4 @@ -package com.random.users.test.rules +package com.random.users.users.rules import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/core/test/src/main/kotlin/com/random/users/test/rules/RoborazziRules.kt b/presentation/users/src/test/kotlin/com/random/users/users/rules/RoborrazziRules.kt similarity index 97% rename from core/test/src/main/kotlin/com/random/users/test/rules/RoborazziRules.kt rename to presentation/users/src/test/kotlin/com/random/users/users/rules/RoborrazziRules.kt index ea10a90..c534076 100644 --- a/core/test/src/main/kotlin/com/random/users/test/rules/RoborazziRules.kt +++ b/presentation/users/src/test/kotlin/com/random/users/users/rules/RoborrazziRules.kt @@ -1,4 +1,4 @@ -package com.random.users.test.rules +package com.random.users.users.rules import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule diff --git a/presentation/users/src/test/kotlin/com/random/users/users/screenshot/UserDetailScreenshotTest.kt b/presentation/users/src/test/kotlin/com/random/users/users/screenshot/UserDetailScreenshotTest.kt index 35ef5f4..8c101b1 100644 --- a/presentation/users/src/test/kotlin/com/random/users/users/screenshot/UserDetailScreenshotTest.kt +++ b/presentation/users/src/test/kotlin/com/random/users/users/screenshot/UserDetailScreenshotTest.kt @@ -1,16 +1,15 @@ package com.random.users.users.screenshot -import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.compose.ui.test.onRoot import androidx.navigation.compose.rememberNavController import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziRule import com.github.takahirom.roborazzi.captureRoboImage -import com.random.users.test.rules.MainDispatcherRule -import com.random.users.test.rules.createRoborazziRule -import com.random.users.test.rules.createScreenshotTestComposeRule +import com.random.users.users.rules.MainDispatcherRule +import com.random.users.users.rules.createRoborazziRule +import com.random.users.users.rules.createScreenshotTestComposeRule import com.random.users.users.mapper.toUiModel import com.random.users.users.mother.UserMother import com.random.users.users.screen.UserDetailScreen @@ -31,9 +30,6 @@ import kotlin.test.Test sdk = [34], ) internal class UserDetailScreenshotTest { - @get:Rule - val instantRule = InstantTaskExecutorRule() - @get:Rule val composeTestRule = createScreenshotTestComposeRule() diff --git a/presentation/users/src/test/kotlin/com/random/users/users/screenshot/UsersScreenshotTest.kt b/presentation/users/src/test/kotlin/com/random/users/users/screenshot/UsersScreenshotTest.kt index 3047f3b..9230744 100644 --- a/presentation/users/src/test/kotlin/com/random/users/users/screenshot/UsersScreenshotTest.kt +++ b/presentation/users/src/test/kotlin/com/random/users/users/screenshot/UsersScreenshotTest.kt @@ -1,7 +1,6 @@ package com.random.users.users.screenshot -import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.compose.ui.test.click import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot @@ -16,9 +15,9 @@ import com.github.takahirom.roborazzi.captureRoboImage import com.random.users.domain.models.UsersErrors import com.random.users.domain.usecase.DeleteUserUseCase import com.random.users.domain.usecase.GetUserListUseCase -import com.random.users.test.rules.MainDispatcherRule -import com.random.users.test.rules.createRoborazziRule -import com.random.users.test.rules.createScreenshotTestComposeRule +import com.random.users.users.rules.MainDispatcherRule +import com.random.users.users.rules.createRoborazziRule +import com.random.users.users.rules.createScreenshotTestComposeRule import com.random.users.users.mother.UserMother import com.random.users.users.screen.UsersScreen import com.random.users.users.viewmodel.UsersViewModel @@ -43,16 +42,13 @@ import kotlin.test.Test sdk = [34], ) internal class UsersScreenshotTest { - @get:Rule(order = 1) - var instantRule: TestRule = InstantTaskExecutorRule() - - @get:Rule(order = 2) + @get:Rule(order = 0) var mainRule: TestRule = MainDispatcherRule() - @get:Rule(order = 3) + @get:Rule(order = 1) val composeTestRule = createScreenshotTestComposeRule() - @get:Rule(order = 4) + @get:Rule(order = 2) val roborazziRule = createRoborazziRule(composeTestRule = composeTestRule, captureType = RoborazziRule.CaptureType.None) diff --git a/presentation/users/src/test/kotlin/com/random/users/users/viewmodel/UsersViewModelIntegrationTest.kt b/presentation/users/src/test/kotlin/com/random/users/users/viewmodel/UsersViewModelIntegrationTest.kt index 333fa6b..2e345cd 100644 --- a/presentation/users/src/test/kotlin/com/random/users/users/viewmodel/UsersViewModelIntegrationTest.kt +++ b/presentation/users/src/test/kotlin/com/random/users/users/viewmodel/UsersViewModelIntegrationTest.kt @@ -4,10 +4,10 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import app.cash.turbine.test import com.random.users.domain.usecase.DeleteUserUseCase import com.random.users.domain.usecase.GetUserListUseCase -import com.random.users.test.model.getUserListResponsePage1Json -import com.random.users.test.rules.MainDispatcherRule -import com.random.users.users.contract.UsersErrorUiEventsState -import com.random.users.users.contract.UsersEvent +import com.random.users.users.model.getUserListResponsePage1Json +import com.random.users.users.rules.MainDispatcherRule +import com.random.users.users.contract.UsersErrorUiState +import com.random.users.users.contract.UsersUiEvent import com.random.users.users.contract.UsersScreenUiState import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest @@ -26,7 +26,6 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import javax.inject.Inject -import kotlin.getValue import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -51,13 +50,13 @@ internal class UsersViewModelIntegrationTest { @Inject lateinit var deleteUserUseCase: DeleteUserUseCase - @Inject lateinit var mockWebServer: MockWebServer - lateinit var viewModel: UsersViewModel @Before fun setup() { + mockWebServer = MockWebServer() + mockWebServer.start(8080) hiltRule.inject() } @@ -76,7 +75,32 @@ internal class UsersViewModelIntegrationTest { initViewModel() mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(getUserListResponsePage1Json)) - viewModel.handleEvent(UsersEvent.OnLoadUsers) + viewModel.handleEvent(UsersUiEvent.OnLoadUsers) + runCurrent() + + viewModel.uiState.test { + val initialState = awaitItem() + val finalState = awaitItem() + assertTrue(initialState.contentState is UsersScreenUiState.ContentState.Loading) + assertTrue(finalState.contentState is UsersScreenUiState.ContentState.Idle) + assertTrue(finalState.users.isNotEmpty()) + expectNoEvents() + } + + mockWebServer.takeRequest().requestUrl?.let { + assertEquals("0", it.queryParameter("page")) + } + } + + @Test + fun `GIVEN getUsersListUseCase returns users WHEN delete user THEN receives users without deleted one`() = + runTest { + initViewModel() + mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(getUserListResponsePage1Json)) + + viewModel.handleEvent(UsersUiEvent.OnDeleteUser("1")) + runCurrent() + viewModel.handleEvent(UsersUiEvent.OnLoadUsers) runCurrent() viewModel.uiState.test { @@ -85,6 +109,7 @@ internal class UsersViewModelIntegrationTest { assertTrue(initialState.contentState is UsersScreenUiState.ContentState.Loading) assertTrue(finalState.contentState is UsersScreenUiState.ContentState.Idle) assertTrue(finalState.users.isNotEmpty()) + assertTrue(finalState.users.find { it.user.uuid == "1" } == null) expectNoEvents() } } @@ -95,7 +120,7 @@ internal class UsersViewModelIntegrationTest { initViewModel() mockWebServer.enqueue(MockResponse().setResponseCode(500)) - viewModel.handleEvent(UsersEvent.OnLoadUsers) + viewModel.handleEvent(UsersUiEvent.OnLoadUsers) runCurrent() viewModel.uiState.test { @@ -107,7 +132,7 @@ internal class UsersViewModelIntegrationTest { } viewModel.uiEventsState.test { - assertEquals(UsersErrorUiEventsState.LoadUsersError, awaitItem()) + assertEquals(UsersErrorUiState.LoadUsersError, awaitItem()) expectNoEvents() } } @@ -118,12 +143,12 @@ internal class UsersViewModelIntegrationTest { initViewModel() mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(getUserListResponsePage1Json)) - viewModel.handleEvent(UsersEvent.OnLoadUsers) + viewModel.handleEvent(UsersUiEvent.OnLoadUsers) runCurrent() viewModel.uiState.test { skipItems(2) - viewModel.handleEvent(UsersEvent.OnFilterUsers("Jos")) + viewModel.handleEvent(UsersUiEvent.OnFilterUsers("Jos")) runCurrent() val newState = awaitItem() @@ -140,7 +165,7 @@ internal class UsersViewModelIntegrationTest { initViewModel() mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(getUserListResponsePage1Json)) - viewModel.handleEvent(UsersEvent.OnFilterUsers("")) + viewModel.handleEvent(UsersUiEvent.OnFilterUsers("")) runCurrent() viewModel.uiState.test { diff --git a/presentation/users/src/test/kotlin/com/random/users/users/viewmodel/UsersViewModelUnitTest.kt b/presentation/users/src/test/kotlin/com/random/users/users/viewmodel/UsersViewModelUnitTest.kt index 8953630..77faf47 100644 --- a/presentation/users/src/test/kotlin/com/random/users/users/viewmodel/UsersViewModelUnitTest.kt +++ b/presentation/users/src/test/kotlin/com/random/users/users/viewmodel/UsersViewModelUnitTest.kt @@ -5,9 +5,9 @@ import arrow.core.left import com.random.users.domain.models.UsersErrors import com.random.users.domain.usecase.DeleteUserUseCase import com.random.users.domain.usecase.GetUserListUseCase -import com.random.users.test.rules.MainDispatcherRule -import com.random.users.users.contract.UsersErrorUiEventsState -import com.random.users.users.contract.UsersEvent +import com.random.users.users.rules.MainDispatcherRule +import com.random.users.users.contract.UsersErrorUiState +import com.random.users.users.contract.UsersUiEvent import com.random.users.users.contract.UsersScreenUiState import io.mockk.coEvery import io.mockk.coVerify @@ -37,7 +37,7 @@ internal class UsersViewModelUnitTest { runTest { coEvery { deleteUserUseCase("1") } returns UsersErrors.UserError.left() - viewModel.handleEvent(UsersEvent.OnDeleteUser("1")) + viewModel.handleEvent(UsersUiEvent.OnDeleteUser("1")) runCurrent() viewModel.uiState.test { @@ -46,7 +46,7 @@ internal class UsersViewModelUnitTest { } viewModel.uiEventsState.test { - assertEquals(UsersErrorUiEventsState.DeleteError, awaitItem()) + assertEquals(UsersErrorUiState.DeleteError, awaitItem()) expectNoEvents() } diff --git a/settings.gradle.kts b/settings.gradle.kts index 21dbe74..d2edd20 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,7 +25,6 @@ include(":core:api") include(":core:database") include(":core:preferences") include(":core:presentation") -include(":core:test") include(":data") include(":domain") include(":presentation:users")