稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

架构测试为何需要双视图

本文译自「Kotlin Architecture Tests: Why Konture Exists」,原文链接Kotlin Architecture Tests: Why Konture Exists,由Bao Le发布于2026年7月16日。

Kotlin 架构对同一架构有两种不同的理解:Gradle 图决定了哪些组件可以链接,而 Kotlin 源代码模型决定了代码的实际内容。Konture 的存在就是为了测试这两种理解。

考虑以下来自 Android 项目的规则:

展示代码不得在屏幕状态或面向 UI 的 API 中暴露传输 DTO。

这条规则在两个不同的地方都可能失效。

当表示模块直接连接到传输模块时,构建图可能会失败:

1
2
3
4
// feature/profile/presentation/build.gradle.kts
dependencies {
    implementation(project(":core:network"))
}

它在源端也可能失败:

1
2
3
4
5
package com.acme.profile.presentation
import com.acme.network.dto.UserDto
data class ProfileUiState(
    val user: UserDto,
)

这些故障彼此关联,但并不完全相同。第一种是 Gradle 模块的物理依赖问题,第二种是源代码级别的导入问题。一款优秀的 Kotlin 架构测试工具必须能够识别这两种故障,因为多平台系统同时受这两种因素的影响。

这就是 Konture 存在的理由。

不完整的观点

现有工具很有用。Konture 的目的并非取代编译器、代码检查器、测试运行器或所有架构测试库。问题在于,Kotlin 项目的每种视图都会有所遗漏。

视图 优势 不足
JVM 字节码 已编译类及其依赖关系 Kotlin 源代码意图、源代码集、Gradle 项目边界、部分编译器插件的影响
Kotlin 源代码扫描 包、导入、声明、修饰符、注解 真实的 Gradle 模块图和项目依赖策略
Gradle 图检查 模块、源代码集、已声明的项目依赖关系 源代码级导入、公共签名、可见性和类型泄漏
代码检查工具 局部风格和单文件质量 整体项目结构和架构所有权

Kotlin架构贯穿于这些观点之中。

你可以在已提交的示例中看到这一点。“Now in Android”示例包含 36 个 Gradle 项目(位于 settings.gradle.kts 文件中),其中包括 API/实现功能拆分和一个专门的 :konture-test 项目。KotlinConf KMP 示例 包含 9 个 Gradle 项目,涵盖共享核心、后端、Android、桌面、Web、管理和架构测试。单视图工具在这些项目中仍然有用,但它无法自然地看到项目使用的所有边界。

工具对比

实际问题不是“哪个工具最好?”,而是“哪个工具遵循哪种规则?”

工具选项 优势 劣势 适用场景 与 Konture 结合使用
ArchUnit 成熟的 JVM 字节码规则和类依赖性检查 Gradle 项目图、KMP 源集、源代码级 Kotlin 意图 系统主要基于 JVM,规则在已编译类中可见 你还需要模块依赖策略、Kotlin 可见性或 KMP/平台边界
detekt 自定义规则 Kotlin PSI、样式、命名、本地声明规则 Gradle 模块图和跨模块依赖策略 该规则类似于 lint:命名、注解、本地源代码约定 lint 发现的问题需要与模块所有权或公共 API 边界一致
Gradle Doctor 或模块图插件 构建健康状况、项目图可见性、依赖性报告 源导入、签名、类可见性、API 泄漏 你需要图洞察、构建性能报告或依赖性卫生 图边缘只是故事的一半,源代码级别的泄漏也至关重要
自定义 PSI 工具 高度特定的 Kotlin 源代码规则 维护成本、构建集成、图感知 你拥有一个针对特定组织的规则,团队可以拥有该工具 自定义规则应与构建图契约并行运行
运行时或 UI 测试工具,例如 Kakao 用户流程和 Android UI 行为 静态架构 你正在验证行为、页面和交互流程 结构边界应在成为运行时行为缺陷之前失效
简单的审查清单 判断、细微差别、例外情况 在时间压力下的一致性 规则仍在不断演变或取决于人为设计的权衡 清单项变得足够稳定,可以在 CI 中执行

Konture 的赌注很窄:Kotlin 架构需要一个单一的测试表面,能够同时讨论 Gradle 模块和 Kotlin 源代码声明。

促使它发生的失败模式

最昂贵的架构故障很少始于“糟糕的架构”,它们往往始于合理的局部修复:

  • 一个功能实现导入了另一个功能实现,因为 API 模块尚未公开某个缺失的契约。
  • 一个共享的 KMP 模块在截止日期前接受了一个 Android 导入,之后发现桌面或 iOS 无法再干净地重用该代码。
  • 一个公共存储库接口返回一个数据库实体,因为该实体已经包含所需的字段。
  • 后端路由直接调用存储库,因为服务层对于一个端点来说过于繁琐。

每一次失败都会产生利息债务。下一次重构必须先偿还隐藏的耦合,才能实现预期的变更。下一个平台目标必须分离原本应该可移植的 API。下一个代码审查员必须从分散的导入和构建文件中重构架构决策。

Konture 的设计初衷就是为了解决这类问题:它既不是纯粹的源代码级规则,也不是纯粹的构建级规则,而是架构级规则,因为它介于两者之间。

字节码固然重要,但它并非 Kotlin 程序的全部。

ArchUnit 已成熟并被证明适用于 JVM 系统。如果你的架构规则可以从编译后的类中得到解答,那么字节码分析通常是一个不错的选择。

现代 Kotlin 项目通常需要比这更多的上下文信息。

Kotlin 源代码结构并非总能与团队想要维护的源代码级设计完美契合。顶层函数和扩展函数会被编译成生成的持有者类。内联函数会将函数体移至调用点。object、委托属性和编译器插件可能会引入一些对运行时很重要但对源代码级设计规则而言却显得冗余的生成结构。

但这并不意味着字节码分析是错误的。而是说,对于诸如以下规则而言,字节码并非最佳的主要分析视角:

  • :feature:checkout:impl 是否依赖于同级实现模块?
  • commonMain 是否导入了 Android API?
  • 公共领域签名是否暴露了持久化类型?
  • impl 包在源代码中是否保持内部状态?

这些是 Kotlin 和 Gradle 架构方面的问题,而不仅仅是 JVM 类方面的问题。

源代码扫描很有用,但文件夹并非构建方式

Kotlin优先的源代码扫描器擅长处理声明、导入、命名、注解和可见性。它们可以表达许多字节码工具难以处理的规则。

但源代码目录并非 Gradle 项目。包名并非模块依赖项。名为 api 的文件夹与通过 apiimplementation 依赖的其他项目所依赖的模块并不相同。

这种区别在 Android 和 Kotlin 多平台项目中至关重要:

1
2
3
4
5
6
7
8
9
10
:app 
:core:domain 
:core:data 
:feature:checkout:api 
:feature:checkout:impl 
:feature:profile:api 
:feature:profile:impl 
:shared 
:androidApp 
:iosApp

构建过程已经知道哪些模块存在、哪些源集是生产源集,以及声明了哪些项目依赖项。架构测试应该使用这些信息,而不是仅仅依靠命名约定来重建架构。

代码检查器不是架构测试器

detektktlint 非常擅长局部检查。它们并非设计用于回答系统整体问题:

  • 此模块是否依赖于被禁止的同级模块?
  • 某个功能实现是否对其他实现可见?
  • 项目图是否存在循环?
  • 公共 API 是否暴露了其他层拥有的类型?

这些不是格式问题,而是所有权和依赖关系问题。

Konture的双视图模型

Konture 将构建视图和源代码视图结合在一起。

构建视图包括:

  • Gradle 模块。
  • 源代码集。
  • 生产环境 Kotlin 源代码目录。
  • 已声明的项目依赖项。
  • 已应用的插件上下文。

源视图包括:

  • 文件、包和导入。
  • 类、接口、函数和属性。
  • 可见性和注解。
  • 项目类之间的引用。

让架构边界变成可执行的测试

本文译自「Kotlin Architecture Tests: What They Are and Why They Matter - Part 1/3」,原文链接https://medium.com/proandroiddev/kotlin-architecture-tests-what-they-are-and-why-they-matter-389ce9a1204c,由Bao Le发布于2026年7月16日。

一个 Kotlin 项目可以编译通过,通过单元测试,满足代码检查工具的要求,但其结构仍然可能变得难以修改。架构测试正是为了弥补这一缺陷而存在的。