首页
/ Now in Android `:core:data-test` 模块剖析:数据层测试替身与 Hilt 测试安装指南

Now in Android `:core:data-test` 模块剖析:数据层测试替身与 Hilt 测试安装指南

2026-09-12 17:07:11作者:裴麒琰

本篇技术指南聚焦 Now in Android(NIA)示例应用中 :core:data-test 模块的完整设计与实现。该模块是数据层(:core:data)的专用测试基础设施:它通过 Hilt 的 @TestInstallIn 机制将生产环境的仓库、网络监控与时区监控实现整体替换为内存版 Fake 与固定桩(stub),从而让 App 可以在无网络、无后端的环境下用本地 JSON 假数据跑通全部功能与截图测试。读完本文,你将掌握:NIA 数据层测试模块的构建配置与依赖关系、5 个 Fake 仓库与 2 个监控器替身的实现细节、@TestInstallIn 模块替换的完整装配流程,以及如何在 testDemo / androidTest 中落地这套方案。

模块定位:为数据层准备的"测试专用变体"

core/data-test/README.md 是模块的唯一定义文档,它通过模块依赖图明确了 :core:data-test 在 NIA 模块化架构中的位置。根据文档中的依赖图(graph TB:core:data-test --> :core:data 的实线箭头),该模块单向依赖 :core:data,两者共同隶属于 :core 分组。文档中 :core:data 的依赖关系还揭示了数据层本体的构成:

  • 实线(强依赖)::core:common:core:database:core:datastore:core:network
  • 虚线(弱依赖,以 .-> 表示)::core:analytics:core:notifications

这正是 NIA 中"离线优先(offline-first)"数据层的模块划分:网络层提供远端数据,数据库提供本地缓存,DataStore 保存用户偏好,Common 提供跨模块的调度器与工具,Analytics/Notifications 作为可选旁路依赖存在。

依赖图还提供了模块类型的图例(legend):android-applicationandroid-featureandroid-libraryjvm-libraryandroid-test 等五类着色规范,可用于理解整个 NIA 仓库所有模块 README 中内嵌的同类 Mermaid 依赖图。

构建配置:一个"测试产物"如何被打包

:core:data-test 的构建配置位于 core/data-test/build.gradle.kts,整体极为精简:

plugins {
    alias(libs.plugins.nowinandroid.android.library)
    alias(libs.plugins.nowinandroid.hilt)
}

android {
    namespace = "com.google.samples.apps.nowinandroid.core.data.test"
}

dependencies {
    api(projects.core.data)

    implementation(libs.hilt.android.testing)
}

三个关键设计值得注意:

  1. api(projects.core.data):以 api 而非 implementation 暴露数据层,意味着凡是依赖 :core:data-test 的模块(如 app 的测试源码集),会同时传递性地获得 :core:data 的全部 API 可见性。Fake 类需要实现 NewsRepositoryTopicsRepository 等接口,因此必须能编译期访问这些接口。
  2. libs.hilt.android.testing:这是 Hilt 的测试支持库,提供了 @TestInstallInCustomTestApplication 等测试专用注解与运行时支持——它是下面 TestDataModule 能"替换"生产模块的前提。
  3. namespace 独立:模块命名空间为 com.google.samples.apps.nowinandroid.core.data.test,与 core/data-test/src/main/AndroidManifest.xml 中的空 <manifest /> 声明一致——本模块不包含任何四大组件,只是纯 Kotlin 测试代码的载体。

使用方:谁在消费这个模块

app/build.gradle.kts 中可以找到两个消费点:

testImplementation(projects.core.dataTest)          // 本地单元测试(testDemo)
androidTestImplementation(projects.core.dataTest)   // 仪器化测试(androidTest)

结合 settings.gradle.kts 中的 include(":core:data-test"),可以确认该模块同时服务于 app 的 demo 变体单元测试androidTest 仪器化测试两类场景。这一"主源码集(main)中放测试替身代码"的组织方式,与 NIA 中 core/datastore-testsync/sync-test 等姊妹模块保持了一致的模式。

Fake 仓库:5 个数据层接口的测试实现

core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/ 目录下提供了 5 个 Fake 实现,分别对应 core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository 中的 5 个仓库接口:

Fake 类 实现的接口 数据来源 典型行为
FakeNewsRepository NewsRepository DemoNiaNetworkDataSource(news.json + topics.json) NewsResourceQuery 过滤新闻,syncWith 直接返回 true
FakeTopicsRepository TopicsRepository DemoNiaNetworkDataSource(topics.json) 映射为外部 Topic 模型,syncWith 直接返回 true
FakeUserDataRepository UserDataRepository NiaPreferencesDataSource(DataStore) 透传全部读写到真实 DataStore
FakeRecentSearchRepository RecentSearchRepository 无(硬编码空值) 查询返回空列表,写操作空实现
FakeSearchContentsRepository SearchContentsRepository 无(硬编码空值) searchContents 返回空流,getSearchContentsCount 返回 1

FakeNewsRepository:带查询过滤的内存版新闻仓库

FakeNewsRepository.kt 是最有代表性的实现。它通过 DemoNiaNetworkDataSource 读取打包在 core:network 模块 assets 中的 news.jsontopics.json,在内存中完成 NewsResourceQuery 的过滤逻辑:

class FakeNewsRepository @Inject constructor(
    @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
    private val datasource: DemoNiaNetworkDataSource,
) : NewsRepository {

    override fun getNewsResources(
        query: NewsResourceQuery,
    ): Flow<List<NewsResource>> =
        flow {
            val newsResources = datasource.getNewsResources()
            val topics = datasource.getTopics()

            emit(
                newsResources
                    .filter { networkNewsResource ->
                        // 无 filterNewsIds / filterTopicIds 时直接返回
                        listOfNotNull(
                            true,
                            query.filterNewsIds?.contains(networkNewsResource.id),
                            query.filterTopicIds?.let { filterTopicIds ->
                                networkNewsResource.topics.intersect(filterTopicIds).isNotEmpty()
                            },
                        ).all(true::equals)
                    }
                    .map { it.asExternalModel(topics) },
            )
        }.flowOn(ioDispatcher)

    override suspend fun syncWith(synchronizer: Synchronizer) = true
}

实现细节与生产版本(OfflineFirstNewsRepository)形成鲜明对照:

  • 过滤语义listOfNotNull(true, ...) 保证当查询未指定任何过滤条件时所有条目都被保留;当同时指定 filterNewsIdsfilterTopicIds 时,采用 AND 语义(all(true::equals))。
  • 外部模型映射asExternalModel(topics) 将网络层模型(NetworkNewsResource)与主题信息合并为 UI 层消费的 NewsResource 领域模型。
  • syncWith 恒为 true:Fake 无需真正的同步逻辑,直接报告"同步成功",从而把"同步"从测试路径中彻底摘除。
  • 调度器保持:仍使用 @Dispatcher(IO)flowOn(ioDispatcher),保留真实实现中的 IO 调度语义,避免测试出现并发层面的假象。

FakeUserDataRepository:唯一"半真半假"的仓库

FakeUserDataRepository.kt 与其它 4 个 Fake 不同——它没有抛弃底层存储,而是注入真实的 NiaPreferencesDataSource(DataStore),并将全部读写操作透传过去:

class FakeUserDataRepository @Inject constructor(
    private val niaPreferencesDataSource: NiaPreferencesDataSource,
) : UserDataRepository {

    override val userData: Flow<UserData> =
        niaPreferencesDataSource.userData

    override suspend fun setFollowedTopicIds(followedTopicIds: Set<String>) =
        niaPreferencesDataSource.setFollowedTopicIds(followedTopicIds)
    // setTopicIdFollowed / setNewsResourceBookmarked / setNewsResourceViewed
    // setThemeBrand / setDarkThemeConfig / setDynamicColorPreference / setShouldHideOnboarding
    // 均为一对一透传
}

其 doc 注释说明这正是设计意图:"Fake implementation of the UserDataRepository that returns hardcoded user data. This allows us to run the app with fake data, without needing an internet connection or working backend." 也就是说,用户偏好(关注主题、收藏、已读、主题品牌、深色模式、动态取色、是否隐藏引导)需要真实的持久化来支撑跨界面交互,而"假"的部分只在于不依赖远端后端。这与 FakeNewsRepository/FakeTopicsRepository 的"纯 JSON 假数据"策略互补,构成了 NIA 测试数据的完整闭环。

其余三个极简 Fake

  • FakeRecentSearchRepository.ktinsertOrReplaceRecentSearch/clearRecentSearches 为空操作,getRecentSearchQueries 返回 flowOf(emptyList())
  • FakeSearchContentsRepository.ktsearchContents 返回空流,getSearchContentsCount 返回 flowOf(1)(用于满足界面计数展示的最小值),populateFtsData 为空操作。这两个 Fake 从代码结构看,服务于搜索页在 demo 变体下的占位呈现。
  • FakeTopicsRepository.kt:从 JSON 读取主题后手工映射 TopicgetTopic(id) 基于 getTopics()first { it.id == id } 查找,syncWith 同样恒为 true

监控器替身:固定的网络与时区状态

除了仓库,数据层还暴露两个"环境状态"接口——NetworkMonitor(网络在线状态)与 TimeZoneMonitor(当前时区)。生产实现分别基于 ConnectivityManager 广播与系统时区广播(见 DataModule.kt),在测试中则需要完全确定性的替身:

// AlwaysOnlineNetworkMonitor.kt
class AlwaysOnlineNetworkMonitor @Inject constructor() : NetworkMonitor {
    override val isOnline: Flow<Boolean> = flowOf(true)
}

// DefaultZoneIdTimeZoneMonitor.kt
class DefaultZoneIdTimeZoneMonitor @Inject constructor() : TimeZoneMonitor {
    override val currentTimeZone: Flow<TimeZone> = flowOf(TimeZone.of("Europe/Warsaw"))
}

两个替身的核心价值是确定性

  • AlwaysOnlineNetworkMonitorisOnline 恒为 true。这使依赖网络状态决定的数据刷新路径(例如"在线时后台同步")在测试中始终走"在线"分支,规避了模拟器/CI 环境中网络状态抖动带来的 flaky 测试。
  • DefaultZoneIdTimeZoneMonitor 将时区固定为 Europe/Warsaw,保证新闻卡片的"相对时间"文案(如"x 分钟前")在快照测试中完全可复现——这正是 NIA 截图测试(screenshot testing)能稳定对比像素的前提。

核心装配:@TestInstallIn 模块替换的完整流程

连接 5 个 Fake 仓库与 2 个监控器替身的关键,是 TestDataModule.kt 中的 Hilt 测试模块:

@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [DataModule::class],
)
internal interface TestDataModule {
    @Binds
    fun bindsTopicRepository(fakeTopicsRepository: FakeTopicsRepository): TopicsRepository

    @Binds
    fun bindsNewsResourceRepository(fakeNewsRepository: FakeNewsRepository): NewsRepository

    @Binds
    fun bindsUserDataRepository(userDataRepository: FakeUserDataRepository): UserDataRepository

    @Binds
    fun bindsRecentSearchRepository(recentSearchRepository: FakeRecentSearchRepository): RecentSearchRepository

    @Binds
    fun bindsSearchContentsRepository(searchContentsRepository: FakeSearchContentsRepository): SearchContentsRepository

    @Binds
    fun bindsNetworkMonitor(networkMonitor: AlwaysOnlineNetworkMonitor): NetworkMonitor

    @Binds
    fun binds(impl: DefaultZoneIdTimeZoneMonitor): TimeZoneMonitor
}

替换语义逐条对照

将它与生产模块 DataModule.kt 并排阅读,可以清楚看到一一对应的替换关系:

接口 生产绑定(DataModule) 测试绑定(TestDataModule)
TopicsRepository OfflineFirstTopicsRepository FakeTopicsRepository
NewsRepository OfflineFirstNewsRepository FakeNewsRepository
UserDataRepository OfflineFirstUserDataRepository FakeUserDataRepository
RecentSearchRepository DefaultRecentSearchRepository FakeRecentSearchRepository
SearchContentsRepository DefaultSearchContentsRepository FakeSearchContentsRepository
NetworkMonitor ConnectivityManagerNetworkMonitor AlwaysOnlineNetworkMonitor
TimeZoneMonitor TimeZoneBroadcastMonitor DefaultZoneIdTimeZoneMonitor

@TestInstallIn(replaces = <a href="https://link.gitcode.com/i/bcf970f9d73d623f6e6335138fe641bc" target="_blank">DataModule::class]) 的语义是:在测试构建中,用本模块的绑定整体覆盖 DataModule 的同名绑定,其余未覆盖的绑定(如 database、datastore 提供的 DAO 与 DataStore 绑定)继续由生产模块提供。由于 TestDataModuleDataModule 都是 @Binds 抽象模块,实际实例化发生在 @Inject constructor 标注的 Fake 类上——这要求每个 Fake 的构造参数都能被 Hilt 解析,例如 FakeNewsRepository 依赖的 DemoNiaNetworkDataSource:core:network 的 demo 风味模块([FlavoredNetworkModule.kt 中 binds(impl: DemoNiaNetworkDataSource): NiaNetworkDataSource)提供。

数据流全景

从源码可以梳理出完整的数据流:测试 App(demo 变体)→ 界面 ViewModel → Fake 仓库 → DemoNiaNetworkDataSource → assets 中的 news.json / topics.jsonDemoNiaNetworkDataSource(见 core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/demo/DemoNiaNetworkDataSource.kt)使用 kotlinx.serialization 从 JSON 流式解码(API 23 及以下走 decodeFromString 兼容路径),数据文件位于 core/network/src/main/assetsnews.jsontopics.json)。这套链路让测试完全脱离真实后端。

工程价值与适用场景总结

结合 app/build.gradle.ktstestDemoandroidTestprojects.core.dataTest 的引入,:core:data-test 的实际价值可归纳为:

  1. 离线可跑:App 的 demo 变体与截图测试(例如 app/src/testDemo 下的 SnackbarScreenshotTestsSnackbarInsetsScreenshotTests)无需网络与后端即可运行,彻底移除环境不确定性。
  2. 行为确定:固定的在线状态与时区使"时间敏感、网络敏感"的 UI 快照可像素级复现,是 NIA 截图测试稳定性的基石。
  3. 接口兼容:Fake 严格实现数据层公共接口,测试代码与生产代码共享同一套类型系统,切换成本为零。
  4. 模式可复用@TestInstallIn + Fake 仓库 + 固定监控器的三件套,是 Android 多模块项目中"为数据层构建测试替身"的推荐范式,与 core/datastore-testcore/testing 等模块共同构成 NIA 完整的测试基础设施矩阵。
登录后查看全文
热门项目推荐
相关项目推荐