Skip to Content

Testing trong Android

1. Giới thiệu

Testing đảm bảo code hoạt động đúng và giúp refactor an toàn.

2. Testing Pyramid

┌─────────────┐ │ E2E/UI │ ← Slow, expensive ├─────────────┤ │ Integration │ ├─────────────┤ │ Unit Tests │ ← Fast, cheap └─────────────┘

3. Setup

// build.gradle.kts 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

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 testing class MainDispatcherRule : TestWatcher() { val dispatcher = StandardTestDispatcher() override fun starting(description: Description) { Dispatchers.setMain(dispatcher) } override fun finished(description: Description) { Dispatchers.resetMain() } }

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

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

// By text onNodeWithText("Hello") onAllNodesWithText("Item") // By content description onNodeWithContentDescription("Back") // By testTag onNodeWithTag("button_submit") // By semantic properties onNode(hasText("Hello") and hasClickAction()) // All nodes onAllNodes(hasText("Item")).assertCountEquals(5)

7. Test Actions

// Click onNodeWithText("Submit").performClick() // Type text onNodeWithTag("input").performTextInput("Hello") onNodeWithTag("input").performTextClearance() // Scroll onNodeWithTag("list").performScrollToIndex(10) // Swipe onNodeWithTag("card").performTouchInput { swipeLeft() }

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

@HiltAndroidTest class 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

@Test fun 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

@Test fun 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

Test TypeLocationSpeed
Unitsrc/testFast
Integrationsrc/test or src/androidTestMedium
UI/Composesrc/androidTestSlow
FrameworkPurpose
JUnitTest runner
MockKMocking
TurbineFlow testing
Compose TestUI testing
Last updated on