From e2b1d395312605dd332e33e3fcad1428246bf966 Mon Sep 17 00:00:00 2001 From: Juan Gaines Date: Tue, 8 Apr 2025 20:56:04 -0500 Subject: [PATCH 1/2] TestDispatchers --- .idea/.gitignore | 1 + .../testground/data/UserRepositoryTest.kt | 3 -- .../presentation/ProfileViewModelTest.kt | 54 +++++-------------- .../presentation/UserRepositoryFake.kt | 48 +++++++++++++++++ 4 files changed, 62 insertions(+), 44 deletions(-) create mode 100644 app/src/test/java/com/juandgaines/testground/presentation/UserRepositoryFake.kt diff --git a/.idea/.gitignore b/.idea/.gitignore index add5165..98fb131 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -11,3 +11,4 @@ /compiler.xml /androidTestResultsUserPreferences.xml /.name +/vcs.xml diff --git a/app/src/test/java/com/juandgaines/testground/data/UserRepositoryTest.kt b/app/src/test/java/com/juandgaines/testground/data/UserRepositoryTest.kt index 710752a..c705175 100644 --- a/app/src/test/java/com/juandgaines/testground/data/UserRepositoryTest.kt +++ b/app/src/test/java/com/juandgaines/testground/data/UserRepositoryTest.kt @@ -22,9 +22,6 @@ import retrofit2.Retrofit @OptIn(ExperimentalCoroutinesApi::class) class UserRepositoryTest { - @get:Rule - val mainDispatcherRule = MainDispatcherRule() - private lateinit var repository: UserRepositoryImpl private lateinit var api: UserApiFake private lateinit var mockWebServer: MockWebServer diff --git a/app/src/test/java/com/juandgaines/testground/presentation/ProfileViewModelTest.kt b/app/src/test/java/com/juandgaines/testground/presentation/ProfileViewModelTest.kt index c749c98..2d96c49 100644 --- a/app/src/test/java/com/juandgaines/testground/presentation/ProfileViewModelTest.kt +++ b/app/src/test/java/com/juandgaines/testground/presentation/ProfileViewModelTest.kt @@ -9,9 +9,14 @@ import com.juandgaines.testground.domain.Profile import com.juandgaines.testground.domain.User import com.juandgaines.testground.domain.UserRepository import com.juandgaines.testground.util.MainDispatcherRule +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test @@ -20,14 +25,15 @@ import java.util.UUID @OptIn(ExperimentalCoroutinesApi::class) class ProfileViewModelTest { - @get:Rule - val mainDispatcherRule = MainDispatcherRule() + private val testDispatcher = StandardTestDispatcher() private lateinit var viewModel: ProfileViewModel private lateinit var repository: UserRepositoryFake @Before fun setUp() { + Dispatchers.setMain(testDispatcher) + repository = UserRepositoryFake() viewModel = ProfileViewModel( repository = repository, @@ -39,6 +45,11 @@ class ProfileViewModelTest { ) } + @After + fun tearDown() { + Dispatchers.resetMain() + } + @Test fun givenValidUserId_whenLoadProfile_thenProfileIsLoaded() = runTest { // Act @@ -84,42 +95,3 @@ class ProfileViewModelTest { } } -class UserRepositoryFake : UserRepository { - var profileToReturn = profile() - var errorToReturn: Exception? = null - - override suspend fun getProfile(userId: String): Result { - return if (errorToReturn != null) { - Result.failure(errorToReturn!!) - } else Result.success(profileToReturn) - } - - override suspend fun getPlaces(userId: String): Result> { - return Result.success(profileToReturn.places) - } -} - -private fun user(): User { - return User( - id = UUID.randomUUID().toString(), - username = "test-user" - ) -} - -private fun place(userId: String): Place { - return Place( - id = UUID.randomUUID().toString(), - name = "test place", - coordinates = Coordinates(1.0, 1.0) - ) -} - -private fun profile(): Profile { - val user = user() - return Profile( - user = user, - places = (1..10).map { - place(user.id) - } - ) -} \ No newline at end of file diff --git a/app/src/test/java/com/juandgaines/testground/presentation/UserRepositoryFake.kt b/app/src/test/java/com/juandgaines/testground/presentation/UserRepositoryFake.kt new file mode 100644 index 0000000..4350d3d --- /dev/null +++ b/app/src/test/java/com/juandgaines/testground/presentation/UserRepositoryFake.kt @@ -0,0 +1,48 @@ +package com.juandgaines.testground.presentation + +import com.juandgaines.testground.domain.Coordinates +import com.juandgaines.testground.domain.Place +import com.juandgaines.testground.domain.Profile +import com.juandgaines.testground.domain.User +import com.juandgaines.testground.domain.UserRepository +import java.util.UUID + +class UserRepositoryFake : UserRepository { + var profileToReturn = profile() + var errorToReturn: Exception? = null + + override suspend fun getProfile(userId: String): Result { + return if (errorToReturn != null) { + Result.failure(errorToReturn!!) + } else Result.success(profileToReturn) + } + + override suspend fun getPlaces(userId: String): Result> { + return Result.success(profileToReturn.places) + } +} + +private fun user(): User { + return User( + id = UUID.randomUUID().toString(), + username = "test-user" + ) +} + +private fun place(userId: String): Place { + return Place( + id = UUID.randomUUID().toString(), + name = "test place", + coordinates = Coordinates(1.0, 1.0) + ) +} + +private fun profile(): Profile { + val user = user() + return Profile( + user = user, + places = (1..10).map { + place(user.id) + } + ) +} \ No newline at end of file From 6f2cdb5c419e7f5e40a1e81bf42724cdb89d660d Mon Sep 17 00:00:00 2001 From: Juan Gaines Date: Wed, 9 Apr 2025 15:16:06 -0500 Subject: [PATCH 2/2] navigation tests --- .../presentation/ProfileViewModelTest.kt | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 app/src/test/java/com/juandgaines/testground/presentation/ProfileViewModelTest.kt diff --git a/app/src/test/java/com/juandgaines/testground/presentation/ProfileViewModelTest.kt b/app/src/test/java/com/juandgaines/testground/presentation/ProfileViewModelTest.kt new file mode 100644 index 0000000..e3f3a82 --- /dev/null +++ b/app/src/test/java/com/juandgaines/testground/presentation/ProfileViewModelTest.kt @@ -0,0 +1,88 @@ +package com.juandgaines.testground.presentation + +import androidx.lifecycle.SavedStateHandle +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class ProfileViewModelTest { + + private val testDispatcher = StandardTestDispatcher() + + private lateinit var viewModel: ProfileViewModel + private lateinit var repository: UserRepositoryFake + + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + + repository = UserRepositoryFake() + viewModel = ProfileViewModel( + repository = repository, + savedStateHandle = SavedStateHandle( + initialState = mapOf( + "userId" to repository.profileToReturn.user.id + ) + ) + ) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun givenValidUserId_whenLoadProfile_thenProfileIsLoaded() = runTest { + // Act + viewModel.loadProfile() + advanceUntilIdle() + + // Assert + assertThat(viewModel.state.value.profile).isEqualTo(repository.profileToReturn) + assertThat(viewModel.state.value.isLoading).isFalse() + } + + @Test + fun givenRepositoryError_whenLoadProfile_thenErrorStateIsSet() = runTest { + // Arrange + repository.errorToReturn = Exception("Test exception") + + // Act + viewModel.loadProfile() + advanceUntilIdle() + + // Assert + assertThat(viewModel.state.value.profile).isNull() + assertThat(viewModel.state.value.errorMessage).isEqualTo("Test exception") + assertThat(viewModel.state.value.isLoading).isFalse() + } + + @Test + fun givenLoadingState_whenLoadProfile_thenStateUpdatesCorrectly() = runTest { + // Act & Assert + viewModel.state.test { + val emission1 = awaitItem() + assertThat(emission1.isLoading).isFalse() + + viewModel.loadProfile() + + val emission2 = awaitItem() + assertThat(emission2.isLoading).isTrue() + + val emission3 = awaitItem() + assertThat(emission3.isLoading).isFalse() + assertThat(emission3.profile).isEqualTo(repository.profileToReturn) + } + } +}