실제 DI 모듈을 떼고 가짜를 꽂는다.
@HiltAndroidTest
@UninstallModules(NetworkModule::class)
class HomeScreenTest {
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1) val composeRule = createAndroidComposeRule<MainActivity>()
@BindValue @JvmField val api: UserApi = FakeUserApi()
@Before fun setUp() { hiltRule.inject() }
@Test fun 목록이_그려진다() {
composeRule.onNodeWithText("홍길동").assertIsDisplayed()
}
}
Rule 순서가 중요하다
- HiltAndroidRule 이 먼저(order = 0) 돌아야 주입이 끝난 뒤 Activity 가 뜬다
- 뒤집으면 주입 전에 Activity 가 시작돼 크래시한다
테스트 전용 러너
// build.gradle.kts
defaultConfig { testInstrumentationRunner = "com.example.HiltTestRunner" }
// HiltTestRunner.kt
class HiltTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, ctx: Context?) =
super.newApplication(cl, HiltTestApplication::class.java.name, ctx)
}
이걸 빠뜨리면 실제 @HiltAndroidApp 이 떠서 테스트 모듈이 적용되지 않는다
테스트 피라미드
▲ UI 테스트 느리다(초 단위) · 깨지기 쉽다 · 핵심 흐름만
───
통합 테스트 Repository + Room(인메모리) + 가짜 API
─────────
단위 테스트 JVM 에서 밀리초. 가장 많이
비율은 대략 70 / 20 / 10 이 흔한 목표다
코루틴·Flow 테스트
@Test fun 새로고침하면_목록이_갱신된다() = runTest {
val vm = HomeViewModel(FakeRepository())
vm.uiState.test { // turbine
assertEquals(HomeUiState(isLoading = true), awaitItem())
vm.refresh()
assertEquals(2, awaitItem().users.size)
}
}
// Dispatchers.Main 을 테스트 디스패처로 교체한다
@get:Rule val dispatcherRule = MainDispatcherRule()
// (viewModelScope 가 Dispatchers.Main 을 쓰므로 없으면 예외가 난다)
Room 테스트
val db = Room.inMemoryDatabaseBuilder(ctx, AppDatabase::class.java)
.allowMainThreadQueries() // 테스트에서만
.build()
면접 함정
- ❌ "UI 테스트가 통과하면 안전하다" → 느려서 조합을 다 못 돈다. 로직은 단위 테스트로 덮는다.
- ❌ "목킹을 많이 할수록 좋다" → 구현에 결합돼 리팩터링마다 깨진다. 가짜 구현(Fake)이 더 견고한 경우가 많다.