Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
Expand Down
181 changes: 181 additions & 0 deletions app/src/test/java/dev/javfuentes/dailyjoke/ui/JokeScreenTest.kt
Original file line number Diff line number Diff line change
@@ -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<JokeRepository>()
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"
)
}