单元测试与集成测试:分清楚它们,少走一半弯路

做 Compose 重组测试,第一个要搞明白的就是两种测试层次:单元测试和集成测试。我刚开始接触时,觉得不都是跑一下用例吗?后来才发现,选错了测试层次,要么跑得慢得要命,要么根本测不出真正的问题。今天咱们就彻底聊清楚它们的区别、适用场景和优劣。

单元测试 —— 切到最细,测到骨头里

单元测试,说白了就是只测一个最小的、独立的功能单元。在 Compose 里,这个“单元”可以是一个 Composable 函数、一个 ViewModel 中的方法、一个状态计算逻辑。它不依赖任何外部环境——不碰数据库、不碰网络、不碰其他组件。

适用场景:纯逻辑验证、单个组件的渲染行为、状态转换、边界值测试。

优点:

  • 执行飞快——毫秒级跑完几千个用例
  • 定位问题精准——失败时你基本知道是哪一行代码错了
  • 容易维护——依赖少,改动成本低

缺点:

  • 无法验证组件之间的交互
  • 对 UI 渲染结果的检查比较浅——只能测到是否存在某个文本,测不到布局是否对齐
  • 容易写出“为了测试而测试”的代码,背离真实场景

举个例子,我写过这样的单元测试:

@Test
fun testGreeting() {
    composeTestRule.setContent {
        Greeting(name = "World")
    }
    composeTestRule.onNodeWithText("Hello, World!").assertExists()
}

这段代码只测 Greeting 这个 Composable 里有没有显示出指定的字符串。速度快,但一旦这个组件需要从网络拿数据,它就无能为力了。嗯,这里要注意:千万别为了追求单元测试的覆盖率,把本该集成测试的东西硬塞进来。

集成测试 —— 多模块联调,还原真实玩法

集成测试则是把多个单元串起来测。比如一个列表页面,它要从 ViewModel 拿数据、显示加载状态、处理点击事件、跳转详情页——这些全部一起测。它会启动真实的 Activity(或 Compose 的 TestHost),模拟用户操作。

适用场景:页面级交互流程、导航、数据流串联、多组件协作。

优点:

  • 最贴近真实用户行为——能发现组件间接口不匹配、状态传递丢失等问题
  • 能测到 UI 布局、动画、手势等复杂行为
  • 覆盖度高——一次测试可能同时覆盖 View、ViewModel、Repository

缺点:

  • 慢——跑一个集成测试可能要几秒甚至十几秒
  • 定位问题困难——失败时你要从一堆栈里扒原因
  • 环境依赖重——需要 Mock 网络请求、文件系统等

说实话,我团队里曾经有一半的测试是集成测试,跑一次全量测试要 40 分钟……后来我们把纯业务逻辑拆成了单元测试,集成测试只保留核心流程,速度直接降到 8 分钟。所以这两种测试不是谁替代谁,而是各有各的位置。

一张对比表,看清本质

对比维度 单元测试 集成测试
测试范围 单个函数或 Composable 多个组件 + 真实依赖(或 Mock)
执行速度 毫秒级 秒级甚至分钟级
调试难度 低,失败即定位 高,需要追踪调用链
环境依赖 无 需要 Mock 或真实后端
重构敏感性 低,重构通常不破测试 高,重构可能大面积挂掉
典型代表 纯逻辑、状态计算、单组件渲染 页面流程、导航、数据交互
我个人习惯:单元测试占整体测试的 70% 以上,集成测试占 20%,剩下 10% 留给端到端测试。这个比例能让你的测试既快又稳。

为什么你不应该只写一种测试?

你想想看,如果你只有单元测试,一个页面改了什么你不一定知道——可能几个 Composable 之间的状态传递断了,单元测试根本发现不了。反过来,如果只有集成测试,每次改一行代码可能都要跑 10 分钟才知道结果。

我曾经吃过一个亏:对一个电商列表页只写了集成测试。后来要加一个商品标签筛选功能,我改了 ViewModel 的过滤逻辑,集成测试全绿,结果上线后列表不刷新——因为 Compose 重组时状态没有触发重组。如果我们有个单元测试去单独验证重组逻辑,早就发现了。嗯,从那以后我坚持任何状态变化逻辑都要写单元测试。

实际项目中的分层策略

我建议按这个金字塔结构来组织测试:

  1. 底层(单元测试):ViewModel 的普通函数、纯计算类、单个 Composable 的渲染用例。用 composeTestRule 配合 semantics 做节点断言。
  2. 中间层(集成测试):一个页面的完整流程,如点击按钮→加载数据→显示列表→点击 item→跳转。用 createComposeRule() 或 AndroidComposeTestRule 启动真实 Activity。
  3. 顶层(端到端测试):跨页面的用户流程,比如登录→首页→搜索→下单。用 ComposeTestRule 配合 Espresso 模拟真实设备操作。
注意:集成测试中如果需要 Mock 网络请求,一定不要用 Thread.sleep 等硬等待。应该使用 composeTestRule.waitUntil 或协程的 runBlockingTest。硬等待会让测试变得不稳定,而且慢。

好了,现在你对单元测试和集成测试有了清晰的分界线。下一件重要的事是学怎么用 Mock 来模拟这些外部依赖——那就是 Mock 回放方法的用武之地了。

单元测试与集成测试分层对比图 单元测试 vs 集成测试:范围、速度与稳健性 单元测试(快速 + 轻量) 纯函数 / 状态计算 单个 Composable 渲染 ViewModel 方法逻辑 Mock Repository 集成测试(慢速 + 全面) 页面完整流程 多 Composable 协作 导航 + 事件响应 真实或 Mock 网络/DB 原则:单元测试保快速验证,集成测试保真实场景,两者缺一不可 ⚡ 速度快,可靠性高 🐢 速度慢,可靠性(对真实场景)高
记住这张图:左侧的单元测试负责验证每个零件,右侧的集成测试负责验证组装效果。写测试的时候先问自己:我要测的是零件还是组装?答案自然会告诉你该用哪种测试。