单元测试作为Android应用质量保障的第一道防线,专注于验证独立组件的逻辑正确性,尤其在MVVM架构中,ViewModel层的业务逻辑测试至关重要。通过JUnit与Mockito的组合,可有效隔离依赖、模拟交互行为,确保核心功能如数据处理、状态管理等符合预期。实践表明,优先实施单元测试的开发流程能使代码质量提升35%,并显著降低发布后缺陷率[31]。
核心测试工具链
单元测试依赖以下工具构建完整测试环境:
@RunWith与JUnit 5的@ExtendWith支持测试规则扩展。 @Mock生成模拟对象(如Repository),@InjectMocks自动注入依赖,verify方法验证交互行为,解决外部依赖耦合问题。 runTest函数简化协程代码测试,自动管理调度器,确保异步逻辑同步执行。 ViewModel测试实践
以任务管理应用的TasksViewModel为例,验证"添加任务"功能的核心逻辑——确保调用addTask方法时,Repository的saveTask被正确触发。
测试三步骤
Task对象),通过@Mock初始化Repository依赖,@InjectMocks实例化ViewModel。 addTask方法,传入测试数据。 verify确认Repository的saveTask方法被调用且参数正确。@RunWith(MockitoJUnitRunner::class)
class TasksViewModelTest {
@Mock
private lateinit var repository: TasksRepository // 模拟仓库依赖
@InjectMocks
private lateinit var viewModel: TasksViewModel // 注入模拟依赖到ViewModel
@get:Rule
val instantTaskRule = InstantTaskExecutorRule() // 同步LiveData测试
@Test
fun `addTask inserts task to repository`() = runTest { // 协程测试作用域
// 准备测试数据
val task = Task(title = "Test Task", description = "Test Description")
// 执行测试方法
viewModel.addTask(task)
// 验证仓库的saveTask方法被调用,参数为测试任务
verify(repository).saveTask(task)
}
@Test
fun `loadTasks returns non-empty list`() = runTest {
// 模拟仓库返回数据
val mockTasks = listOf(Task("Task 1", ""), Task("Task 2", ""))
`when`(repository.getTasks()).thenReturn(mockTasks)
// 执行加载任务
viewModel.loadTasks()
// 验证LiveData值是否正确更新
assertThat(viewModel.tasks.value, `is`(not(emptyList())))
assertThat(viewModel.tasks.value?.size, `is`(2))
}
}
测试驱动开发(TDD)整合
采用TDD的"红-绿-重构"循环可进一步提升测试质量:
UI测试验证用户界面的交互正确性,Android提供Espresso(传统View测试)与Compose Test(Jetpack Compose测试)两套方案,分别适用于不同UI架构。
Espresso是Google官方UI自动化测试框架,以简洁API和同步机制著称,通过onView()定位控件、perform()执行操作、check()验证结果,确保测试可靠[33][34]。
核心组成与工作原理
Espresso架构包含四大组件:
onView()(普通View)或onData()(AdapterView)获取控件。 withId()(ID匹配)、withText()(文本匹配),支持allOf()组合多条件解决控件歧义。 click()(点击)、typeText()(输入文本)、scrollTo()(滚动)。 matches(isDisplayed())(是否显示)、matches(withText("预期文本"))(文本是否一致)。环境配置与基础示例
在build.gradle添加依赖:
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
测试登录按钮点击后导航到首页的场景:
@RunWith(AndroidJUnit4::class)
class LoginEspressoTest {
@get:Rule
val activityRule = ActivityScenarioRule<LoginActivity>() // 启动登录页
@Test
fun `click login button navigates to home`() {
// 输入用户名密码
onView(withId(R.id.et_username)).perform(typeText("test_user"))
onView(withId(R.id.et_password)).perform(typeText("test_pwd"), closeSoftKeyboard())
// 点击登录按钮
onView(withId(R.id.btn_login)).perform(click())
// 验证导航到首页(通过首页特有控件判断)
onView(withId(R.id.home_toolbar)).check(matches(isDisplayed()))
}
}
高级应用技巧
allOf()组合属性:
onView(allOf(withId(R.id.btn_submit), withText("提交"))).check(matches(isDisplayed()))
onData()加载,如验证Spinner选项:
onData(allOf(`is`(instanceOf(String::class.java)), `is`("选项1")))
.inAdapterView(withId(R.id.spinner))
.perform(click())
Espresso-Contrib扩展支持MockWebServer(网络模拟)和Room数据库操作,隔离外部依赖[34]。Compose Test专为声明式UI设计,基于语义树(Semantics)定位可组合函数,语法更直观,支持组件隔离测试,执行速度比Espresso更快[21]。
核心优势与测试规则
相比Espresso,Compose Test具备以下特点:
LoginButton),无需启动整个Activity。 onNodeWithText)、测试标签(onNodeWithTag)或内容描述定位控件。测试需使用ComposeTestRule,通过createComposeRule()(本地测试)或createAndroidComposeRule<Activity>()(依赖Activity)初始化环境。
登录场景测试示例
验证登录按钮点击后导航到首页(Compose版本):
@RunWith(AndroidJUnit4::class)
class LoginComposeTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<LoginActivity>() // 启动登录Activity
@Test
fun `click login button navigates to home`() {
// 设置Compose内容(若直接测试可组合函数,无需依赖Activity)
composeTestRule.setContent {
LoginScreen(
onLoginClick = { /* 模拟登录逻辑 */ },
navController = rememberNavController()
)
}
// 输入用户名密码(通过文本标签定位输入框)
composeTestRule.onNodeWithTag("username_field")
.performTextInput("test_user")
composeTestRule.onNodeWithTag("password_field")
.performTextInput("test_pwd")
// 点击登录按钮(通过内容描述定位)
composeTestRule.onNodeWithContentDescription("登录按钮")
.performClick()
// 验证导航到首页(检查首页标题是否显示)
composeTestRule.onNodeWithText("首页").assertIsDisplayed()
}
}
组件隔离测试示例
单独测试登录按钮的禁用状态(无依赖Activity):
@Test
fun `login button is disabled when fields are empty`() {
composeTestRule.setContent {
LoginButton(
isEnabled = false,
onClick = {}
)
}
// 断言按钮禁用且文本正确
composeTestRule.onNodeWithText("登录")
.assertIsNotEnabled()
.assertHasText("登录")
}
| 维度 | Espresso | Compose Test |
|---|---|---|
| 适用场景 | XML布局的传统View体系 | Jetpack Compose声明式UI |
| 定位方式 | 基于View树,依赖ID、文本等属性 | 基于语义树,支持标签、内容描述 |
| 代码简洁性 | 命令式链式调用,需处理控件歧义 | 声明式语法,直接调用可组合函数 |
| 执行效率 | 依赖Android系统渲染,速度较慢 | 直接操作Compose树,无需系统渲染 |
| 隔离性 | 需启动Activity,难以单独测试控件 | 支持独立测试单个可组合函数 |
UI测试最佳实践
testTag标记关键控件(如Modifier.testTag("login_btn")),避免因文本变化导致测试失败。集成测试聚焦模块间协作逻辑,如Repository层从网络获取数据并更新本地数据库的完整流程,需模拟接近真实的运行环境,验证数据流的正确性。
集成测试覆盖以下关键场景:
测试工具包括:
assertThat(data).isNotNull()。测试Repository从网络获取任务列表并更新本地数据库的流程:
1. 测试准备
InstantTaskExecutorRule和MockWebServer规则。2. 测试执行
fetchTasks()方法。 3. 结果验证
@RunWith(AndroidJUnit4::class)
class TasksRepositoryIntegrationTest {
@get:Rule
val instantTaskRule = InstantTaskExecutorRule() // 同步LiveData
@get:Rule
val mockWebServer = MockWebServer() // 模拟网络
private lateinit var db: AppDatabase
private lateinit var apiService: TaskApiService
private lateinit var repository: TasksRepository
@Before
fun setup() {
// 配置Retrofit指向MockWebServer
apiService = Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(TaskApiService::class.java)
// 初始化内存数据库
db = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java
).allowMainThreadQueries().build() // 测试中允许主线程数据库操作
repository = TasksRepository(apiService, db.taskDao())
}
@After
fun teardown() {
db.close()
mockWebServer.shutdown()
}
@Test
fun `fetchTasks updates local database`() = runTest {
// 1. 准备:MockWebServer返回测试数据
val mockResponse = MockResponse()
.setBody("""[{"id":1,"title":"Network Task","description":"From server"}]""")
.setResponseCode(200)
mockWebServer.enqueue(mockResponse)
// 2. 执行:调用仓库获取数据
repository.fetchTasks()
// 3. 验证:本地数据库是否更新
val savedTasks = db.taskDao().getAll()
assertThat(savedTasks.size, `is`(1))
assertThat(savedTasks[0].title, `is`("Network Task"))
// 验证LiveData数据更新
assertThat(repository.tasksFlow.first().size, `is`(1))
}
}
AndroidJUnitRunner确保测试在Android环境运行,避免纯JVM测试遗漏平台特性。 mock flavor)替换真实网络/数据库为模拟实现,加速测试执行[24]。 Android测试需构建"单元测试-集成测试-UI测试"三级保障体系:
{
"legend": {
"data": [
"单元测试",
"UI测试",
"集成测试"
],
"left": "center",
"textStyle": {
"fontSize": 14
},
"top": "90%"
},
"series": [
{
"center": [
"50%",
"60%"
],
"data": [
{
"name": "单元测试",
"value": 90
},
{
"name": "UI测试",
"value": 20
},
{
"name": "集成测试",
"value": 10
}
],
"endAngle": 360,
"label": {
"formatter": "{b}: {d}%",
"show": true
},
"name": "测试策略",
"radius": [
"40%",
"70%"
],
"startAngle": 180,
"type": "pie"
}
],
"title": {
"left": "center",
"text": "测试策略分布比例",
"textStyle": {
"fontSize": 18
},
"top": 10
},
"tooltip": {
"trigger": "item"
}
}
结合CI/CD流程(如GitHub Actions、Jenkins),在代码提交后自动运行测试套件,可及时发现回归问题。Android Architecture Samples等官方项目提供完整测试示例,建议参考其目录结构(test目录放单元测试,androidTest放UI/集成测试)组织代码[25][26]。
通过系统化测试策略,可显著提升应用稳定性,降低线上故障风险,同时Kotlin语言特性(协程、数据流)与现代测试工具(MockK、Compose Test)的结合,使测试编写更高效、可靠。