易君召
发布于 2026-09-06 / 作者:易君召 / 6 阅读
0

如何通过单元测试提高代码质量

单元测试核心思想:把最小代码单元(函数 / 方法)隔离,验证输入输出、分支逻辑、异常行为是否符合预期,不是为了测而测,而是驱动设计、提前发现 bug、方便重构、降低维护成本。下面从原则、实践、落地技巧、避坑点完整说明。

一、单元测试带来哪些质量收益

  1. 提前发现缺陷:在开发阶段捕获逻辑 bug,而不是留给测试 / 线上;bug 发现越早修复成本越低。

  2. 倒逼代码设计变好:难以写单元测试的代码,本身设计往往有问题(强耦合、大函数、依赖满天飞)。可测≈高内聚低耦合。

  3. 安全重构保障:修改业务逻辑、重构代码,单元测试可以快速验证原有功能没有被破坏,防止改出回归 bug。

  4. 充当活文档:测试用例就是示例,能告诉其他开发者这个方法应该怎么用、边界是什么。

  5. 简化调试定位:单元测试失败直接定位到具体方法,不用跑完整应用、不用搭完整环境。

二、高质量单元测试的核心原则

1. AIRA(也常用 FIRST 原则)

FIRST 原则

  • Fast(快):单元测试必须跑的快,毫秒级;不能依赖数据库、网络、外部服务,否则变成集成测试。

  • Independent(独立):每个用例互不依赖,不依赖执行顺序;一个用例失败不影响别的用例;每个用例自己准备数据、自己清理。

  • Repeatable(可重复):无论本地、CI 机器,每次运行结果一致,不能依赖时间、环境变量、外部状态。

  • Self‑validating(自校验):用断言判断结果,不靠人工看日志;要么 pass 要么 fail。

  • Timely(及时):和业务代码同步写;TDD 是先写测试再写实现;普通开发写完业务立刻补单元测试。

2. 只测单元,隔离外部依赖

单元测试测业务逻辑,不测数据库、MQ、HTTP 接口、文件系统。 外部依赖要使用 Mock/Stub 隔离:

  • 数据库:mock DAO/Mapper 返回假数据,不要连真实库;真实数据库交给集成测试。

  • RPC/HTTP 调用:mock 客户端返回模拟响应。

  • 时间、随机数:注入时间接口,不要直接用System.currentTimeMillis(),方便 mock 固定时间。

3. 测试什么,不测试什么

要测

  • 正常业务流程(正常输入得到正确输出)

  • 边界条件:空值、null、空集合、最大最小值、临界值

  • 异常分支:入参非法、抛出预期异常

  • 各种 if‑else、switch、for 循环分支,保证分支覆盖

  • 业务规则:核心算法、计算逻辑

不要测

  • 不要简单测试 getter/setter(无业务逻辑)

  • 不要 mock 你自己的待测试类

  • 不要测框架本身(Spring、Mybatis 底层,框架自有测试)

  • 不要把单元测试写成集成测试,拉起完整 Spring 上下文做大量 IO

三、编写单元测试最佳实践(Java 举例,JUnit5 + Mockito)

1. 测试类命名规范

  • 业务类:OrderService → 测试类:OrderServiceTest

  • 测试方法命名,清晰表达场景:方法名_场景_预期结果

// 好的命名
void calculatePrice_nullOrder_throwIllegalArgument()
void calculatePrice_discountZero_returnOriginalPrice()

// 不好:test1、testCalculate

2. 标准用例三段式:AAA 模式

Arrange (准备) → Act (执行) → Assert (断言)

@Test
void calculatePrice_normalOrder_returnCorrectAmount(){
    // Arrange:准备入参、mock依赖
    Order order = new Order();
    order.setAmount(BigDecimal.valueOf(100));
    DiscountService mockDiscount = Mockito.mock(DiscountService.class);
    when(mockDiscount.getDiscount(any())).thenReturn(BigDecimal.valueOf(0.8));

    OrderService orderService = new OrderService(mockDiscount);

    // Act:执行待测试方法
    BigDecimal result = orderService.calculatePrice(order);

    // Assert:断言结果、异常、交互行为
    assertEquals(BigDecimal.valueOf(80), result);
}
  • Arrange:构造输入、mock 依赖行为

  • Act:仅仅调用一次待测试方法

  • Assert:断言输出结果;必要时断言 mock 是否被调用(交互测试)

3. 异常场景测试

校验是否抛出期望异常,以及异常信息

@Test
void calculatePrice_nullOrder_throwsException(){
    OrderService service = new OrderService(mockDiscount);
    assertThrows(IllegalArgumentException.class, ()->{
        service.calculatePrice(null);
    });
}

4. Mock 使用边界

  • Mock外部依赖(别的 Service、DAO、RPC 客户端)

  • 不要 mock 被测试的目标类

  • 不要过度 mock:简单值对象不需要 mock,直接 new 对象即可。

四、覆盖率怎么正确看待

覆盖率是工具指标,不是质量目标。100% 覆盖率≠代码质量高。

  • 行覆盖率、分支覆盖率:用来发现哪些逻辑完全没被测试覆盖,找出遗漏用例。

  • 坑:可以写出全通过 100% 覆盖但是断言写错的无效测试用例。

关注点:业务分支、边界、异常路径有没有用例覆盖,而不是单纯数字。

CI 流水线可以配置覆盖率门禁,但是不要强制追求 100%;工具如 JaCoCo (Java)。

五、TDD 测试驱动开发如何提升质量

TDD 流程:红‑绿‑重构

  1. 红:先写单元测试,功能还没实现,测试失败(红)

  2. 绿:写最小实现代码,让测试跑通(绿)

  3. 重构:优化实现代码,不修改测试,测试依旧保持通过

TDD 价值:从需求出发,先定义 “代码应该做到什么”,再写实现;天然逼迫代码可测试。适合核心业务、算法模块;普通业务不必强制 TDD,但可以借鉴思路。

六、单元测试融入研发流程,真正落地提质量

  1. 开发阶段:业务代码同步写单元测试;核心业务逻辑要求单元测试;简单 CRUD 可以酌情,复杂规则必须测。

  2. 代码评审(CR):评审不仅看业务代码,同时评审单元测试:场景是否齐全、断言是否正确、有没有无效 mock。

  3. CI 持续集成:提交代码自动跑单元测试;单元测试失败不允许合并代码。

  4. 定期维护测试用例:业务变更,同步修改单元测试;业务逻辑删除,删除废弃测试;允许重构测试代码。

  5. 区分单元测试 / 集成测试

    • 单元测试:无 IO、mock 依赖,速度快,跑在每次 commit。

    • 集成测试:启动容器、连接真实数据库,运行慢,放在 CI 后期阶段。

七、常见坑,反而损害代码质量

  1. 测试写得比业务代码还复杂:测试本身出现 bug,测试不可信。测试代码同样需要维护。

  2. 单元测试大量启动 Spring 上下文,加载数据库,变成慢集成测试,没人愿意跑。

  3. 用例之间共享变量、状态;一个用例失败连锁导致一批失败,很难定位。

  4. 只测正常流程,忽略 null、空集合、异常分支。

  5. 为了覆盖率凑用例,没有有效断言,只是调用一遍代码。

  6. 单元测试写大量业务逻辑,复制业务逻辑;业务改了测试忘记同步更新。

八、单元测试和其他质量手段关系

单元测试是质量防线的第一层: 单元测试 → 集成测试 → 接口测试 → E2E自动化测试 → 人工测试 单元测试解决代码内部逻辑正确性,不能替代上层测试;上层测试也不能替代单元测试。

总结

单元测试提升代码质量本质不是 “多写一堆测试文件”,而是:

  1. 通过隔离依赖的测试模式,倒逼业务代码解耦、高内聚;

  2. 在开发阶段拦截逻辑 bug;

  3. 给重构提供安全网;

  4. 用例作为活文档,降低后续维护成本。

好的单元测试特征:跑得快、独立、场景完整、断言到位、易于维护。


本文原创作者:易君召,详见:https://www.yijunzhao.cn/authors/yijunzhao,转载请注明出处。

原文链接 https://www.yijunzhao.cn/archives/unit-testing-improve-code-quality-guide

欢迎访问 小易撩挨踢

https://www.yijunzhao.cn/