From ebbbf753201f3215986413a8b877a87491f8d3da Mon Sep 17 00:00:00 2001 From: JavFuentes Date: Wed, 30 Jul 2025 07:41:14 -0400 Subject: [PATCH] Pruebas unitarias en el JokeViewModel --- README.md | 14 ++ .../javfuentes/dailyjoke/ui/JokeScreenTest.kt | 181 ++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 app/src/test/java/dev/javfuentes/dailyjoke/ui/JokeScreenTest.kt diff --git a/README.md b/README.md index d09d00e..25f5c2e 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,20 @@ The project includes comprehensive tests following TDD approach: - ✅ Data Sources - ✅ UI Components (in development) +## 🔄 CI/CD + +Automated CI/CD pipeline with **GitHub Actions**: + +- ✅ **Build & Test** - Automatic builds and unit tests on push/PR +- ✅ **Instrumented Tests** - UI tests on Android emulator +- ✅ **Code Quality** - Lint checks and static analysis +- 🎯 **Branches**: `master`, `ci-testing` + +```bash +# Run CI checks locally +./gradlew clean build test lint +``` + ## 📄 License ``` diff --git a/app/src/test/java/dev/javfuentes/dailyjoke/ui/JokeScreenTest.kt b/app/src/test/java/dev/javfuentes/dailyjoke/ui/JokeScreenTest.kt new file mode 100644 index 0000000..75c14ab --- /dev/null +++ b/app/src/test/java/dev/javfuentes/dailyjoke/ui/JokeScreenTest.kt @@ -0,0 +1,181 @@ +package dev.javfuentes.dailyjoke.ui + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import dev.javfuentes.dailyjoke.data.Joke +import dev.javfuentes.dailyjoke.data.JokeType +import dev.javfuentes.dailyjoke.data.model.* +import dev.javfuentes.dailyjoke.data.repository.JokeRepository +import dev.javfuentes.dailyjoke.viewmodel.JokeUiEvent +import dev.javfuentes.dailyjoke.viewmodel.JokeViewModel +import io.mockk.* +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.* + +class JokeViewModelUnitTest { + + private val mockRepository = mockk() + private lateinit var viewModel: JokeViewModel + + @Before + fun setup() { + clearAllMocks() + // Mock the favorites loading to avoid initialization issues + coEvery { mockRepository.getFavoriteJokes() } returns emptyList() + } + + @After + fun tearDown() { + clearAllMocks() + } + + @Test + fun `initial state should be loading when ViewModel is created`() = runTest { + // Given + coEvery { mockRepository.getRandomJoke() } returns flowOf(ApiResult.Loading()) + + // When + viewModel = JokeViewModel(mockRepository) + + // Then + viewModel.uiState.test { + val state = awaitItem() + assertThat(state.isLoading).isTrue() + assertThat(state.joke).isNull() + assertThat(state.errorMessage).isNull() + } + } + + @Test + fun `should load joke successfully when repository returns success`() = runTest { + // Given + val mockJoke = createMockJoke() + coEvery { mockRepository.getRandomJoke() } returns flowOf(ApiResult.Success(mockJoke)) + + // When + viewModel = JokeViewModel(mockRepository) + + // Then + viewModel.uiState.test { + val state = awaitItem() + assertThat(state.isLoading).isFalse() + assertThat(state.joke).isEqualTo(mockJoke) + assertThat(state.errorMessage).isNull() + } + } + + @Test + fun `should handle error when repository returns error`() = runTest { + // Given + val errorMessage = "Network error" + coEvery { mockRepository.getRandomJoke() } returns flowOf( + ApiResult.Error(NetworkException(errorMessage)) + ) + + // When + viewModel = JokeViewModel(mockRepository) + + // Then + viewModel.uiState.test { + val state = awaitItem() + assertThat(state.isLoading).isFalse() + assertThat(state.joke).isNull() + assertThat(state.errorMessage).isEqualTo(errorMessage) + } + } + + @Test + fun `should refresh joke when RefreshJoke event is handled`() = runTest { + // Given + val mockJoke = createMockJoke() + coEvery { mockRepository.getRandomJoke() } returns flowOf(ApiResult.Success(mockJoke)) + viewModel = JokeViewModel(mockRepository) + clearMocks(mockRepository, answers = false) + coEvery { mockRepository.getRandomJoke() } returns flowOf(ApiResult.Success(mockJoke)) + + // When + viewModel.handleEvent(JokeUiEvent.RefreshJoke) + + // Then + coVerify { mockRepository.getRandomJoke() } + } + + @Test + fun `should handle ClearError event`() = runTest { + // Given + val mockJoke = createMockJoke() + coEvery { mockRepository.getRandomJoke() } returns flowOf(ApiResult.Success(mockJoke)) + viewModel = JokeViewModel(mockRepository) + + // When + viewModel.handleEvent(JokeUiEvent.ClearError) + + // Then - Just verify the event doesn't crash + assertThat(true).isTrue() + } + + @Test + fun `should load joke by category when LoadJokeByCategory event is handled`() = runTest { + // Given + val category = "Programming" + val mockJoke = createMockJoke(category = category) + coEvery { mockRepository.getRandomJoke() } returns flowOf(ApiResult.Success(mockJoke)) + coEvery { mockRepository.getJokeByCategory(category) } returns flowOf(ApiResult.Success(mockJoke)) + viewModel = JokeViewModel(mockRepository) + + // When + viewModel.handleEvent(JokeUiEvent.LoadJokeByCategory(category)) + + // Then + coVerify { mockRepository.getJokeByCategory(category) } + } + + @Test + fun `should save favorite joke when SaveFavoriteJoke event is handled`() = runTest { + // Given + val mockJoke = createMockJoke() + coEvery { mockRepository.getRandomJoke() } returns flowOf(ApiResult.Success(mockJoke)) + coEvery { mockRepository.saveFavoriteJoke(any()) } just Runs + viewModel = JokeViewModel(mockRepository) + + // Wait for initial load to complete + viewModel.uiState.test { + awaitItem() // Wait for state with joke loaded + } + + // When + viewModel.handleEvent(JokeUiEvent.SaveFavoriteJoke) + + // Then + coVerify { mockRepository.saveFavoriteJoke(mockJoke) } + } + + @Test + fun `should remove from favorites when RemoveFromFavorites event is handled`() = runTest { + // Given + val mockJoke = createMockJoke() + coEvery { mockRepository.getRandomJoke() } returns flowOf(ApiResult.Success(mockJoke)) + coEvery { mockRepository.removeFavoriteJoke(any()) } just Runs + viewModel = JokeViewModel(mockRepository) + + // When + viewModel.handleEvent(JokeUiEvent.RemoveFromFavorites(mockJoke)) + + // Then + coVerify { mockRepository.removeFavoriteJoke(mockJoke.id) } + } + + private fun createMockJoke( + id: Int = 1, + category: String = "Programming" + ) = Joke( + id = id, + category = category, + setup = "Test setup", + punchline = "Test punchline", + type = JokeType.TWOPART, + safe = true, + lang = "en" + ) +} \ No newline at end of file