第4章:测试模块与主模块分离、源码集配置、测试资源目录规范

Compose 测试夹具要求被测代码与测试代码在物理结构上彻底分离。本章围绕三个核心支柱:模块解耦(主模块只产生产物,测试模块依赖主模块但不污染产物)、源码集配置(通过 Gradle SourceSets 或 CMake 的 PRIVATE/PUBLIC 控制可见性)、测试资源目录规范(统一定位测试专用素材)。完成本章后,你将获得一份在任何 Compose 项目中都可复用的模块骨架。

4.1 为什么强制模块分离

Gradle 多模块下的测试依赖关系遵循以下原则:

  • 主模块(例如 :app 或 :library)只包含业务代码,不引入任何测试依赖。
  • 测试模块(例如 :app:test-fixtures 或 :app:test-utils)通过 testImplementation(project(":app")) 依赖主模块。
  • 测试模块产出的 JAR/AAR 不会被包含在 Release 产物中,实现编译时隔离和运行时隔离。

分离后的依赖示意图如下(本小节后将展示完整 SVG 结构图):

角色包含内容典型 Gradle 配置
主模块生产代码(Compose UI、ViewModel、Data Layer)src/main/kotlin/
测试模块测试夹具、Mock 对象、测试资源、自定义 ComposeTestRulesrc/main/kotlin/(注意这里是 main 而非 test)
测试执行模块集成测试、UI 测试、快照测试src/test/ 或 src/androidTest/
关键原则:测试夹具模块的源码集应使用 src/main 而非 src/test,因为夹具需要被编译为常规产物供其他测试模块引用。只有真正的测试用例才放在 src/test 或 src/androidTest 中。
效果:当 CI 运行 ./gradlew :library:assembleRelease 时,所有测试夹具模块的代码都不会被编译,从而缩短构建流水线。

4.2 Gradle 多模块结构搭建

假设已有模块 :ui-components,我们需要为其创建一个专门的测试夹具模块 :ui-components:test-fixtures。以下是完整的目录结构与关键配置文件。

4.2.1 项目目录树

MyComposeProject/
├── settings.gradle.kts
├── ui-components/
│   ├── build.gradle.kts               # 主模块
│   ├── src/
│   │   ├── main/kotlin/...            # 生产代码
│   │   └── test/kotlin/...            # 单元测试(直接依赖夹具模块)
│   └── test-fixtures/                 # 夹具模块(子目录)
│       ├── build.gradle.kts
│       └── src/
│           ├── main/kotlin/...        # 夹具代码
│           └── main/resources/        # 测试资源(图像、JSON、配置)
└── settings.gradle.kts                # 需要 include 夹具模块

4.2.2 主模块 build.gradle.kts(关键部分)

plugins {
    id("com.android.library")
    id("org.jetbrains.kotlin.android")
}

android {
    namespace = "com.example.ui"
    compileSdk = 34
    defaultConfig { minSdk = 24 }

    // 测试配置:让主模块知道夹具模块的存在(但仅限 testImplementation)
    testOptions {
        unitTests.isReturnDefaultValues = true
    }
}

dependencies {
    // 生产依赖
    implementation("androidx.compose.ui:ui:1.5.0")
    implementation("androidx.compose.material3:material3:1.1.0")

    // 测试依赖(仅 test 源码集)
    testImplementation(project(":ui-components:test-fixtures"))  // 指向夹具模块
    testImplementation("junit:junit:4.13.2")
    testImplementation("org.robolectric:robolectric:4.11")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
}

4.2.3 夹具模块 build.gradle.kts(完整示例)

plugins {
    id("com.android.library")
    id("org.jetbrains.kotlin.android")
}

android {
    namespace = "com.example.ui.testfixtures"
    compileSdk = 34
    defaultConfig {
        minSdk = 24
        // 注意:夹具模块不应有 applicationId
    }
}

dependencies {
    // 通过 api 暴露主模块的生产代码,使夹具可以访问 Compose API
    api(project(":ui-components"))

    // 测试框架依赖(仅用于编译夹具,运行时由最终测试模块提供)
    implementation("androidx.compose.ui:ui-test-junit4:1.5.0")
    implementation("junit:junit:4.13.2")
    // 如果需要 Compose 创建场景,则引入 compose-test-manifest
    implementation("androidx.compose.ui:ui-test-manifest:1.5.0")
}

// 确保夹具模块的源码集编译为 AAR,供其他模块的 testImplementation 使用
android.libraryVariants.all {
    // 可选:将特定资源打包进 AAR
}

4.2.4 settings.gradle.kts 注册

include(":ui-components")
include(":ui-components:test-fixtures")   // 必须显式 include

4.3 源码集配置(SourceSets)

当需要自定义测试资源的存放路径时,Gradle 允许通过 sourceSets 块重定向。以下场景常见:

  • 将测试资源从 src/main/resources 移出到独立目录 src/testResources。
  • 为不同构建类型(debug / release)提供不同的测试夹具。

4.3.1 自定义测试资源目录

// 在夹具模块的 build.gradle.kts 中
android {
    sourceSets {
        getByName("main") {
            resources.srcDirs("src/main/resources", "src/sharedTestResources")
        }
        // 也可以为 test 源码集额外指定
        getByName("test") {
            resources.srcDirs("src/test/resources")
        }
    }
}
常见陷阱:切勿将测试资源放在 src/test/resources 同时又在 src/main/resources 中重复。当夹具模块被其他模块的 testImplementation 引用时,src/main/resources 下的文件会被正确打包进 .jar;而 src/test/resources 下的文件仅对夹具模块自身的测试生效。

4.3.2 CMake 原生项目的等效配置

如果 Compose 项目混编 C++(例如通过 JNI 测试),需在 CMakeLists.txt 中区分源码集:

# 主模块目标
add_library(my_compose_lib SHARED
    src/main/cpp/core.cpp
)

# 测试模块目标(使用 PRIVATE 引入主模块头文件)
add_library(my_test_fixture SHARED
    test-fixtures/src/main/cpp/test_helpers.cpp
)
target_include_directories(my_test_fixture PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp
)
target_link_libraries(my_test_fixture PRIVATE my_compose_lib)

4.4 测试资源目录规范

一个可复用的测试资源目录应遵循以下约定:

路径用途示例
src/main/resources/__files__网络 stub 数据(WireMock)response_user.json
src/main/resources/images测试用图片(100x100 PNG)avatar_placeholder.png
src/main/resources/fonts自定义字体文件Roboto-Regular.ttf
src/main/resources/config测试环境配置文件test_config.yaml
src/main/resources/compiled_compose预编译 Compose 快照(用于基线对比)baseline_component_1.png
性能提示:将大体积测试资源(如高清位图)放在 src/main/resources 中并通过夹具模块暴露。这样当多个测试模块都需要同一张图片时,只需加载一次到内存缓存中,而非每个测试模块各自拷贝一份。

4.5 验证模块分离是否成功

执行以下命令来验证编译隔离:

# 1. 编译主模块 Release(应排除所有测试夹具)
./gradlew :ui-components:assembleRelease

# 2. 编译夹具模块(仅用于检查语法)
./gradlew :ui-components:test-fixtures:compileReleaseKotlin

# 3. 运行主模块单元测试(应能成功引用夹具)
./gradlew :ui-components:testDebugUnitTest

# 4. 检查 AAR 中是否包含测试代码(不应包含)
unzip -l ui-components/build/outputs/aar/ui-components-release.aar | grep -i test

如果步骤 4 中出现任何与 “test-fixtures” 或 “junit” 相关的文件,说明依赖配置有误。

4.6 常见错误与调试

  • 错误:Unresolved reference: TestFixture — 夹具模块未正确包含在 settings.gradle.kts 中,或主模块的依赖声明写成了 implementation 而非 testImplementation。
  • 错误:Duplicate class com.example.test.MockRule — 夹具与主模块中定义了相同全限定名的类。解决方案:为夹具模块设置独立的 namespace,并在代码包名前缀中添加 .testfixtures。
  • 错误:资源文件在测试时找不到 — 确认资源放在 src/main/resources 中,并使用 getClass().getClassLoader().getResource("images/logo.png") 加载。

4.7 模块分离与编译期优化

当项目模块数量 ≥ 5 时,分离测试模块可显著提升增量构建速度:

  • 主模块代码变更时,只重新编译主模块,而夹具模块的 AAR 如果未变则直接缓存命中。
  • Gradle 的 --parallel 能并行编译多个夹具模块。
  • 可在 CI 中缓存 test-fixtures 模块的 build/ 目录,进一步加速。

推荐在 gradle.properties 中启用:

org.gradle.parallel=true
org.gradle.caching=true
android.enableBuildCache=true

4.8 本章核心流程图

下方的 SVG 展示了主模块、夹具模块和测试执行模块之间的编译依赖与资源流向:

Compose 测试模块分离结构图 主模块 :ui-components src/main/kotlin 测试夹具模块 :ui-components:test-fixtures src/main/kotlin + resources 测试执行模块 ui-components/src/test 或 androidTest api(project) testImplementation 资源流向: 主模块和夹具模块的 src/main/resources 均被打包进各自 AAR。 测试执行模块通过 classpath 同时获取两份资源。 关键依赖配置清单 1. 主模块 build.gradle.kts: testImplementation(project(":ui-components:test-fixtures")) 2. 夹具模块 build.gradle.kts: api(project(":ui-components")) 3. settings.gradle.kts: include(":ui-components:test-fixtures") 警告:切勿在夹具模块中使用 implementation(project),会导致循环依赖;始终使用 api/compileOnly。

4.9 部署注意事项

  • 在 CI 中,只对变更过的夹具模块执行编译,可使用 ./gradlew :ui-components:test-fixtures:compileReleaseKotlin --only-if-changed。
  • 如果使用 Maven Central 发布主模块 AAR,务必在 publishing 块中排除夹具模块,避免用户错误引用。
  • 对于 Compose 快照测试,所有基线图片应集中放在夹具模块的 src/main/resources/compiled_compose 下,并通过 CI 的 pull_request 触发器自动生成增量基线。