Skip to content

06 DI 与测试 🧪

依赖注入如何提升可测试性,以及 DI 框架的测试策略


一、为什么 DI 提升可测试性

1.1 测试的挑战

kotlin
// ==== 没有 DI 的问题 ====

// ❌ 问题 1:硬编码依赖
class UserRepository {
    private val api = RealUserApi()  // 硬编码,无法替换
    private val database = RealDatabase()  // 硬编码,无法替换
    
    fun getUser(id: Int): User {
        return api.getUser(id)  // 测试时需要真实网络
    }
}

// ❌ 问题 2:难以隔离
class UserService {
    private val repository = UserRepository()
    private val analytics = RealAnalytics()
    private val logger = RealLogger()
    
    fun getUser(id: Int): User {
        val user = repository.getUser(id)
        analytics.track("get_user")
        logger.log("Got user: $user")
        return user
    }
}
// 测试 getUser 需要:网络、数据库、分析服务、日志服务

// ❌ 问题 3:无法 Mock
class OrderProcessor {
    private val paymentGateway = PaymentGateway()  // 真实支付网关
    private val emailService = EmailService()      // 真实邮件服务
    
    fun processOrder(order: Order) {
        paymentGateway.charge(order.amount)  // 测试时会真实扣款!
        emailService.sendConfirmation(order)  // 测试时会真实发邮件!
    }
}

1.2 DI 带来的测试优势

kotlin
// ==== 使用 DI 后的改进 ====

// ✅ 改进 1:依赖可替换
class UserRepository @Inject constructor(
    private val api: UserApi,      // 接口,可替换
    private val database: Database // 接口,可替换
) {
    fun getUser(id: Int): User {
        return api.getUser(id)
    }
}

// 测试时
@Test
fun testGetUser() {
    val mockApi = MockUserApi()      // Mock
    val mockDatabase = MockDatabase() // Mock
    val repository = UserRepository(mockApi, mockDatabase)
    
    val user = repository.getUser(1)
    // 无需网络,快速测试
}

// ✅ 改进 2:依赖隔离
class UserService @Inject constructor(
    private val repository: UserRepository,
    private val analytics: Analytics,
    private val logger: Logger
) {
    fun getUser(id: Int): User {
        val user = repository.getUser(id)
        analytics.track("get_user")
        logger.log("Got user: $user")
        return user
    }
}

// 测试时
@Test
fun testGetUser() {
    val mockRepository = MockUserRepository()
    val mockAnalytics = MockAnalytics()
    val mockLogger = MockLogger()
    
    val service = UserService(mockRepository, mockAnalytics, mockLogger)
    
    every { mockRepository.getUser(1) } returns User("test")
    
    val user = service.getUser(1)
    
    verify { mockAnalytics.track("get_user") }
    verify { mockLogger.log(any()) }
}

// ✅ 改进 3:易于 Mock
class OrderProcessor @Inject constructor(
    private val paymentGateway: PaymentGateway,
    private val emailService: EmailService
) {
    fun processOrder(order: Order) {
        paymentGateway.charge(order.amount)
        emailService.sendConfirmation(order)
    }
}

// 测试时
@Test
fun testProcessOrder() {
    val mockPayment = MockPaymentGateway()
    val mockEmail = MockEmailService()
    
    val processor = OrderProcessor(mockPayment, mockEmail)
    
    every { mockPayment.charge(any()) } returns true
    every { mockEmail.sendConfirmation(any()) } returns true
    
    processor.processOrder(Order(100.0))
    
    verify { mockPayment.charge(100.0) }
    verify { mockEmail.sendConfirmation(any()) }
    // 不会真实扣款或发邮件
}

1.3 测试金字塔与 DI

        /\
       /  \
      / E2E \      端到端测试(少量)
     /______\     - 测试完整流程
    /        \    - 使用真实依赖
   /  Integration \  集成测试(中量)
  /________________\ - 测试组件协作
 /                  \ - 使用部分 Mock
/     Unit Tests     \ 单元测试(大量)
______________________ - 测试单一功能
                        - 使用 Mock 隔离
                        - DI 使 Mock 成为可能

DI 的价值:
- 使单元测试成为可能
- 减少集成测试依赖
- 提高测试速度
- 降低测试成本

二、单元测试中的 Mock 注入

2.1 Mock 基础

kotlin
// ==== Mock 库选择 ====

// MockK(推荐,Kotlin 原生)
testImplementation("io.mockk:mockk:1.13.0")

// Mockito(Java 传统)
testImplementation("org.mockito:mockito-core:5.0.0")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.0.0")

// ==== MockK 基础用法 ====

import io.mockk.*

// 创建 Mock
val mockApi = mockk<UserApi>()

// 设置行为
every { mockApi.getUser(1) } returns User("test")
every { mockApi.getUser(any()) } returns User("default")
every { mockApi.save(any()) } returns true

// 验证调用
mockApi.getUser(1)
verify { mockApi.getUser(1) }
verify(exactly = 1) { mockApi.getUser(1) }
verify { mockApi.getUser(any()) }

// 验证未调用
verify(inverse = true) { mockApi.delete(any()) }

// 抛出异常
every { mockApi.getUser(2) } throws ApiException("Not found")

// 异步 Mock
everySuspend { mockApi.fetchData() } returns Data("test")

2.2 纯单元测试

kotlin
// ==== 被测试类 ====

class UserRepository @Inject constructor(
    private val api: UserApi,
    private val database: Database,
    private val preferences: Preferences
) {
    suspend fun getUser(id: Int): User {
        // 先查缓存
        val cached = preferences.getCachedUser(id)
        if (cached != null) return cached
        
        // 再查数据库
        val local = database.getUser(id)
        if (local != null) {
            preferences.cacheUser(local)
            return local
        }
        
        // 最后查网络
        val remote = api.getUser(id)
        database.saveUser(remote)
        preferences.cacheUser(remote)
        return remote
    }
}

// ==== 单元测试 ====

class UserRepositoryTest {
    
    // Mock 依赖
    private lateinit var mockApi: UserApi
    private lateinit var mockDatabase: Database
    private lateinit var mockPreferences: Preferences
    private lateinit var repository: UserRepository
    
    @Before
    fun setup() {
        // 创建 Mock
        mockApi = mockk()
        mockDatabase = mockk()
        mockPreferences = mockk()
        
        // 创建被测试对象
        repository = UserRepository(mockApi, mockDatabase, mockPreferences)
    }
    
    @Test
    fun `getUser returns cached user when available`() = runTest {
        // 设置 Mock 行为
        val cachedUser = User("cached")
        every { mockPreferences.getCachedUser(1) } returns cachedUser
        
        // 执行
        val result = repository.getUser(1)
        
        // 验证
        assertEquals(cachedUser, result)
        verify(exactly = 0) { mockDatabase.getUser(any()) }
        verify(exactly = 0) { mockApi.getUser(any()) }
    }
    
    @Test
    fun `getUser returns database user when cache miss`() = runTest {
        // 设置 Mock 行为
        every { mockPreferences.getCachedUser(1) } returns null
        val dbUser = User("database")
        every { mockDatabase.getUser(1) } returns dbUser
        
        // 执行
        val result = repository.getUser(1)
        
        // 验证
        assertEquals(dbUser, result)
        verify { mockPreferences.cacheUser(dbUser) }
        verify(exactly = 0) { mockApi.getUser(any()) }
    }
    
    @Test
    fun `getUser fetches from network when cache and database miss`() = runTest {
        // 设置 Mock 行为
        every { mockPreferences.getCachedUser(1) } returns null
        every { mockDatabase.getUser(1) } returns null
        val remoteUser = User("remote")
        every { mockApi.getUser(1) } returns remoteUser
        
        // 执行
        val result = repository.getUser(1)
        
        // 验证
        assertEquals(remoteUser, result)
        verify { mockDatabase.saveUser(remoteUser) }
        verify { mockPreferences.cacheUser(remoteUser) }
    }
    
    @Test
    fun `getUser throws exception when network fails`() = runTest {
        // 设置 Mock 行为
        every { mockPreferences.getCachedUser(1) } returns null
        every { mockDatabase.getUser(1) } returns null
        every { mockApi.getUser(1) } throws ApiException("Network error")
        
        // 执行并验证异常
        assertFailsWith<ApiException> {
            repository.getUser(1)
        }
    }
}

2.3 带 DI 容器的测试

kotlin
// ==== 使用 Koin 测试 ====

class KoinRepositoryTest {
    
    @get:Rule
    val koinTestRule = KoinTestRule.create {
        modules(module {
            // 提供 Mock 依赖
            single { mockk<UserApi>() }
            single { mockk<Database>() }
            single { mockk<Preferences>() }
            single { UserRepository(get(), get(), get()) }
        })
    }
    
    @Test
    fun testWithKoin() {
        // 从 Koin 获取
        val repository = get<UserRepository>()
        val mockApi = get<UserApi>()
        
        // 设置 Mock
        every { mockApi.getUser(1) } returns User("test")
        
        // 测试
        runTest {
            val result = repository.getUser(1)
            assertEquals("test", result.name)
        }
    }
}

// ==== 使用 Hilt 测试 ====

@HiltAndroidTest
class HiltRepositoryTest {
    
    @get:Rule
    val hiltRule = HiltAndroidRule(this)
    
    @Test
    fun testWithHilt() {
        // Hilt 会自动注入
        // 详见后面的 Hilt 测试章节
    }
}

三、Hilt 测试详解

3.1 Hilt 测试依赖

kotlin
// ==== build.gradle.kts 配置 ====

dependencies {
    // Hilt 测试
    testImplementation("com.google.dagger:hilt-android-testing:2.44")
    kaptTest("com.google.dagger:hilt-android-compiler:2.44")
    
    // Instrumentation 测试
    androidTestImplementation("com.google.dagger:hilt-android-testing:2.44")
    kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.44")
    
    // MockK
    testImplementation("io.mockk:mockk:1.13.0")
    androidTestImplementation("io.mockk:mockk-android:1.13.0")
}

3.2 @HiltAndroidRule

kotlin
// ==== 基础测试设置 ====

@HiltAndroidTest
class UserRepositoryTest {
    
    @get:Rule
    val hiltRule = HiltAndroidRule(this)
    
    // 注入测试依赖
    @Inject
    lateinit var repository: UserRepository
    
    @Inject
    lateinit var api: UserApi
    
    @Before
    fun init() {
        hiltRule.inject()
    }
    
    @Test
    fun testRepository() {
        // 使用注入的依赖
        val user = repository.getUser(1)
        // ...
    }
}

// ==== 替换 Module ====

@HiltAndroidTest
class RepositoryTest {
    
    @get:Rule
    val hiltRule = HiltAndroidRule(this)
    
    @Inject
    lateinit var repository: UserRepository
    
    @Before
    fun init() {
        hiltRule.inject()
    }
    
    @Test
    fun testWithRealDependencies() {
        // 使用真实的依赖(如果配置了)
        val user = repository.getUser(1)
    }
}

3.3 @UninstallModules

kotlin
// ==== 替换生产 Module ====

// 生产 Module
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideApi(): ApiService {
        return RealApiService()  // 真实 API
    }
}

// 测试 Module
@Module
@InstallIn(SingletonComponent::class)
object TestNetworkModule {
    @Provides
    @Singleton
    fun provideApi(): ApiService {
        return MockApiService()  // Mock API
    }
}

// 测试类
@HiltAndroidTest
@UninstallModules(NetworkModule::class)  // 卸载生产 Module
class ApiTest {
    
    @get:Rule
    val hiltRule = HiltAndroidRule(this)
    
    @Inject
    lateinit var api: ApiService
    
    @Before
    fun init() {
        hiltRule.inject()
    }
    
    @Test
    fun testApi() {
        // api 是 MockApiService
        // ...
    }
}

3.4 @BindValue

kotlin
// ==== 快速绑定测试依赖 ====

@HiltAndroidTest
class ViewModelTest {
    
    @get:Rule
    val hiltRule = HiltAndroidRule(this)
    
    // 创建 Mock
    private val mockRepository = MockUserRepository()
    
    // 绑定 Mock
    @BindValue
    val repository: UserRepository = mockRepository
    
    @Inject
    lateinit var viewModel: MainViewModel
    
    @Before
    fun init() {
        hiltRule.inject()
    }
    
    @Test
    fun testViewModel() {
        // 设置 Mock 行为
        every { mockRepository.getUser(1) } returns User("test")
        
        // 测试 ViewModel
        viewModel.loadUser(1)
        
        // 验证
        verify { mockRepository.getUser(1) }
    }
}

// ==== 多个 @BindValue ====

@HiltAndroidTest
class ComplexTest {
    
    @get:Rule
    val hiltRule = HiltAndroidRule(this)
    
    @BindValue
    val api: ApiService = MockApiService()
    
    @BindValue
    val database: Database = MockDatabase()
    
    @BindValue
    val preferences: Preferences = MockPreferences()
    
    @Inject
    lateinit var repository: UserRepository
    
    @Before
    fun init() {
        hiltRule.inject()
    }
    
    @Test
    fun testWithMultipleMocks() {
        // 所有依赖都是 Mock
        // ...
    }
}

3.5 HiltTestApplication

kotlin
// ==== 配置测试 Application ====

// 测试 Application
@HiltAndroidApp
class HiltTestApplication : Application()

// ==== Robolectric 测试配置 ====

@HiltAndroidTest
@RunWith(RobolectricTestRunner::class)
@Config(application = HiltTestApplication::class)
class RobolectricTest {
    
    @get:Rule
    val hiltRule = HiltAndroidRule(this)
    
    @Inject
    lateinit var repository: UserRepository
    
    @Before
    fun init() {
        hiltRule.inject()
    }
    
    @Test
    fun testWithRobolectric() {
        // 在 Robolectric 环境中测试
        // ...
    }
}

// ==== Instrumentation 测试配置 ====

// AndroidManifest.xml
<application
    android:name=".HiltTestApplication"
    ... >
</application>

// 测试类
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class InstrumentationTest {
    
    @get:Rule
    val hiltRule = HiltAndroidRule(this)
    
    @Test
    fun testWithInstrumentation() {
        // 在真实设备/模拟器上测试
        // ...
    }
}

3.6 Activity 和 Fragment 测试

kotlin
// ==== Activity 测试 ====

@HiltAndroidTest
class MainActivityTest {
    
    @get:Rule
    val hiltRule = HiltAndroidRule(this)
    
    @BindValue
    val mockRepository = MockUserRepository()
    
    @Test
    fun testActivity() {
        // 创建 Activity Scenario
        val scenario = ActivityScenario.launch(MainActivity::class.java)
        
        scenario.onActivity { activity ->
            // 验证 Activity 状态
            assertNotNull(activity.viewModel)
        }
    }
}

// ==== Fragment 测试 ====

@HiltAndroidTest
class MainFragmentTest {
    
    @get:Rule
    val hiltRule = HiltAndroidRule(this)
    
    @BindValue
    val mockRepository = MockUserRepository()
    
    @Test
    fun testFragment() {
        // 创建 Fragment Scenario
        val scenario = FragmentScenario.launchInContainer(MainFragment::class.java)
        
        scenario.onFragment { fragment ->
            // 验证 Fragment 状态
            assertNotNull(fragment.viewModel)
        }
    }
}

// ==== ViewModel 测试 ====

@HiltAndroidTest
class MainViewModelTest {
    
    @get:Rule
    val hiltRule = HiltAndroidRule(this)
    
    @BindValue
    val mockRepository = MockUserRepository()
    
    @Inject
    lateinit var viewModel: MainViewModel
    
    @Before
    fun init() {
        hiltRule.inject()
    }
    
    @Test
    fun testViewModel() {
        // 设置 Mock
        every { mockRepository.getUser(1) } returns User("test")
        
        // 测试
        viewModel.loadUser(1)
        
        // 验证 LiveData
        val observer = Observer<User> { }
        viewModel.user.observeForever(observer)
        
        verify { mockRepository.getUser(1) }
    }
}

四、Instrumentation 测试

4.1 Instrumentation 测试基础

kotlin
// ==== 配置 ====

// build.gradle.kts
androidTestImplementation("com.google.dagger:hilt-android-testing:2.44")
androidTestImplementation("androidx.test:runner:1.5.0")
androidTestImplementation("androidx.test:rules:1.5.0")
androidTestImplementation("androidx.test.ext:junit:1.1.5")

// ==== 基础测试 ====

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class InstrumentationRepositoryTest {
    
    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)
    
    @get:Rule(order = 1)
    val scenarioRule = ActivityScenarioRule(MainActivity::class.java)
    
    @Inject
    lateinit var repository: UserRepository
    
    @Before
    fun init() {
        hiltRule.inject()
    }
    
    @Test
    fun testRealApi() {
        // 使用真实的 API(需要网络)
        runTest {
            val user = repository.getUser(1)
            assertNotNull(user)
        }
    }
}

4.2 使用 MockWebServer

kotlin
// ==== 配置 MockWebServer ====

// build.gradle.kts
androidTestImplementation("com.squareup.okhttp3:mockwebserver:4.11.0")

// ==== 测试类 ====

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class ApiIntegrationTest {
    
    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)
    
    private val mockWebServer = MockWebServer()
    
    @get:Rule(order = 1)
    val hiltInstallRule = HiltInstallRule(mockWebServer)
    
    @Inject
    lateinit var api: ApiService
    
    @Before
    fun setup() {
        mockWebServer.start()
        hiltRule.inject()
    }
    
    @After
    fun teardown() {
        mockWebServer.shutdown()
    }
    
    @Test
    fun testGetUser() {
        // 设置 Mock 响应
        val jsonResponse = """{"id": 1, "name": "test"}"""
        val response = MockResponse()
            .setResponseCode(200)
            .setBody(jsonResponse)
        mockWebServer.enqueue(response)
        
        // 执行请求
        runTest {
            val user = api.getUser(1)
            
            // 验证
            assertEquals(1, user.id)
            assertEquals("test", user.name)
            
            // 验证请求
            val request = mockWebServer.takeRequest()
            assertEquals("/users/1", request.path)
        }
    }
}

// ==== HiltInstallRule ====

class HiltInstallRule(
    private val mockWebServer: MockWebServer
) : TestRule {
    
    override fun apply(base: Statement, description: Description): Statement {
        return object : Statement() {
            override fun evaluate() {
                // 在测试前配置 Hilt 使用 MockWebServer
                System.setProperty("test.base_url", mockWebServer.url("/").toString())
                try {
                    base.evaluate()
                } finally {
                    System.clearProperty("test.base_url")
                }
            }
        }
    }
}

4.3 Room 数据库测试

kotlin
// ==== 配置 Room 测试 ====

// build.gradle.kts
androidTestImplementation("androidx.room:room-testing:2.5.0")

// ==== 测试类 ====

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class DatabaseTest {
    
    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)
    
    private lateinit var database: AppDatabase
    
    @Inject
    lateinit var userDao: UserDao
    
    @Before
    fun setup() {
        hiltRule.inject()
        
        // 创建内存数据库
        database = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            AppDatabase::class.java
        ).build()
        
        userDao = database.userDao()
    }
    
    @After
    fun teardown() {
        database.close()
    }
    
    @Test
    fun testInsertAndQuery() = runTest {
        // 插入数据
        val user = User(id = 1, name = "test")
        userDao.insert(user)
        
        // 查询数据
        val result = userDao.getById(1)
        
        // 验证
        assertEquals(user, result)
    }
    
    @Test
    fun testUpdate() = runTest {
        // 插入数据
        userDao.insert(User(id = 1, name = "original"))
        
        // 更新数据
        userDao.update(User(id = 1, name = "updated"))
        
        // 验证
        val result = userDao.getById(1)
        assertEquals("updated", result?.name)
    }
}

五、Koin 测试详解

5.1 Koin 测试依赖

kotlin
// ==== build.gradle.kts 配置 ====

dependencies {
    // Koin 测试
    testImplementation("io.insert-koin:koin-test:3.3.0")
    testImplementation("io.insert-koin:koin-test-junit4:3.3.0")
    
    // Android 测试
    androidTestImplementation("io.insert-koin:koin-test:3.3.0")
    androidTestImplementation("io.insert-koin:koin-test-junit4:3.3.0")
    
    // MockK
    testImplementation("io.mockk:mockk:1.13.0")
}

5.2 KoinTestRule

kotlin
// ==== 基础测试设置 ====

class RepositoryTest {
    
    @get:Rule
    val koinTestRule = KoinTestRule.create {
        modules(appModule)
    }
    
    @Test
    fun testRepository() {
        // 从 Koin 获取依赖
        val repository = get<UserRepository>()
        
        // 测试
        assertNotNull(repository)
    }
}

// ==== 使用 Mock ====

class RepositoryTestWithMock {
    
    @get:Rule
    val koinTestRule = KoinTestRule.create {
        modules(module {
            // 提供 Mock 依赖
            single { mockk<UserApi>() }
            single { mockk<Database>() }
            single { UserRepository(get(), get()) }
        })
    }
    
    @Test
    fun testWithMock() {
        val repository = get<UserRepository>()
        val mockApi = get<UserApi>()
        
        // 设置 Mock 行为
        every { mockApi.getUser(1) } returns User("test")
        
        // 测试
        runTest {
            val user = repository.getUser(1)
            assertEquals("test", user.name)
        }
    }
}

5.3 KoinTest 接口

kotlin
// ==== 使用 KoinTest 接口 ====

class RepositoryTest : KoinTest {
    
    private val repository: UserRepository by inject()
    private val api: UserApi by inject()
    
    @Before
    fun setup() {
        startKoin {
            modules(module {
                single { mockk<UserApi>() }
                single { UserRepository(get()) }
            })
        }
    }
    
    @After
    fun teardown() {
        stopKoin()
    }
    
    @Test
    fun testRepository() {
        // 使用注入的依赖
        val user = repository.getUser(1)
        // ...
    }
}

// ==== 多个测试类共享配置 ====

// 测试基类
abstract class BaseKoinTest {
    
    @get:Rule
    val koinTestRule = KoinTestRule.create {
        modules(listOf(
            appModule,
            testModule
        ))
    }
}

// 具体测试
class RepositoryTest : BaseKoinTest() {
    @Test fun testRepository() { }
}

class ViewModelTest : BaseKoinTest() {
    @Test fun testViewModel() { }
}

5.4 MockK 与 Koin 集成

kotlin
// ==== 完整的测试示例 ====

class UserRepositoryTest : KoinTest {
    
    private lateinit var mockApi: UserApi
    private lateinit var mockDatabase: Database
    private lateinit var repository: UserRepository
    
    @Before
    fun setup() {
        // 创建 Mock
        mockApi = mockk()
        mockDatabase = mockk()
        
        // 启动 Koin
        startKoin {
            modules(module {
                single { mockApi }
                single { mockDatabase }
                single { UserRepository(get(), get()) }
            })
        }
        
        // 获取被测试对象
        repository = get()
    }
    
    @After
    fun teardown() {
        stopKoin()
    }
    
    @Test
    fun `getUser returns cached user`() = runTest {
        // 设置 Mock
        every { mockApi.getUser(any()) } returns User("api")
        every { mockDatabase.getUser(any()) } returns null
        
        // 测试
        val result = repository.getUser(1)
        
        // 验证
        assertEquals("api", result.name)
        verify { mockApi.getUser(1) }
    }
    
    @Test
    fun `getUser throws on network error`() = runTest {
        // 设置 Mock
        every { mockApi.getUser(any()) } throws ApiException("Error")
        
        // 测试并验证异常
        assertFailsWith<ApiException> {
            repository.getUser(1)
        }
    }
}

六、依赖替换策略

6.1 测试 Module

kotlin
// ==== 生产 Module ====

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideApi(): ApiService {
        return RealApiService(BuildConfig.BASE_URL)
    }
    
    @Provides
    @Singleton
    fun provideHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(LoggingInterceptor())
            .build()
    }
}

// ==== 测试 Module ====

@Module
@InstallIn(SingletonComponent::class)
object TestNetworkModule {
    @Provides
    @Singleton
    fun provideApi(): ApiService {
        return MockApiService()
    }
    
    @Provides
    @Singleton
    fun provideHttpClient(): OkHttpClient {
        return OkHttpClient.Builder().build()
    }
}

// ==== 使用 @UninstallModules ====

@HiltAndroidTest
@UninstallModules(NetworkModule::class)
class NetworkTest {
    
    @get:Rule
    val hiltRule = HiltAndroidRule(this)
    
    @Inject
    lateinit var api: ApiService
    
    @Test
    fun testMockApi() {
        // api 是 MockApiService
        // ...
    }
}

6.2 Qualifier 替换

kotlin
// ==== 使用 Qualifier 区分 ====

// 生产
@Module
@InstallIn(SingletonComponent::class)
object ApiModule {
    @Provides
    @Singleton
    @Named("production")
    fun provideApi(): ApiService {
        return RealApiService()
    }
}

// 测试
@Module
@InstallIn(SingletonComponent::class)
object TestApiModule {
    @Provides
    @Singleton
    @Named("production")
    fun provideApi(): ApiService {
        return MockApiService()
    }
}

// 使用
class Repository @Inject constructor(
    @Named("production") private val api: ApiService
)

6.3 BuildConfig 切换

kotlin
// ==== 使用 BuildConfig 切换实现 ====

@Module
@InstallIn(SingletonComponent::class)
object ApiModule {
    @Provides
    @Singleton
    fun provideApi(): ApiService {
        return if (BuildConfig.DEBUG) {
            MockApiService()  // 调试时使用 Mock
        } else {
            RealApiService()  // 生产时使用真实
        }
    }
}

// ==== 使用 Flavor 切换 ====

// debug 版本
// src/debug/java/.../DebugApiModule.kt
@Module
@InstallIn(SingletonComponent::class)
object DebugApiModule {
    @Provides
    @Singleton
    fun provideApi(): ApiService = MockApiService()
}

// release 版本
// src/release/java/.../ReleaseApiModule.kt
@Module
@InstallIn(SingletonComponent::class)
object ReleaseApiModule {
    @Provides
    @Singleton
    fun provideApi(): ApiService = RealApiService()
}

6.4 环境变量切换

kotlin
// ==== 使用环境变量 ====

object ApiFactory {
    fun create(): ApiService {
        val useMock = System.getProperty("test.use_mock") == "true"
        return if (useMock) {
            MockApiService()
        } else {
            RealApiService()
        }
    }
}

// 测试时
// java -Dtest.use_mock=true ...

// ==== 使用 ServiceLoader ====

// 定义接口
interface ApiProvider {
    fun create(): ApiService
}

// 生产实现
class RealApiProvider : ApiProvider {
    override fun create(): ApiService = RealApiService()
}

// 测试实现
class MockApiProvider : ApiProvider {
    override fun create(): ApiService = MockApiService()
}

// 使用 ServiceLoader
object ApiFactory {
    fun create(): ApiService {
        return ServiceLoader.load(ApiProvider::class.java)
            .first()
            .create()
    }
}

// META-INF/services/com.example.ApiProvider
// 生产:com.example.RealApiProvider
// 测试:com.example.MockApiProvider

七、面试考点

7.1 基础问题

Q1: 为什么 DI 提升可测试性?

A:

1. 依赖可替换
   - 可以用 Mock 替换真实依赖
   - 无需真实网络、数据库

2. 依赖隔离
   - 可以单独测试每个组件
   - 减少测试依赖

3. 易于 Mock
   - 接口注入便于创建 Mock
   - 可以验证交互

4. 测试速度快
   - 单元测试无需启动应用
   - 无需真实设备

Q2: Mock 和 Stub 的区别?

A:

Mock:
- 验证交互(是否调用了某个方法)
- 关注行为
- 用于验证

Stub:
- 提供固定返回值
- 关注状态
- 用于设置测试数据

例子:
val mock = mockk<Api>()
every { mock.getUser(1) } returns User("test")  // Stub 行为
verify { mock.getUser(1) }  // Mock 验证

Q3: Hilt 测试的核心注解有哪些?

A:

@HiltAndroidTest - 标记测试类
@HiltAndroidRule - 测试规则
@UninstallModules - 卸载生产 Module
@BindValue - 绑定测试依赖

7.2 进阶问题

Q4: Hilt 测试如何替换依赖?

A:

kotlin
// 方法 1:@UninstallModules
@HiltAndroidTest
@UninstallModules(NetworkModule::class)
class Test {
    @BindValue
    val api: ApiService = MockApiService()
}

// 方法 2:@BindValue
@HiltAndroidTest
class Test {
    @BindValue
    val mockRepo: UserRepository = MockUserRepository()
}

// 方法 3:测试 Module
@Module
@InstallIn(SingletonComponent::class)
object TestModule {
    @Provides fun provideApi(): ApiService = MockApiService()
}

Q5: Koin 测试如何配置?

A:

kotlin
@get:Rule
val koinTestRule = KoinTestRule.create {
    modules(module {
        single { mockk<UserApi>() }
        single { UserRepository(get()) }
    })
}

@Test
fun test() {
    val repository = get<UserRepository>()
    // ...
}

Q6: 什么是 Instrumentation 测试?

A:

Instrumentation 测试:
- 在真实设备/模拟器上运行
- 可以测试 UI 交互
- 可以测试完整流程
- 速度较慢
- 需要 Android 环境

对比单元测试:
- 单元测试:JVM 上运行,快速,隔离
- Instrumentation:设备上运行,慢,集成

7.3 高级问题

Q7: 如何测试 ViewModel?

A:

kotlin
// Hilt 方式
@HiltAndroidTest
class ViewModelTest {
    @get:Rule val hiltRule = HiltAndroidRule(this)
    
    @BindValue
    val mockRepo = MockUserRepository()
    
    @Inject lateinit var viewModel: MainViewModel
    
    @Test
    fun testViewModel() {
        every { mockRepo.getUser(1) } returns User("test")
        
        viewModel.loadUser(1)
        
        // 验证 LiveData
        val observer = Observer<User> { }
        viewModel.user.observeForever(observer)
        verify { mockRepo.getUser(1) }
    }
}

// 纯单元测试
class ViewModelTest {
    @Test
    fun testViewModel() {
        val mockRepo = MockUserRepository()
        val viewModel = MainViewModel(mockRepo)
        
        // 测试...
    }
}

Q8: 如何使用 MockWebServer 测试 API?

A:

kotlin
@HiltAndroidTest
class ApiTest {
    private val mockWebServer = MockWebServer()
    
    @Before
    fun setup() {
        mockWebServer.start()
        // 配置 API 使用 MockWebServer
    }
    
    @After
    fun teardown() {
        mockWebServer.shutdown()
    }
    
    @Test
    fun testApi() {
        // 设置 Mock 响应
        mockWebServer.enqueue(MockResponse().setBody("""{"id": 1}"""))
        
        // 执行请求
        val result = api.getUser(1)
        
        // 验证
        assertEquals(1, result.id)
        
        // 验证请求
        val request = mockWebServer.takeRequest()
        assertEquals("/users/1", request.path)
    }
}

Q9: 测试中的依赖注入顺序?

A:

Hilt 测试顺序:
1. @HiltAndroidRule 初始化
2. @UninstallModules 卸载生产 Module
3. @BindValue 绑定测试依赖
4. hiltRule.inject() 执行注入
5. 测试执行

Koin 测试顺序:
1. KoinTestRule.create { } 配置
2. startKoin { } 启动
3. get<T>() 获取依赖
4. 测试执行
5. stopKoin() 清理

Q10: 如何测试 Room 数据库?

A:

kotlin
@HiltAndroidTest
class DatabaseTest {
    private lateinit var database: AppDatabase
    
    @Before
    fun setup() {
        // 创建内存数据库
        database = Room.inMemoryDatabaseBuilder(
            context,
            AppDatabase::class.java
        ).build()
    }
    
    @After
    fun teardown() {
        database.close()
    }
    
    @Test
    fun testInsertAndQuery() {
        val dao = database.userDao()
        dao.insert(User(1, "test"))
        
        val result = dao.getById(1)
        assertEquals("test", result?.name)
    }
}

八、最佳实践总结

8.1 推荐实践 ✅

kotlin
// 1. 优先使用构造函数注入
class Repository @Inject constructor(private val api: Api)

// 2. 依赖接口而非实现
interface UserRepository
class UserRepositoryImpl @Inject constructor(...) : UserRepository

// 3. 使用 MockK 进行 Mock
val mock = mockk<Api>()
every { mock.get() } returns result

// 4. 隔离单元测试
@Test
fun testUnit() {
    val mock = MockApi()
    val repo = Repository(mock)
    // 只测试 Repository 逻辑
}

// 5. Hilt 测试使用 @BindValue
@BindValue
val mockRepo: UserRepository = MockUserRepository()

// 6. Koin 测试使用 KoinTestRule
@get:Rule
val koinTestRule = KoinTestRule.create { modules(testModule) }

8.2 避免的陷阱 ❌

kotlin
// 1. 不要测试框架本身
@Test
fun testHilt() {
    // ❌ 不要测试 Hilt 是否工作
    // ✅ 测试你的业务逻辑
}

// 2. 不要在单元测试中使用真实依赖
@Test
fun testWithRealApi() {
    val api = RealApi()  // ❌
    // ✅ 使用 Mock
}

// 3. 不要忘记清理
@After
fun teardown() {
    stopKoin()  // ✅ Koin 需要清理
    database.close()  // ✅ Room 需要关闭
}

// 4. 不要过度 Mock
@Test
fun testWithTooManyMocks() {
    val mock1 = mockk<A>()
    val mock2 = mockk<B>()
    val mock3 = mockk<C>()
    // ... 10 个 Mock
    // ❌ 可能设计有问题,考虑重构
}

九、参考资料

官方文档

进阶阅读

视频资源