1. 测试夹具基础概念

1.1 什么是测试夹具

测试夹具(Test Fixture) 是指为执行测试而准备的固定环境与资源集合。在 Android 测试语境中,夹具涵盖了被测对象实例、依赖注入容器、数据库状态、文件系统快照、网络模拟端点以及配置参数等。夹具不是测试逻辑本身,而是让测试逻辑能够稳定、重复、独立运行的前提条件。

维度 解释 Android 示例
物理资源 文件、数据库、SharedPreferences mock 一个 SQLite 数据库,注入测试数据
环境状态 系统语言、网络状况、传感器 使用 ConfigurationContext 设置地区
依赖服务 网络 API、Binder、ContentProvider 用 MockWebServer 模拟 HTTP 响应
对象图 DI 容器中的 Bean 组装 Dagger/Hilt 测试模块替换生产实现
核心原则:测试夹具要做到“可重复构造、独立隔离、快速拆卸”。每次测试执行后夹具应自动清理,避免状态泄漏影响其他用例。

1.2 测试夹具的作用与价值

  • 消除不确定性:固定时间、固定数据、固定依赖,使测试结果仅由被测代码决定。
  • 提升执行速度:预置内存对象而非每次重新创建进程或启动 Activity。
  • 隔离测试用例:每个用例拥有独立的夹具副本,不共享可变状态。
  • 简化断言逻辑:夹具已知输入,输出只需比对预期值,无需处理环境噪声。
  • 支持参数化:同一套夹具结构可注入不同数据组合,驱动大量边界用例。
经验要点:在 Android 测试中,约 70% 的测试失败源自夹具构造不当(如未清理数据库、未重置 Mock 对象),而非被测代码缺陷。优先把夹具的稳定性和隔离性做到位,能显著降低维护成本。

1.3 测试夹具与测试代码的关系

测试夹具和测试代码是“准备 — 执行 — 验证 — 拆卸”四阶段中的前、后两段。夹具负责第 1 和第 4 阶段,测试代码负责第 2、3 阶段。两者通过依赖注入或回调接口连接,不应互相耦合。

阶段 责任主体 典型操作
SetUp(准备) 测试夹具 创建对象、插入数据、启动 Mock Server
Exercise(执行) 测试代码 调用被测方法、触发事件
Verify(验证) 测试代码 断言返回值、验证交互次数
TearDown(拆卸) 测试夹具 关闭数据库、释放锁、清空缓存
// 测试夹具与测试代码分离的典型结构
class UserRepositoryTest {
    // —— 夹具开始 ——
    private lateinit var database: AppDatabase
    private lateinit var api: MockWebServer

    @Before
    fun setUp() {
        database = Room.inMemoryDatabaseBuilder(
            InstrumentationRegistry.getInstrumentation().targetContext,
            AppDatabase::class.java
        ).build()
        api = MockWebServer()
        api.start()
    }

    @After
    fun tearDown() {
        database.close()
        api.shutdown()
    }
    // —— 夹具结束 ——

    // —— 测试代码 ——
    @Test
    fun testSaveUser() {
        val user = User(id = 1, name = "Alice")
        database.userDao().insert(user)
        val loaded = database.userDao().getById(1)
        assertEquals("Alice", loaded.name)
    }
}
常见错误:在测试代码中直接 new 依赖对象、硬编码文件路径或依赖静态全局状态,会导致夹具与测试逻辑紧耦合,一旦环境变化就需要大量修改测试代码。应始终通过构造器注入或测试模块提供夹具。

1.4 Android 测试层次中的夹具定位

Android 测试从上到下分为 UI 测试、集成测试、单元测试 三个层次。不同层次的夹具复杂度和作用域差异很大:

  • 单元测试:夹具最小,通常只包含一个类实例 + Mock 依赖。运行在 JVM 上,不依赖真机或模拟器。
  • 集成测试:夹具涉及真实数据库、文件系统或网络端点。运行在 Android 设备或模拟器上,使用 AndroidJUnitRunner。
  • UI 测试:夹具需要启动 Activity、注入 Intent、配置系统权限。常借助 ActivityScenario 和 ComposeTestRule。
分层策略:尽量把业务逻辑下沉到单元测试,用轻量夹具覆盖;集成测试和 UI 测试只验证跨越边界的行为,避免为每一行代码都准备重量级夹具。
Android 测试层次与测试夹具关系图 UI 测试 Espresso / Compose Test Rule 夹具:ActivityScenario Intent / 权限 / 系统栏 MockWebServer 网络端点 集成测试 AndroidJUnitRunner 夹具:Room 内存库 SQLite / ContentProvider 文件系统 临时目录 单元测试 JUnit 5 / MockK / Mockito 夹具:构造器注入 Mock 对象 / 协程调度器 测试数据 Data 类 / 枚举 关键原则 每层测试都有特定夹具类型:上层依赖真实组件多,下层依赖 Mock 多 夹具应始终与测试代码分离,通过 @Before / @After 管理生命周期

1.5 Android 测试夹具的典型实现模式

在实际项目中,夹具设计有三种主流实现模式,开发者应根据测试层次和团队规范选择:

模式 适用层次 实现方式 优点 缺点
Implicit Fixture 单元测试 在 @Before 中集中构造,所有用例共享 代码量少,适合通用依赖 用例间可能耦合,修改影响范围大
Explicit Fixture 所有层次 每个测试方法自包含构造代码 完全隔离,容易阅读 重复代码多,维护成本高
Factory/Build Pattern 集成测试、UI 测试 使用 ObjectMother 或 TestDataBuilder 复用性强,组合灵活 需要额外工厂类,初期搭建较慢
// Factory/Build Pattern 示例:ObjectMother 风格
object TestUsers {
    fun alice() = User(
        id = 1,
        name = "Alice",
        email = "alice@example.com",
        role = Role.ADMIN
    )

    fun bob() = User(
        id = 2,
        name = "Bob",
        email = "bob@example.com",
        role = Role.GUEST
    )

    fun listOfThree() = listOf(alice(), bob(), User(
        id = 3,
        name = "Charlie",
        email = "charlie@example.com",
        role = Role.EDITOR
    ))
}
推荐做法:团队内约定使用 Explicit Fixture + ObjectMother 组合。核心业务数据通过 ObjectMother 构造,而每个测试方法的特殊状态(如异常条件、边界值)显式写在方法内,既保证可读性又避免臃肿的 @Before 块。

1.6 常见错误与稳定性优化

  • 错误:全局静态状态泄漏 — 多个测试共享一个静态 DatabaseHelper 实例,导致数据污染。应使用 inMemoryDatabaseBuilder 每次重新创建。
  • 错误:Fixture 与测试逻辑混合 — 在断言语句中创建文件或网络连接。必须通过 @Before / @After 分离。
  • 错误:忽略 TearDown 异常 — 夹具拆卸中抛异常会掩盖测试失败。使用 @Rule 或 Closeable 接口确保资源释放。
// 使用 @Rule 安全管理夹具资源的推荐写法
@RunWith(AndroidJUnit4::class)
class SafeFixtureTest {
    @get:Rule
    val mockServerRule = MockWebServerRule() // 自动 shutdown

    @get:Rule
    val databaseRule = InMemoryDatabaseRule() // 自动 close

    @Test
    fun testWithSafeFixture() {
        // 直接使用 databaseRule.database 和 mockServerRule.server
        // 无需手动 setUp / tearDown
    }
}
性能警示:在集成测试和 UI 测试中,每次重新创建数据库或启动 MockWebServer 都会产生毫秒级开销。如果夹具构造时间超过测试执行时间的 50%,应考虑使用 @ClassRule 或测试套件级别共享只读夹具,但必须确保共享数据不会被修改。

1.7 本章要点总结

知识点 核心结论
测试夹具定义 为测试执行提供的固定环境与资源集合,包括对象、数据、配置、依赖
测试夹具作用 消除不确定性、提升速度、隔离用例、简化断言、支持参数化
与测试代码的关系 夹具负责 SetUp 和 TearDown,测试代码负责 Exercise 和 Verify,通过构造器注入解耦
Android 测试层次 UI 测试 → 集成测试 → 单元测试,每层夹具复杂度递减,执行速度递增
实现模式选择 单元测试用 Implicit/Explicit,集成和 UI 测试推荐 Factory/Build Pattern
稳定性关键 资源必须释放,状态必须隔离,Fixture 与测试逻辑必须分离
下一阶段预告:第 2 章将深入讲解如何基于 JUnit4/5 的 Rule 体系设计可复用的测试夹具,包括 @Rule、@ClassRule 和自定义 TestWatcher 的实现细节与实战案例。