Kotlin Unit Testing Guidelines & Best Practices for Flow
This skill outlines the standards and conventions for writing unit tests in the Flow codebase (io.github.aedev.flow).
1. Naming Conventions
Test Class Naming
- Test class names MUST mirror the target class with a
Testsuffix.- Example:
LikedVideosViewModel→LikedVideosViewModelTest - Example:
HistoryViewModel→HistoryViewModelTest - Example:
SearchViewModel→SearchViewModelTest
- Example:
Test Method Naming
- Use backtick-quoted descriptive phrases outlining the scenario and expected outcome:
- Format:
`[given condition or action] [expected result]` - Good:
`initial state loads liked videos from repository` - Good:
`removeLike calls repository and updates state` - Good:
`search with query updates results flow` - Bad:
testRemoveLike(),test1()
- Format:
2. Mocking Framework & Cleanup Rules
- Framework: Use MockK (
io.mockk.*). - Initialization: Create mocks in
@Beforeor as class properties viamockk(relaxed = true)ormockk(). - Teardown / Cleanup: ALWAYS clean up mocks in an
@Aftermethod usingunmockkAll()orclearAllMocks()to ensure isolated test execution and prevent memory leaks/polluted mock states across tests.
@Before
fun setUp() {
repository = mockk(relaxed = true)
}
@After
fun tearDown() {
unmockkAll()
}
3. Coroutines & ViewModel Testing
ViewModel unit tests run on the JVM without an Android Main Looper. Since viewModelScope uses Dispatchers.Main, you MUST replace the Main dispatcher during tests:
- Use
Dispatchers.setMain(testDispatcher)in@Before(or via a JUnitTestRule). - Use
Dispatchers.resetMain()in@After. - Wrap suspend test logic inside
runTestfromkotlinx.coroutines.test.
@OptIn(ExperimentalCoroutinesApi::class)
class MyViewModelTest {
private val testDispatcher = StandardTestDispatcher()
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
@After
fun tearDown() {
Dispatchers.resetMain()
unmockkAll()
}
}
4. Assertions & AAA Pattern
- Assertion Library: Use Google Truth (
com.google.common.truth.Truth.assertThat). - AAA Pattern: Structure tests clearly with Arrange, Act, and Assert:
@Test
fun `removeLike calls repository to remove item`() = runTest {
// Arrange
val videoId = "test_id_123"
coEvery { repository.removeLikeState(videoId) } returns Unit
// Act
viewModel.removeLike(videoId)
testScheduler.advanceUntilIdle()
// Assert
coVerify(exactly = 1) { repository.removeLikeState(videoId) }
}
5. Summary Checklist Before Shipping Tests
- ✅ Test file resides under
app/src/test/java/...matching package structure of source class. - ✅ Method names use backtick-quoted descriptive sentences.
- ✅ MockK is used for dependencies with
unmockkAll()in@After. - ✅ Main dispatcher rule/setup is present for ViewModel tests.
- ✅ Google Truth (
assertThat) is used for assertions. - ✅ Test passes cleanly via
./gradlew :app:testGithubDebugUnitTest.