Appearance
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
// ❌ 可能设计有问题,考虑重构
}