第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 对象、测试资源、自定义 ComposeTestRule | src/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 展示了主模块、夹具模块和测试执行模块之间的编译依赖与资源流向:
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触发器自动生成增量基线。