第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测试完整生命周期。注意图中深色节点代表关键步骤,箭头表示顺序依赖关系。

Compose 测试夹具生命周期 @BeforeClass @Before (非静态) @Test @After (非静态) @AfterClass 测试类实例创建 每条 @Test 间重复执行

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测试用例的“地基”与“清道夫”。扎实地理解其执行顺序、资源管理原则及常见的错误模式,能有效降低测试维护成本,提升测试套件的稳定性和执行速度。下一章将深入到 参数化测试与数据驱动,进一步减少重复代码,覆盖更多边界场景。