Testing trong Android
1. Giới thiệu
Phần tiêu đề “1. Giới thiệu”Testing đảm bảo code hoạt động đúng và giúp refactor an toàn.
2. Testing Pyramid
Phần tiêu đề “2. Testing Pyramid” ┌─────────────┐ │ E2E/UI │ ← Slow, expensive ├─────────────┤ │ Integration │ ├─────────────┤ │ Unit Tests │ ← Fast, cheap └─────────────┘3. Setup
Phần tiêu đề “3. Setup”dependencies { // Unit tests testImplementation("junit:junit:4.13.2") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0") testImplementation("io.mockk:mockk:1.13.10")
// Android/Instrumentation tests androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.compose.ui:ui-test-junit4")}4. Unit Tests
Phần tiêu đề “4. Unit Tests”Test ViewModel
Phần tiêu đề “Test ViewModel”class UserViewModelTest {
@get:Rule val mainDispatcherRule = MainDispatcherRule()
private lateinit var repository: UserRepository private lateinit var viewModel: UserViewModel
@Before fun setup() { repository = mockk() viewModel = UserViewModel(repository) }
@Test fun `loadUsers success updates state`() = runTest { // Given val users = listOf(User(1, "Alice", "alice@email.com")) coEvery { repository.getUsers() } returns Result.success(users)
// When viewModel.loadUsers()
// Then assertEquals(users, viewModel.uiState.value.users) assertEquals(false, viewModel.uiState.value.isLoading) }
@Test fun `loadUsers error shows error message`() = runTest { // Given coEvery { repository.getUsers() } returns Result.failure(Exception("Network error"))
// When viewModel.loadUsers()
// Then assertEquals("Network error", viewModel.uiState.value.error) }}
// MainDispatcherRule for testingclass MainDispatcherRule : TestWatcher() { val dispatcher = StandardTestDispatcher()
override fun starting(description: Description) { Dispatchers.setMain(dispatcher) }
override fun finished(description: Description) { Dispatchers.resetMain() }}Test UseCase
Phần tiêu đề “Test UseCase”class GetUsersUseCaseTest {
private val repository: UserRepository = mockk() private val useCase = GetUsersUseCase(repository)
@Test fun `invoke returns users from repository`() = runTest { val expected = listOf(User(1, "Alice")) coEvery { repository.getUsers() } returns Result.success(expected)
val result = useCase()
assertTrue(result.isSuccess) assertEquals(expected, result.getOrNull()) }}5. Compose UI Tests
Phần tiêu đề “5. Compose UI Tests”class UserListScreenTest {
@get:Rule val composeTestRule = createComposeRule()
@Test fun displayUserList() { val users = listOf( User(1, "Alice", "alice@email.com"), User(2, "Bob", "bob@email.com") )
composeTestRule.setContent { UserListContent(users = users, onUserClick = {}) }
composeTestRule.onNodeWithText("Alice").assertExists() composeTestRule.onNodeWithText("Bob").assertExists() }
@Test fun clickUser_triggersCallback() { var clickedId: Int? = null
composeTestRule.setContent { UserListContent( users = listOf(User(1, "Alice")), onUserClick = { clickedId = it } ) }
composeTestRule.onNodeWithText("Alice").performClick() assertEquals(1, clickedId) }
@Test fun searchField_filtersUsers() { composeTestRule.setContent { UserSearchScreen() }
composeTestRule.onNodeWithTag("search_field") .performTextInput("alice")
composeTestRule.onNodeWithText("Alice").assertExists() composeTestRule.onNodeWithText("Bob").assertDoesNotExist() }}6. Test Finders
Phần tiêu đề “6. Test Finders”// By textonNodeWithText("Hello")onAllNodesWithText("Item")
// By content descriptiononNodeWithContentDescription("Back")
// By testTagonNodeWithTag("button_submit")
// By semantic propertiesonNode(hasText("Hello") and hasClickAction())
// All nodesonAllNodes(hasText("Item")).assertCountEquals(5)7. Test Actions
Phần tiêu đề “7. Test Actions”// ClickonNodeWithText("Submit").performClick()
// Type textonNodeWithTag("input").performTextInput("Hello")onNodeWithTag("input").performTextClearance()
// ScrollonNodeWithTag("list").performScrollToIndex(10)
// SwipeonNodeWithTag("card").performTouchInput { swipeLeft()}8. Test Assertions
Phần tiêu đề “8. Test Assertions”onNodeWithText("Hello").assertExists()onNodeWithText("Hello").assertDoesNotExist()onNodeWithTag("button").assertIsEnabled()onNodeWithTag("button").assertIsNotEnabled()onNodeWithTag("checkbox").assertIsSelected()onNodeWithText("Title").assertIsDisplayed()9. Testing với Hilt
Phần tiêu đề “9. Testing với Hilt”@HiltAndroidTestclass UserScreenTest {
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1) val composeTestRule = createAndroidComposeRule<MainActivity>()
@Inject lateinit var repository: UserRepository
@Before fun setup() { hiltRule.inject() }
@Test fun userScreenLoadsData() { composeTestRule.onNodeWithTag("user_list").assertExists() }}
// Fake module for testing@Module@TestInstallIn( components = [SingletonComponent::class], replaces = [RepositoryModule::class])object FakeRepositoryModule { @Provides @Singleton fun provideFakeRepository(): UserRepository = FakeUserRepository()}10. Testing StateFlow
Phần tiêu đề “10. Testing StateFlow”@Testfun testStateFlow() = runTest { val viewModel = UserViewModel(fakeRepository)
viewModel.uiState.test { assertEquals(UiState.Loading, awaitItem())
viewModel.loadUsers()
val successState = awaitItem() assertTrue(successState is UiState.Success) }}11. Screenshot Testing
Phần tiêu đề “11. Screenshot Testing”@Testfun userCard_matchesScreenshot() { composeTestRule.setContent { UserCard(user = User(1, "Alice", "alice@email.com")) }
composeTestRule.onNodeWithTag("user_card") .captureToImage() .assertAgainstGolden(rule, "user_card")}📝 Tóm tắt
Phần tiêu đề “📝 Tóm tắt”| Test Type | Location | Speed |
|---|---|---|
| Unit | src/test |
Fast |
| Integration | src/test or src/androidTest |
Medium |
| UI/Compose | src/androidTest |
Slow |
| Framework | Purpose |
|---|---|
| JUnit | Test runner |
| MockK | Mocking |
| Turbine | Flow testing |
| Compose Test | UI testing |