模块化测试分层概述:单元测试、集成测试、系统测试、验收测试

做 CI/CD 这么多年,我见过最多的坑就是——测试不分层。或者说,大家把所有测试都堆在同一个阶段跑。你看,一跑就是两三个小时,开发等得抓狂,最后干脆跳过测试直接合并。

其实测试分层不是什么新概念。说白了,就是给产品质量设几道关卡,每道关卡检查不同维度的东西。你想想看,一条生产线不会只在一个工位完成所有质检吧?同样的道理。

那我们来看看,这套分层体系到底长什么样。

为什么一定要分层?

我个人习惯把测试比作「过滤网」。第一层筛掉最大的石头,第二层筛掉小石子,第三层留下沙子,最后一层检查成色。如果一开始就用最细的网去筛,那效率就太低了。

核心原则:每层测试的关注点不同,越底层跑得越快、越频繁;越顶层跑得越慢、越接近真实环境。

我曾经遇到过这样一个项目:团队把所有测试写在一个大文件里,单元测试、数据库连接、外部 API 调用混在一起。每次提交代码后要等 40 分钟才知道结果。后来我花了半天时间把它们按分层拆开,前两层 5 分钟就跑完了,开发再也不怕跑测试了。

四层测试模型

目前在业界最经典的分层就是这四层,咱们一个一个过。

层次 测试对象 执行速度 典型工具 负责角色
单元测试 单个函数、类、模块 毫秒级 Jest, JUnit, pytest 开发
集成测试 模块间交互、接口、数据库 秒级到分钟级 Testcontainers, MockServer 开发 + QA
系统测试 完整系统功能、性能、安全 分钟级到小时级 Selenium, JMeter, OWASP ZAP QA
验收测试 业务需求、用户场景 小时级 Cucumber, Robot Framework QA + 产品

单元测试:你的第一道防线

单元测试,说白了就是给最小的代码单元做自检。一个函数、一个方法,甚至一个条件分支。我要求团队里每个开发都要给自己的代码写单元测试。

这里有个指标我特别看重:代码覆盖率。但你记住,覆盖率不是越高越好。有次我见过有人把 getter/setter 都测了,覆盖率 95%,但核心算法一个分支都没走到。那是假覆盖。

// 一个工厂函数的单元测试示例
test('createUser should return user object with hashed password', () => {
  const input = { name: 'Alice', password: '123456' };
  const result = createUser(input);
  expect(result.name).toBe('Alice');
  expect(result.passwordHash).not.toBe('123456');
  expect(result.passwordHash.length).toBeGreaterThan(10);
});

我个人的建议:团队定一个硬性门槛——单元测试覆盖率不低于 60%。但别忘了检查测试质量,别让团队为了凑数写那些「assert(true)」的无意义用例。

集成测试:把碎片拼起来看

单元测试只证明每个零件是好的,但它们组装在一起可能就冒烟了。集成测试就是干这个的——验证模块之间的协作。

我记得有个项目,前端和后端分别通过单元测试,但一联调就报 500。后来发现是后端接口返回的日期格式和前端预期的不一致。单元测试各测各的,根本发现不了这种问题。

// 一个典型的集成测试:测试用户注册 + 数据库写入
@SpringBootTest
@AutoConfigureMockMvc
class UserRegistrationIntegrationTest {
    @Test
    void shouldPersistUserWhenRegisterWithValidData() {
        mockMvc.perform(post("/api/register")
                .contentType(JSON)
                .content("{\"email\":\"test@example.com\",\"pass\":\"secret\"}"))
                .andExpect(status().isCreated());
        // 验证数据库里确实有这条记录
        assertThat(userRepo.findByEmail("test@example.com")).isPresent();
    }
}

注意:集成测试往往需要外部依赖(数据库、消息队列)。千万别在本地跑真实环境。我建议用 Testcontainers 或 Docker Compose 做隔离。你想想看,如果测试依赖了一个共享数据库,别人一删数据你就挂了。

系统测试:站在用户的角度

系统测试就是真正启动整个应用,模拟用户操作。这时候测试的不再是代码逻辑,而是整个系统的行为是否符合预期。

这一层跑得最慢,所以往往不会在每次提交时触发。我通常只在 PR 合入主干后或每天定时运行一次。但这不代表它可以被忽略。有一次我们上线一个新功能,单元和集成测试全绿,结果生产上用户反馈「点击保存按钮页面卡死」。后来发现是前端和后端之间有个代理超时设置不一致——系统测试因为跑在本地环境,没模拟生产网络条件,根本没测出来。

所以系统测试里也分很多子类:功能测试、性能测试、安全测试、兼容性测试。时间有限的情况下,优先保证核心业务流程的功能测试。

验收测试:给业务方一个交代

验收测试,有时候也叫 E2E(端到端)测试。它的重点在于「验收」两个字——业务方说「嗯,这个功能我满意,可以上线了」。

我见过不少团队用 BDD(行为驱动开发)来做验收测试。用自然语言描述场景,然后自动执行。比如

Feature: 用户登录
  Scenario: 使用正确密码登录
    Given 用户已注册,账号为 alice@test.com,密码为 123456
    When 用户在登录页面输入 alice@test.com 和 123456
    And 点击登录按钮
    Then 页面跳转到个人主页
    And 页面顶部显示「欢迎,Alice」

这种描述,产品和开发都能看懂。而且这些用例直接可以生成自动化测试脚本。我个人很喜欢这种方式,因为它把「需求」和「测试」绑在了一起。

一个重要的避坑点:验收测试非常脆弱。前端一个按钮文字变了,脚本可能就挂了。所以不要把太多细节写进步骤里。我曾经因为弹窗文案从「确认」改成了「确定」,让整个验收套件失效了一晚上。第二天我把所有的选择器都改成了 data-testid 属性。

四层测试的关系图

下面这张图就是你理解整个体系的终极工具。注意看层次之间的「反馈箭头」——不是从上到下单向的,下层发现问题会反馈回上层修正。

测试分层结构与反馈模型 验收测试 (E2E) 验证业务需求与用户场景 系统测试 验证完整系统功能、性能、安全 集成测试 验证模块间交互与接口 单元测试 验证单个函数/类/模块 毫秒级 秒级 分钟级 小时级 ↑ 速度递减 纯内存 轻量容器 预发布环境 生产配置 虚线箭头表示「反馈驱动」:下层发现问题,推动上层改进;上层新增需求,倒逼下层补充测试 每层跑得越快就越常跑,越靠上越慢但越贴近用户真实体验

各层的触发时机与CI集成

有了分层,还要知道怎么在流水线里插入。我通常这样配:

  • 单元测试:每次代码提交(git push)就触发。跑完立刻给结果,阻塞 MR 合并。
  • 集成测试:PR 创建后合并前触发,阻塞合并。如果涉及外部依赖变更,必须重新跑。
  • 系统测试:每日凌晨定时跑一次,或者 PR 合入主干后触发。不阻塞,但结果通知到团队群。
  • 验收测试:版本发布前一天跑一次,全绿才允许打 Release 标签。不阻塞日常开发。

我曾经踩过一个坑:把系统测试和单元测试放在同一个 stage 里并行跑,结果 8 核机器卡死,MR 合并排队堵了一整个下午。后来我老老实实把慢的测试抽到独立的 stage,用更大的 Runner 去跑。这个经验告诉我,分层不只是逻辑上的,物理资源也得跟着分。

小结

四层测试模型不是什么教科书的死定义,而是工程实践里沉淀出来的最佳路径。你不需要在所有项目里都铺满四层——如果只是一个内部工具,单元测试 + 集成测试可能就够了。但如果是面向用户的商业系统,哪怕少一层都会让你睡不踏实。

记住四个关键词:快、准、稳、验。单元测试要快,集成测试要准,系统测试要稳,验收测试要验。把这四个字记在脑子里,你的 CI 管道就不会跑偏。