《代码整洁之道》第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 方法论的核心,它驱动你思考代码如何使用,促进更好的设计,并确保不会遗漏测试。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/903376.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

openGauss新特性 | DataKit支持PostgreSQL到openGauss的迁移能力

Postgresql-\>openGauss迁移工具debezium-connector-postgres 可获得性 本特性自openGauss 7.0.0-RC1版本开始引入。 特性简介 debezium-connector-postgres工具是一个基于Java语言的Postgresql到openGauss的复制工具。该工具提供了初始全量数据及对象(视图、…

在MySQL Shell里 重启MySQL 8.4实例

前一段时间看到MySQL官方视频的Oracle工程师在mysql shell里面重启mysql实例,感觉这个操作很方便,所以来试试,下面为该工程师的操作截图 1.MySQL Shell 通过root用户连上mysql,shutdown mysql实例 [rootmysql8_3 bin]# mysqlshMy…

truffle

文章目录 truffle目录结构各文件作用在本地测试合约 truffle 项目来自https://github.com/Dapp-Learning-DAO/Dapp-Learning/blob/main/basic/04-web3js-truffle/README-CN.md Truffle 是基于 Solidity 语言的一套开发框架,它简化了去中心化应用(Dapp&…

SpringCloud核心组件Eureka菜鸟教程

关于Spring Cloud Eureka的核心概念 Eureka 是 Netflix 开源的一款基于 REST 的服务发现工具,主要用于中间层服务器的云端负载均衡。它通过维护一个服务注册表来实现服务之间的通信1。在 Spring Cloud 中,Eureka 提供了一个高可用的服务注册与发现机制&a…

职业教育新形态数字教材的建设与应用:重构教育生态的数字化革命

教育部新时代职业学校名师(名匠)名校长培养计划专题 四川省第四批职业学校名师(名匠)培养计划专题 在某职业院校的智能制造课堂上,学生佩戴VR设备,通过数字教材中的虚拟工厂完成设备装配训练,系统实时生成操作评分与改进建议。这一场景折射出…

基于Python的携程国际机票价格抓取与分析

一、项目背景与目标 携程作为中国领先的在线旅行服务平台,提供了丰富的机票预订服务。其国际机票价格受多种因素影响,包括季节、节假日、航班时刻等。通过抓取携程国际机票价格数据,我们可以进行价格趋势分析、性价比评估以及旅行规划建议等…

Windows 图形显示驱动开发-初始化WDDM 1.2 和 PnP

(WDDM) 1.2 及更高版本显示微型端口驱动程序的所有 Windows 显示驱动程序都必须支持以下行为,以响应即插即用 (PnP) 基础结构启动和停止请求。 根据驱动程序返回成功或失败代码,或者系统硬件是基于基本输入/输出系统 (BIOS) 还是统一可扩展固件接口 (UEF…

【1区SCI】Fusion entropy融合熵,多尺度,复合多尺度、时移多尺度、层次 + 故障识别、诊断-matlab代码

引言 2024年9月,研究者在数学领域国际顶级SCI期刊《Chaos, Solitons & Fractals》(JCR 1区,中科院1区 Top)上以“Fusion entropy and its spatial post-multiscale version: Methodology and application”为题发表最新科学研…

高并发架构设计之缓存

一、引言 缓存技术作为高并发架构设计的基石之一,通过数据暂存和快速访问机制,在提升系统性能、降低后端负载方面发挥着不可替代的作用。优秀的缓存设计能够将系统吞吐量提升数个数量级,将响应时间从秒级降至毫秒级,甚至成为系统…

Unity AI-使用Ollama本地大语言模型运行框架运行本地Deepseek等模型实现聊天对话(一)

一、Ollama介绍 官方网页:Ollama官方网址 中文文档参考:Ollama中文文档 相关教程:Ollama教程 Ollama 是一个开源的工具,旨在简化大型语言模型(LLM)在本地计算机上的运行和管理。它允许用户无需复杂的配置…

Docker Python 镜像使用指南

1. 使用 Python 镜像创建容器 docker run -itd -v /data:/data python:latest 作用:创建一个基于 python:latest 镜像的容器,并后台运行。 参数说明: -itd:交互式后台运行(-i 交互模式,-t 分配伪终端&…

matlab中Simscape的调用-入门

Simscape 是由 MathWorks 公司开发的一款基于物理建模的仿真工具,它建立在 MATLAB/Simulink 平台之上,专门用于建模和仿真多领域物理系统。 主要特点 多领域建模:Simscape 提供了丰富的物理元件库,涵盖了机械、电气、液压、气动…

Flowable7.x学习笔记(十三)查看部署流程图

前言 Flowable 的流程图是 Flowable Modeler 或 Process Editor 中,使用拖拽和属性面板基于 BPMN 2.0 元素(如任务、网关、事件、序列流等)渲染出的业务流程图形界面​。 一、将图形导出可查看的作用 ① 可视化建模 帮助业务分析师和开发者…

Bootstrap 模态框

Bootstrap 模态框 Bootstrap 模态框(Modal)是 Bootstrap 框架中的一个组件,它允许你在一个页面中创建一个模态对话框,用于显示内容、表单、图像或其他信息。模态框通常覆盖在当前页面上,提供了一种不离开当前页面的交…

python-69-基于graphviz可视化软件生成流程图

文章目录 1 Graphviz可视化软件1.1 graphviz简介1.2 安装部署2 基于python示例应用2.1 基本示例2.2 解决中文显示乱码2.3 显示多个输出边2.4 显示输出引脚名称2.5 从左至右显示布局2.6 设置节点为方形3 参考附录1 Graphviz可视化软件 1.1 graphviz简介 Graphviz(Graph Visua…

AJAX 介绍

一、什么是AJAX ? AJAX 是 异步的 JavaScript 和 XML(Asynchronous JavaScript And XML) 的缩写,是一种实现浏览器与服务器进行数据通信的技术。其核心是通过 XMLHttpRequest 对象在不重新刷新页面的前提下,与服务器交换数据并更…

新ubuntu物理机开启ipv6让外网访问

Ubuntu 物理机 SSH 远程连接与 IPv6 外网访问测试指南 1. 通过 SSH 远程连接 Ubuntu 物理机 1.1 安装 SSH 服务 sudo apt update sudo apt install openssh-server1.2 检查 SSH 服务状态 sudo systemctl status ssh确认出现 active (running)。 1.3 获取物理机 IP 地址 i…

linux系统上使用nginx访问php文件返回File not found错误处理方案

linux系统上使用nginx访问php文件返回File not found错误处理方案 第一种情况第二种情况 第一种情况 可以在你的location php 里面添加当文件不存在时返回404而不是交给php-fpm进行处理 location ~ \.php$ { ... #文件不存在转404 try_files $uri 404; ... }然后&#xff0c…

基于 SpringBoot 与 Redis 的缓存预热案例

文章目录 “缓存预热” 是什么?项目环境搭建创建数据访问层预热数据到 Redis 中创建缓存服务类测试缓存预热 “缓存预热” 是什么? 缓存预热是一种优化策略,在系统启动或者流量高峰来临之前,将一些经常访问的数据提前加载到缓存中…

java—11 Redis

目录 一、Redis概述 二、Redis类型及编码 三、Redis对象的编码 1. 类型&编码的对应关系 2. string类型常用命令 (1)string类型内部实现——int编码 (2)string类型内部实现——embstr编码 ​编辑 (3&#x…