29、Jetpack测试:单元测试、UI测试、Hilt测试与Room测试
说实话,测试这个话题,很多Android开发者都绕道走。我刚入行那会儿也觉得,写测试干嘛?浪费时间。直到有一次,我改了一个工具类里的日期格式化方法,结果导致三个业务模块的订单时间全部显示异常。线上bug炸了,我加班到凌晨三点才定位到问题。嗯,从那以后,我再也不敢不写测试了。
Jetpack架构组件给我们提供了非常完善的测试支持。今天我就带你把这套东西捋清楚。说白了,测试就分几类:单元测试测逻辑、UI测试测界面、Hilt测试测依赖注入、Room测试测数据库。咱们一个一个来。
29.1 单元测试:最基础的防线
单元测试,说白了就是测你的函数对不对。我个人习惯,每个ViewModel、每个工具类,都要配上单元测试。你想想看,如果连最基础的业务逻辑都没验证过,你怎么敢上线?
核心原则:单元测试要快、要独立、要可重复。不要依赖Android框架,尽量用纯JVM测试。
举个例子,假设我们有一个计算订单总价的函数:
// 待测试的类
class OrderCalculator {
fun calculateTotal(price: Double, quantity: Int, discount: Double): Double {
val subtotal = price * quantity
val discountedAmount = subtotal * discount
return subtotal - discountedAmount
}
}
// 单元测试
class OrderCalculatorTest {
private val calculator = OrderCalculator()
@Test
fun `calculateTotal should return correct value`() {
val result = calculator.calculateTotal(100.0, 2, 0.1)
assertEquals(180.0, result, 0.001)
}
@Test
fun `calculateTotal with zero discount`() {
val result = calculator.calculateTotal(50.0, 3, 0.0)
assertEquals(150.0, result, 0.001)
}
}
我在项目中遇到过一个问题:测试里用了浮点数比较,结果因为精度问题,测试时好时坏。后来我统一用了assertEquals的第三个参数——delta,才彻底解决。避坑指南:浮点数比较一定要给误差范围。
29.2 UI测试:用Compose Test验证界面
UI测试,说白了就是模拟用户操作,看界面反应对不对。以前用Espresso写UI测试,那叫一个痛苦。现在有了Compose Test,舒服多了。
我的习惯:UI测试不要写太多,覆盖核心用户路径就行。比如登录、下单、支付这些关键流程。写太多UI测试,维护成本会很高。
// 一个简单的Compose UI测试
@RunWith(AndroidJUnit4::class)
class LoginScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun `login button should be disabled when fields are empty`() {
composeTestRule.setContent {
LoginScreen()
}
// 验证登录按钮初始状态为禁用
composeTestRule
.onNodeWithText("登录")
.assertIsNotEnabled()
// 输入用户名和密码
composeTestRule
.onNodeWithTag("username_field")
.performTextInput("test@example.com")
composeTestRule
.onNodeWithTag("password_field")
.performTextInput("password123")
// 验证登录按钮变为可用
composeTestRule
.onNodeWithText("登录")
.assertIsEnabled()
}
}
为什么会这样?因为Compose Test直接操作Composable节点,不需要像Espresso那样找ID、等异步。你想想看,这省了多少事。
29.3 Hilt测试:依赖注入的测试之道
Hilt测试,说白了就是测试你的依赖注入对不对。我记得刚开始用Dagger的时候,测试配置能写一整天。Hilt在这方面做了大量优化。
注意:Hilt测试需要添加@HiltAndroidTest注解,并且测试类要用@RunWith(AndroidJUnit4::class)运行。
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class UserRepositoryTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Inject
lateinit var userRepository: UserRepository
@Inject
lateinit var userDao: UserDao
@Before
fun setup() {
hiltRule.inject()
}
@Test
fun `getUser should return cached user after first fetch`() {
// 模拟网络请求
val testUser = User(id = 1, name = "Test User")
// 第一次调用,应该从网络获取
val user1 = userRepository.getUser(1)
// 第二次调用,应该从缓存获取
val user2 = userRepository.getUser(1)
assertEquals(testUser, user1)
assertEquals(testUser, user2)
}
}
我曾经踩过一个坑:Hilt测试里忘了加@HiltAndroidTest注解,结果依赖注入一直失败,排查了半天。嗯,这里要注意,注解一个都不能少。
29.4 Room测试:数据库的可靠性保障
Room测试,说白了就是验证你的数据库操作对不对。数据库这东西,一旦出问题,数据就乱了。所以Room测试我建议写得细一点。
核心要点:Room测试要用内存数据库,不要用真数据库文件。这样测试跑得快,而且每次都是干净的。
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
private lateinit var userDao: UserDao
private lateinit var database: AppDatabase
@Before
fun setup() {
// 创建内存数据库
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java
).build()
userDao = database.userDao()
}
@After
fun teardown() {
database.close()
}
@Test
fun `insert and retrieve user`() {
val user = User(
id = 1,
name = "张三",
email = "zhangsan@example.com"
)
userDao.insertUser(user)
val retrievedUser = userDao.getUserById(1)
assertEquals(user.name, retrievedUser?.name)
assertEquals(user.email, retrievedUser?.email)
}
@Test
fun `delete user should remove from database`() {
val user = User(id = 1, name = "测试用户")
userDao.insertUser(user)
userDao.deleteUser(user)
val result = userDao.getUserById(1)
assertNull(result)
}
}
我在项目中遇到过一个问题:Room测试里用了LiveData,结果测试一直不结束。后来发现是LiveData的观察者没清理。避坑指南:用LiveData做测试时,记得用getOrAwaitValue()这类扩展函数,或者直接用Kotlin Flow。
29.5 测试覆盖率:别盲目追求数字
很多团队把测试覆盖率当成KPI,要求必须达到80%甚至90%。我个人觉得,这有点本末倒置。你想想看,有些工具类、数据类,测试覆盖率再高也没意义。关键是要把核心业务逻辑覆盖到。
| 测试类型 | 推荐覆盖率 | 重点关注 |
|---|---|---|
| 单元测试 | 70%-80% | ViewModel、工具类、业务逻辑 |
| UI测试 | 20%-30% | 核心用户路径 |
| Hilt测试 | 50%-60% | 依赖注入配置、模块集成 |
| Room测试 | 80%-90% | DAO操作、数据迁移 |
我的建议:先写单元测试和Room测试,这两个性价比最高。UI测试和Hilt测试可以等核心功能稳定了再补。别一上来就想把所有测试都写完,那样你会崩溃的。
29.6 知识体系总览
下面这张图,我把整个Jetpack测试的知识体系梳理了一下。你可以把它当成一个检查清单,看看自己哪些地方还没覆盖到。
这张图把测试分成了四大块,每一块都有对应的工具和关注点。你写测试的时候,可以对照着看看,哪些地方还没覆盖到。
最后说一句:测试不是银弹,但它能帮你省下大量排查bug的时间。我现在的习惯是,写完一个功能,先跑一遍单元测试,再跑一遍Room测试,最后才考虑UI测试。这个顺序,能让你最快发现问题。