Testing Strategy
Relevant source files
The following files were used as context for generating this wiki page:- app/src/androidTest/java/com/vocabri/test/ui/dictionary/DictionaryScreenStateTest.kt
- domain/src/test/java/com/vocabri/domain/AddWordUseCaseTest.kt
- data/src/test/java/com/vocabri/data/WordRepositoryImplTest.kt
- app/src/test/java/com/vocabri/rules/MainDispatcherRule.kt
- app/src/test/java/com/vocabri/ui/addword/MainViewModelTest.kt
- app/src/test/java/com/vocabri/ui/dictionarydetails/DictionaryDetailsViewModelTest.kt
- data/src/test/java/com/vocabri/data/WordLocalDataSourceImplTest.kt
- app/src/androidTest/java/com/vocabri/test/ui/dictionarydetails/DictionaryDetailsNavigationRouteTest.kt
- README.md
The Vocabri project employs a multi-layered testing strategy to ensure code quality, correctness, and maintainability. The approach covers all major architectural layers, from the data persistence layer up to the user interface. Testing is a mix of JVM-based unit tests and Android instrumented tests for the UI.
The strategy relies on a standard set of tools including JUnit 4, MockK for mocking, and Kotlin Coroutines Test for handling asynchronous code. This comprehensive approach allows for isolated testing of components, verification of business logic, and validation of user interactions, contributing to the overall stability of the application.
Sources: README.md:82-85
Testing Layers
The project's tests are structured to align with its clean architecture, with distinct tests for the Data, Domain, and UI layers. This separation ensures that each component can be tested in isolation.
The diagram below illustrates the different layers of testing implemented in the project.
Unit Testing (JVM)
Unit tests run on the local JVM and are used for the data, domain, and ViewModel layers. They are fast, reliable, and form the foundation of the testing pyramid.
Data Layer Testing
The data layer is tested at two levels: the Repository implementation and the local data source.
Repository Testing
WordRepositoryImpl is tested by mocking its dependencies (WordDataSource and RemoteWordDataSource) using MockK. This isolates the repository and allows for verification of its logic, such as checking for duplicate words before insertion or ensuring that both local and remote data sources are called correctly.
This sequence diagram shows the interaction during the insertWord test when a duplicate word is checked.
Data Source Testing
WordLocalDataSourceImpl is tested against a real database instance using an in-memory JDBC driver (createTestJdbcSqlDriver). This ensures that the SQLDelight queries and mappers work correctly. Tests cover insertion, retrieval, and deletion of words and their associated details (translations, examples).
Sources: data/src/test/java/com/vocabri/data/WordLocalDataSourceImplTest.kt:43-44, data/src/test/java/com/vocabri/data/WordLocalDataSourceImplTest.kt:69-86
Domain Layer Testing
Use cases in the domain layer are tested to validate the core business logic. These tests typically use a fake implementation of the repository (FakeWordRepositoryImpl) to provide a controlled environment. This allows tests to verify that the use case correctly processes data and handles various scenarios, such as adding a new word or preventing duplicates.
The following test from AddWordUseCaseTest demonstrates adding a word and verifying its presence in the repository.
// domain/src/test/java/com/vocabri/domain/AddWordUseCaseTest.kt:44-58
@Test
fun `execute adds word with details to repository`() = runTest {
val word = Word.Verb(
id = "1",
text = "lernen",
translations = listOf(Translation("1", "learn")),
examples = listOf(),
conjugation = "irregular",
management = "auf + Akk.",
)
addWordUseCase.execute(word)
val words = fakeRepository.getWordsByPartOfSpeech(partOfSpeech = PartOfSpeech.ALL)
assertEquals(1, words.size)
// ... assertions
}
Sources: domain/src/test/java/com/vocabri/domain/AddWordUseCaseTest.kt:44-58
ViewModel Testing
ViewModels are tested to verify state management logic. The tests follow an MVI-like pattern:
- An
Eventis sent to the ViewModel. - Coroutines are advanced using
advanceUntilIdle(). - The resulting
Stateis collected and asserted.
The MainDispatcherRule is a crucial component for these tests, as it replaces the main coroutine dispatcher with a TestDispatcher, allowing for precise control over asynchronous operations.
This flowchart shows the typical lifecycle of a ViewModel test.
An example from MainViewModelTest shows how updating the text field is tested:
// app/src/test/java/com/vocabri/ui/addword/MainViewModelTest.kt:82-91
@Test
fun `updateText updates the text in state`() = runTest {
// Act
viewModel.handleEvent(AddWordEvent.UpdateText("lernen"))
advanceUntilIdle()
// Assert
val state = viewModel.state.first()
assertTrue(state is AddWordState.Editing)
assertEquals("lernen", (state as AddWordState.Editing).text)
}
Sources: app/src/test/java/com/vocabri/ui/addword/MainViewModelTest.kt:82-91, app/src/test/java/com/vocabri/rules/MainDispatcherRule.kt
UI Testing (Instrumented)
Instrumented tests run on an Android device or emulator and are used to test the Jetpack Compose UI. These tests verify that the UI displays the correct state and that user interactions trigger the expected events or navigation.
Composable and Screen State Testing
The createComposeRule is used to render Composables in a test environment. Tests can then use finders (onNodeWithText, etc.) to locate UI elements and perform assertions (assertIsDisplayed) or actions (performClick).
For example, DictionaryScreenStateTest verifies that in an error state, a specific error message and a "Retry" button are shown. Clicking the button is also verified to dispatch a RetryClicked event.
// app/src/androidTest/java/com/vocabri/test/ui/dictionary/DictionaryScreenStateTest.kt:53-67
@Test
fun errorState_showsRetry_andDispatchesEvent() {
val events = mutableListOf<DictionaryEvent>()
composeTestRule.setContent {
DictionaryScreenRoot(
state = DictionaryState.Error(message = "Network error"),
onEvent = { events.add(it) },
)
}
composeTestRule.onNodeWithText("An error occurred: Network error").assertIsDisplayed()
composeTestRule.onNodeWithText("Retry").assertIsDisplayed().performClick()
assert(events.contains(DictionaryEvent.RetryClicked))
}
Sources: app/src/androidTest/java/com/vocabri/test/ui/dictionary/DictionaryScreenStateTest.kt:53-67
Navigation Testing
Navigation behavior is also tested. In DictionaryDetailsNavigationRouteTest, a mock NavHostController is used to verify that popBackStack() is called when the back button is clicked.
Dependency Injection in UI Tests
The KoinTestRule is used to set up a Koin dependency injection container with test-specific modules. This allows for replacing real ViewModels or other dependencies with test doubles.
Tooling and Libraries
The project leverages a standard set of libraries for testing.
| Library | Purpose |
|---|---|
| JUnit 4 | The core framework for running tests. |
| MockK | A mocking library for Kotlin, used to create mocks and verify interactions. |
| Kotlin Coroutines Test | Provides TestDispatcher and runTest for testing code that uses coroutines. |
| Robolectric | Enables running Android-dependent unit tests on the JVM by providing shadow implementations of Android APIs. |
| Compose UI Testing | A framework for testing Jetpack Compose UIs, providing rules and APIs to interact with Composables. |
Sources: README.md:84-85, app/src/test/java/com/vocabri/ui/addword/MainViewModelTest.kt:43, data/src/test/java/com/vocabri/data/WordRepositoryImplTest.kt:38, app/src/androidTest/java/com/vocabri/test/ui/dictionary/DictionaryScreenStateTest.kt:32
Coroutine Test Management
The MainDispatcherRule is a custom JUnit 4 rule that simplifies coroutine testing in ViewModels. It sets the main dispatcher to a TestDispatcher before each test and resets it afterward, ensuring that tests involving viewModelScope are predictable and controllable.
// app/src/test/java/com/vocabri/rules/MainDispatcherRule.kt
@OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherRule(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}
Sources: app/src/test/java/com/vocabri/rules/MainDispatcherRule.kt
Summary
Vocabri's testing strategy is robust and comprehensive, covering all layers of its architecture. By separating unit and instrumented tests and leveraging modern libraries like MockK, Coroutines Test, and Compose UI Testing, the project ensures that individual components, business logic, and user interactions are thoroughly validated. This layered approach is key to maintaining high code quality and enabling confident refactoring and feature development.