第4章 @Before与@After注解使用:初始化夹具、清理资源、执行顺序
4.1 注解在Compose测试中的角色
在Compose UI测试中,每个测试用例通常依赖一组可预测的初始状态——例如特定的Activity、Compose节点树、模拟数据源或网络拦截器。@Before 与 @After 注解正是用来管理这些夹具(Fixture)的入口点:前者在每条测试方法执行前运行,执行初始化工作;后者在每条测试方法结束后运行,负责清理资源,防止测试间污染。
JUnit 4/5 生命周期模型与Compose测试框架深度整合,所有标注了 @Before 的方法会在 @Test 方法之前执行,标注了 @After 的方法则在其之后执行。若类中定义了多个 @Before 或 @After 方法,JUnit 将按照继承层级与方法名的确定性顺序执行(通常是父类优先,同一类中按方法名自然排序)。
4.2 核心执行流程
下图展示了带 @Before / @After 注解的Compose测试完整生命周期。注意图中深色节点代表关键步骤,箭头表示顺序依赖关系。
4.3 注解对比:@Before vs @BeforeClass
| 特性 | @Before (JUnit 4) / @BeforeEach (JUnit 5) | @BeforeClass (JUnit 4) / @BeforeAll (JUnit 5) |
|---|---|---|
| 执行次数 | 每条 @Test 执行前均执行 | 整个测试类运行前执行一次 |
| 方法要求 | 非静态(非static) | 静态(static) |
| 用途 | 初始化局部夹具(如新建Compose节点、重置模拟对象) | 初始化全局资源(如启动Mock服务器、加载配置文件) |
| 示例焦点 | 创建 ComposeTestRule,设置 content | 启动 Ktor MockEngine,装载路由 |
4.4 代码实现:标准夹具初始化与清理
以下示例展示如何在Compose测试类中通过 @Before 创建测试规则与Activity,并在 @After 中释放资源。为清晰起见,代码直接使用JUnit 4的注解风格,JUnit 5的 @BeforeEach/@AfterEach 语法同理。
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LoginScreenTest {
@get:Rule
val composeTestRule = createAndroidComposeRule()
// 测试专用的模拟数据源
private lateinit var mockApi: MockApiService
@Before
fun setUp() {
// 1. 初始化模拟API,在测试前替换真实网络层
mockApi = MockApiService()
mockApi.enqueueLoginSuccess()
// 2. 通过ComposeTestRule设置测试内容(如果未使用ActivityScenarioRule)
// 此处已经通过createAndroidComposeRule自动启动了MainActivity
// 3. 等待Compose树渲染完毕
composeTestRule.waitForIdle()
}
@After
fun tearDown() {
// 1. 清理模拟API,释放网络资源
mockApi.shutdown()
// 2. 重置Compose树状态(如有必要)
composeTestRule.runOnIdle {
// 例如清除SharedPreferences或数据库残留
}
}
@Test
fun whenLoginButtonClicked_shouldShowHome() {
composeTestRule.onNodeWithText("登录").performClick()
composeTestRule.onNodeWithText("首页").assertIsDisplayed()
}
}
关键实践: 始终在
@After 中释放可关闭的资源(如 MockWebServer、OkHttpClient、数据库连接)。如果漏掉清理,轻则测试间数据泄漏,重则导致后续测试因端口占用或状态脏写而失败。
4.5 多注解执行顺序:调试与验证
当测试类中定义了多个 @Before 或 @After 方法时,执行顺序遵循以下原则:
- 继承链优先:父类的
@Before在所有子类@Before之前执行;@After则相反,子类先执行,父类后执行。 - 同一类内按方法名排序:通过
Method.getName()的自然字典序执行。
建议在所有 @Before 和 @After 方法的第一行添加日志输出,便于在测试报告或Logcat中回溯执行路径。例如:
@Before
fun initDatabase() {
Log.d("TestLifecycle", "[BEFORE] initDatabase")
// ...
}
@After
fun closeDatabase() {
Log.d("TestLifecycle", "[AFTER] closeDatabase")
}
4.6 常见错误与解决方案
| 错误现象 | 根本原因 | 解决方案 |
|---|---|---|
| java.lang.Exception: Method setUp() should be void | @Before 方法返回了非void类型 | 确保方法签名返回类型为 void,也不要有参数 |
| NullPointerException 在 @Test 中访问mocked对象 | @Before 内部初始化未完成或顺序错乱 | 检查是否在 @Before 中调用了 waitForIdle();若使用 MockitoAnnotations.openMocks 须确保在 @Before 开始时执行 |
| 资源泄漏:端口已被占用 | @After 未关闭 MockWebServer 或 HttpServer | 在 @After 中显式调用 mockServer.shutdown(),并使用 try-finally 包裹清理代码 |
| ComposeTestRule 状态冲突:前一条测试的节点残留 | @After 未重置 Compose 内容 | 在 @After 中调用 composeTestRule.setContent { EmptyComposable() } 或重置 ViewModel |
注意: 在
@After 中释放网络或磁盘资源时,务必捕获异常并记录日志,避免因清理失败导致后续测试全部中断。推荐使用 try { ... } catch (e: Exception) { Log.w(...) } 模式。
4.7 性能与稳定性优化建议
- 避免耗时操作:
@Before与@After会为每条测试重复执行,应只包含轻量初始化(如创建普通对象、设置模拟),将重型操作(如启动外部服务、加载大数据集)移到@BeforeClass。 - 使用 @Rule 或 @RegisterExtension:对于需要跨测试共享的夹具(如
GrantPermissionRule、MockWebServer),优先通过JUnit规则注入,规则自身的before()/after()生命周期更稳定且不会被意外覆盖。 - 超时保护:为每个
@Test添加超时注解,防止@Before或@After中的死锁阻塞整个测试套件:@Test(timeout = 3000)。 - Compose 专用优化:在
@Before中调用composeTestRule.waitForIdle()以确保 Compose 树完全渲染后再执行断言,否则可能导致异步副作用干扰。
4.8 实战案例:模拟登录测试
下面是一个紧凑的完整测试类,集成了 @Before/@After、MockWebServer 和 ComposeTestRule。注意 @After 中关闭服务器与清除缓存的操作。
@RunWith(AndroidJUnit4::class)
class LoginE2ETest {
@get:Rule
val composeTestRule = createAndroidComposeRule()
private val mockServer = MockWebServer()
@Before
fun setUp() {
mockServer.start(8080)
mockServer.enqueue(MockResponse().setBody("{\"\":\"abc\"}"))
// 替换应用中的BaseUrl
TestAppModule.baseUrl = mockServer.url("/").toString()
composeTestRule.waitForIdle()
}
@After
fun tearDown() {
mockServer.shutdown()
// 清除应用缓存
composeTestRule.runOnUiThread {
SharedPrefsUtil.clearAll()
}
}
@Test
fun successfulLoginNavigatesToDashboard() {
composeTestRule.onNodeWithTag("username_input").performTextInput("user@test.com")
composeTestRule.onNodeWithTag("password_input").performTextInput("test123")
composeTestRule.onNodeWithTag("login_button").performClick()
composeTestRule.onNodeWithText("仪表盘").assertIsDisplayed()
}
}
小结:
@Before 与 @After 是Compose测试用例的“地基”与“清道夫”。扎实地理解其执行顺序、资源管理原则及常见的错误模式,能有效降低测试维护成本,提升测试套件的稳定性和执行速度。下一章将深入到 参数化测试与数据驱动,进一步减少重复代码,覆盖更多边界场景。