当前位置: 首页 > news >正文

《代码整洁之道》第9章 单元测试 - 笔记

测试驱动开发 (TDD) 是一种编写整洁代码的“规程”或“方法论”,而不仅仅是测试技术。

JaCoCo 在运行测试后生成详细的覆盖率报告的工具, maven 引用。

测试驱动开发

测试驱动开发(TDD)是什么?

TDD 不是说写完代码再写测试,而是先写测试,再写代码。它是一种开发流程,一个不断循环的“节奏”:

  1. 红灯 (Red): 写一个针对某个新功能的自动化测试。运行这个测试,它应该失败,因为你还没写对应的功能代码。这个失败告诉你,“我想要的功能还不存在”。
  2. 绿灯 (Green):最少量的程序代码,让刚才失败的测试通过。你的目标只是让测试变绿,代码可能写得不好看、效率不高都没关系。然后运行所有的测试(包括之前写过的),确保没有破坏已有的功能。
  3. 重构 (Refactor): 现在所有测试都通过了,功能是正确的。这时,你可以放心地改进和优化你的程序代码和测试代码,让它们更整洁、更高效、结构更好。重构过程中,要持续运行所有测试,确保改进没有引入新的 Bug。

这个 红 -> 绿 -> 重构 的循环非常短,可能只需要几分钟到十几分钟。你不断地重复这个循环,逐步完善你的功能。

TDD 的三定律

  • 在你编写一个失败的测试之前,不能编写任何生产代码。
  • 在一个失败的测试中,你不能编写多于恰好能够暴露失败的测试代码。
  • 在你编写一个失败的测试之外,你不能编写多于恰好能够使得当前失败测试通过的生产代码。

举例说明:构建一个简单的字符串计算器

假设我们要写一个函数,能够接收一个包含数字和逗号的字符串,并计算所有数字的和。例如 "1,2,3" 应该返回 6。

我们使用 TDD 的流程来开发这个功能。

我们要测试的函数:int add(String numbers)

步骤 1:红灯 (Red) - 测试空字符串

  • 需求: 空字符串应该返回 0。

写测试:

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;// 我们要测试的类,也就是生产类
class StringCalculator {// 我们还没写这个 add 方法// public int add(String numbers) { ... }
}class StringCalculatorTest {@Testvoid shouldReturnZeroForEmptyString() {StringCalculator calculator = new StringCalculator();// 断言:调用 add("") 应该返回 0assertEquals(0, calculator.add(""));}
}
  • 运行测试: 测试会编译失败(因为 StringCalculator 类可能不存在,或者 add 方法不存在),或者运行时失败(比如 add 方法抛异常)。测试失败了,红灯!

步骤 2:绿灯 (Green) - 让测试通过

  • 目标:shouldReturnZeroForEmptyString 测试通过。

写最少量代码:

class StringCalculator {public int add(String numbers) {// 最简单让上面测试通过的代码就是判断是不是空字符串if (numbers == null || numbers.isEmpty()) {return 0;}// 现在还不能处理非空字符串,先随便返回个东西或抛异常 (这里暂不处理非空)// 实际上,为了让测试尽快通过,我们可能直接返回 0return 0; // !!! 让 shouldReturnZeroForEmptyString 通过的最少量代码 !!!}
}
  • 运行测试:shouldReturnZeroForEmptyString 测试通过了。如果之前有其他测试,也运行一下。所有测试都通过了,绿灯!

步骤 3:重构 (Refactor) - 改进代码

  • 代码量很少,暂时不需要大的重构。可能把 if 判断写得更清晰一点,或者给类和方法加注释。这里跳过大的重构。

步骤 4:红灯 (Red) - 测试单个数字

  • 需求: 输入 "1" 应该返回 1。

写测试:

class StringCalculatorTest {// ... shouldReturnZeroForEmptyString 测试 ...@Testvoid shouldReturnNumberForSingleNumberString() {StringCalculator calculator = new StringCalculator();// 断言:调用 add("1") 应该返回 1assertEquals(1, calculator.add("1"));// 断言:调用 add("5") 应该返回 5assertEquals(5, calculator.add("5"));}
}
  • 运行测试:shouldReturnNumberForSingleNumberString 测试会失败(因为 add("1") 仍然返回 0)。shouldReturnZeroForEmptyString 应该仍然通过。测试失败,红灯!

步骤 5:绿灯 (Green) - 让测试通过

  • 目标:shouldReturnNumberForSingleNumberString 通过。

写最少量代码:

class StringCalculator {public int add(String numbers) {if (numbers == null || numbers.isEmpty()) {return 0;}// !!! 添加处理单个数字的代码 !!!// 尝试将字符串转换为整数return Integer.parseInt(numbers); // !!! 让测试通过的最少量代码 !!!}
}
  • 运行测试:shouldReturnZeroForEmptyStringshouldReturnNumberForSingleNumberString 都通过了。所有测试都通过了,绿灯!

步骤 6:重构 (Refactor) - 改进代码

  • Integer.parseInt 可能会抛出 NumberFormatException,虽然当前测试没有覆盖到无效数字字符串,但为了健壮性,可以在这里考虑异常处理(或者等写了相关测试后再处理)。这里暂时不展开。

步骤 7:红灯 (Red) - 测试两个数字

  • 需求: 输入 "1,2" 应该返回 3。

写测试:

class StringCalculatorTest {// ... shouldReturnZeroForEmptyString 测试 ...// ... shouldReturnNumberForSingleNumberString 测试 ...@Testvoid shouldReturnSumForTwoNumbersSeparatedByComma() {StringCalculator calculator = new StringCalculator();// 断言:调用 add("1,2") 应该返回 3assertEquals(3, calculator.add("1,2"));// 断言:调用 add("5,7") 应该返回 12assertEquals(12, calculator.add("5,7"));}
}
  • 运行测试:shouldReturnSumForTwoNumbersSeparatedByComma 测试会失败(因为 add("1,2") 会因为无法直接解析 "1,2" 而抛出 NumberFormatException)。测试失败,红灯!

步骤 8:绿灯 (Green) - 让测试通过

  • 目标:shouldReturnSumForTwoNumbersSeparatedByComma 通过。

写最少量代码:

class StringCalculator {public int add(String numbers) {if (numbers == null || numbers.isEmpty()) {return 0;}// !!! 添加处理逗号分隔的代码 !!!String[] numberArray = numbers.split(","); // 按逗号分割if (numberArray.length == 1) {// 如果分割后只有一个元素 (处理单个数字的情况)return Integer.parseInt(numberArray[0]);} else {// 如果分割后有两个元素 (处理两个数字的情况)int num1 = Integer.parseInt(numberArray[0]);int num2 = Integer.parseInt(numberArray[1]);return num1 + num2; // 求和}// 注意:这段代码现在还不能处理三个或更多数字,甚至无效数字字符串// 但它让当前的测试通过了}
}
  • 运行测试: 所有三个测试都应该通过。所有测试都通过了,绿灯!

步骤 9:重构 (Refactor) - 改进代码

现在的代码有点简陋,只能处理空字符串、一个数字或两个数字。我们可以重构它,让它能处理任意数量的数字(注意这里违反了第三条定律):

(需求就是实现测试,目前已经完成了,这里是重构,不过重构不建议加新功能哈,这里不加新功能没啥好重构的了hhhh)

class StringCalculator {public int add(String numbers) {if (numbers == null || numbers.isEmpty()) {return 0;}// 重构:处理任意数量的数字String[] numberArray = numbers.split(","); // 按逗号分割int sum = 0;for (String numberStr : numberArray) {// 这里应该加上 NumberFormatException 的处理,但为了例子简洁暂不加sum += Integer.parseInt(numberStr); // 累加每个数字}return sum;}
}
  • 运行测试: 再次运行所有测试,确保重构没有破坏功能。它们都应该通过。

这个过程会一直进行下去,每次只添加一点点功能(比如处理换行符分隔、处理负数、忽略大于 1000 的数字等等),为每个新功能写一个测试,让测试通过,然后重构。

这就是 TDD 的基本流程。它通过小步快跑、频繁测试和重构,确保你构建的功能是正确的,并且代码保持整洁。

测试的整洁

整洁测试三要素:可读性、可读性和可读性。

核心思想: 好的测试和好的生产代码一样重要,它们必须是整洁且易于维护的。

整洁测试的五大原则 (F.I.R.S.T.):

F - Fast (快速):

  • 什么意思: 你的测试应该运行得非常快。
  • 为什么重要: 如果测试运行得慢,开发者就不会频繁地运行它们(比如在每次修改代码后)。不频繁运行测试,测试的价值就大打折扣,无法及时发现问题。快速的测试才能融入到小步快跑的 TDD 循环中。

I - Independent (独立):

  • 什么意思: 每个测试用例都应该是独立的,它们不应该相互依赖。一个测试的通过或失败不应该影响到其他测试的运行结果。
  • 为什么重要: 如果测试相互依赖,当一个测试失败时,可能会导致一系列其他测试也跟着失败(级联失败),让你很难判断是哪个测试真正发现了问题,调试会非常困难。独立性也意味着你可以随意调整测试的运行顺序,或者只运行某个特定的测试,而不用担心遗漏依赖项。

R - Repeatable (可重复):

  • 什么意思: 在任何环境(你的开发机、测试服务器、CI/CD 环境)下,无论何时运行,测试都应该给出相同的结果。
  • 为什么重要: 如果测试的结果不可重复(有时通过,有时失败),你就无法信任你的测试套件。它可能是因为外部因素(如网络、时间、文件状态)或测试本身的设计问题导致的不稳定(Flaky Test)。不可重复的测试是最大的障碍,会让人失去对测试的信心。

S - Self-validating (自我验证): 就是用断言,控制台通过或报错,而不是看控制台输出

  • 什么意思: 测试的输出必须是明确的“通过”或“失败”。它应该通过自动化断言(Assert)来判断结果,而不是需要人工去查看日志、比较文件或观察程序行为来判断是否正确。
  • 为什么重要: 自动化测试的目的就是减少人工干预。测试运行完毕后,你只需要看一个简单的报告(比如绿条或红条)就知道代码是否工作正常,不需要花费时间去分析结果。

T - Timely (及时):

  • 什么意思: 测试应该在正确的时间编写。在 TDD 中,正确的时间就是恰好在需要实现对应功能之前
  • 为什么重要: 及时编写测试(先于代码)是 TDD 方法论的核心,它驱动你思考代码如何使用,促进更好的设计,并确保不会遗漏测试。

相关文章:

  • 《代码整洁之道》第5章 格式 - 笔记
  • MRI学习笔记-conjunction analysis
  • docker(3) -- 图形界面
  • 驱动开发硬核特训 · Day 22(下篇): # 深入理解 Power-domain 框架:概念、功能与完整代码剖析
  • 《操作系统真象还原》第十章(1)——输入输出系统
  • 加密算法 AES、RSA、MD5、SM2 的对比分析与案例(AI)
  • 「Docker已死?」:基于Wasm容器的新型交付体系如何颠覆十二因素应用宣言
  • 2025.4.21-2025.4.26学习周报
  • 泰迪杯实战案例超深度解析:基于YOLOv5的农田害虫图像识别系统设计
  • 「Mac畅玩AIGC与多模态04」开发篇01 - 创建第一个 LLM 对话应用
  • 迷你世界UGC3.0脚本Wiki组件事件管理
  • 显存在哪里看 分享查看及优化方法
  • 分布式一致性算法起源思考与应用
  • 从“世界工厂”到“智造之都”:双运放如何改写东莞产业基因?
  • 云原生--核心组件-容器篇-5-Docker核心之-容器
  • 大模型、知识图谱和强化学习三者的结合,可以形成哪些研究方向?
  • 给视频自动打字幕:从Humanoid-X、UH-1到首个人形VLA Humanoid-VLA:迈向整合第一人称视角的通用人形控制
  • 蓝桥杯 1. 确定字符串是否包含唯一字符
  • Suna开源框架分析
  • 广度优先搜索(BFS)算法详解
  • 外交部:对伊朗拉贾伊港口爆炸事件遇难者表示深切哀悼
  • 广东一公司违规开展学科培训被罚没470万,已注销营业执照
  • 下任美联储主席热门人选沃什:美联储犯下“系统性错误”,未能控制一代人以来最严重的通胀
  • 第三款在美获批的国产PD-1肿瘤药来了,影响多大?
  • 宝龙地产:委任中金国际为境外债务重组新的独家财务顾问
  • 韩国检方以受贿嫌疑起诉前总统文在寅