设计模式学习笔记 - 规范与重构 - 2.保证重构不出错的技术手段

前言

很多工程师对重构这种做法是很认同的,面对项目中的烂代码,也想重构一下,但有担心重构之后出问题,出力不讨好。确实,如果你需要重构的代码是别的同事开发的,你不是特别熟练,在没有任何保障的情况下,重构引入 bug 的风险还是挺大的。

那如何保证重构不出错呢?

你需要掌握各种设计思想、设计原则、设计模式,还需要对所重构的业务和代码有足够的了解。除了这些个人能力因素外,最可落地执行、最有效的保证重构不出错的手段应该是单元测试了。当重构完成之后,如果新的代码仍然能通过单元测试,那就说明代码原有逻辑的正确性未被破坏,原有的外部可见行为未变,符合《设计模式学习笔记 - 规范与重构 - 1.什么情况下要重构?重构什么?又该如何重构?》中对重构的定义。

今天,我们来学习下单元测试。学习内容包括:

  • 什么是单元测试?
  • 为什么要写单元测试?
  • 如何编写单元测试?
  • 如何在团队中推行单元测试?

什么是单元测试?

单元测试由工程师自己来编写,用来测试自己写的代码的正确性。我们常常将它跟集成测试放到一起来对比。单元测试相对集成测试来说,测试的粒度更小。

  • 集成测试的测试对象是整个系统或者某个功能模块,比如用户注册、登录功能是否正常,是一种端到端的测试。
  • 而单元测试的测试对象是类或者函数,用来测试一个类或一个函数是否按照预期的逻辑执行。这是代码层级的测试。

举个例子来解释下。

public class Text {private String content;public Text(String content) {this.content = content;}/*** 将字符串转化成数字,忽略字符串中的首尾空格;* 如果字符串中包含除首尾空格之外的非数字字符,则返回null*/public Integer toNumber() {if (content == null || content.isEmpty()) {return null;}// 省略代码实现...return null;}
}

如果我们要测试 Text 类中的 toNumber() 函数的正确性,应该如何编写单元测试?

为保证测试的全面性,针对 toNumber() 函数,我们要设计下面这样几个测试用户:

  1. 入股字符串只包含数字:“123”,toNumber() 函数输出对应的整数:123。
  2. 如果字符串是空或 null,toNumber() 函数返回: null。
  3. 如果字符串包含收尾空格:“ 123”,“123 ”,“ 123 ”,toNumber() 返回对应的整数:123。
  4. 如果字符串包含多个收尾空格:“ 123 ”,toNumber() 返回对应的整数:123。
  5. 如果字符串包含非数字字符:“123a4”,“123 4”, toNumber() 返回 null。

当我们设计好测试用例后,剩下的就是将其翻译成代码。代码贴在下发,你可以参考看一下。

public class Assert {public static void assertEquals(Integer expectedValue, Integer actualValue) {if (expectedValue != actualValue) {String msg = String.format("Test failed, expected: %d, actual: %d", expectedValue, actualValue);System.out.println(msg);} else {System.out.println("Test passed");}}public static boolean assertNull(Integer actualValue) {boolean isNull = actualValue == null;if (isNull) {System.out.println("Test passed");} else {System.out.println("Test failed, the value is not null:" + actualValue);}return isNull;}
}public class TestCaseRunner {public static void main(String[] args) {System.out.println("Run testToNumber()");new TextTest().testToNumber();System.out.println("Run testToNumber_nullOrEmpty()");new TextTest().testToNumber_nullOrEmpty();System.out.println("Run testToNumber_containsLeadingAndTrailingSpaces()");new TextTest().testToNumber_containsLeadingAndTrailingSpaces();System.out.println("Run testToNumber_containsMultiLeadingAndTrailingSpaces()");new TextTest().testToNumber_containsMultiLeadingAndTrailingSpaces();System.out.println("Run testToNumber_containsInvalidCharaters()");new TextTest().testToNumber_containsInvalidCharaters();}
}public class TextTest {public void testToNumber() {Text text = new Text("123");Assert.assertEquals(123, text.toNumber());}public void testToNumber_nullOrEmpty() {Text text1 = new Text(null);Assert.assertNull(text1.toNumber());Text text2 = new Text("");Assert.assertNull(text2.toNumber());}public void testToNumber_containsLeadingAndTrailingSpaces() {Text text1 = new Text(" 123");Assert.assertEquals(123, text1.toNumber());Text text2 = new Text("123 ");Assert.assertEquals(123, text2.toNumber());Text text3 = new Text(" 123 ");Assert.assertEquals(123, text3.toNumber());}public void testToNumber_containsMultiLeadingAndTrailingSpaces() {Text text1 = new Text("  123");Assert.assertEquals(123, text1.toNumber());Text text2 = new Text("123  ");Assert.assertEquals(123, text2.toNumber());Text text3 = new Text("  123  ");Assert.assertEquals(123, text3.toNumber());}public void testToNumber_containsInvalidCharaters() {Text text1 = new Text("123a4");Assert.assertNull(text1.toNumber());Text text2 = new Text("123 4");Assert.assertNull(text2.toNumber());}
}

为什么要写单元测试?

单元测试除了能有效地为重构保驾护航之外,也是保证代码质量最有效的两个手段之一(另一个是 Code Review)。单元测试有以下几点好处。

1.单元测试能有效地帮助你发现代码中的 BUG

能够写出 bug free 的代码,是判断工程师编码能力的重要标准之一。即便是非常有经验的工程师,通过编写单元测试也常常会发现代码中的很多考虑不全的地方。

在尽管现在的开发模式都是“快、糙、猛”,对单元测试根本没有要求,但是我们应该自己坚持为自己提交的每一份代码,都编写完善的单元测试,这样可以让我们编写的代码几乎是bug free 的。这会节省很多 fix 低级 buf 的时间,能够有时间去做其他有意义的事情。

可以这么说,坚持编写单元测试是保证我们代码质量的一种 “杀手锏”,也是帮助工程师拉开与其他人差距的一个小秘密。

2.写单元测试能发现代码设计上的问题

代码的可测试性是评判代码质量的一个重要标准。对于一段代码,如果很难为其编写单元测试,或者单元测试写起来很吃力,需要依靠单元测试框架里很高级的特性才能完成,那玩玩就意味着代码设计得不够合理,比如,没有使用依赖注入、大量使用静态函数、全局变量、代码高度耦合等。

3.写单元测试是对集成测试的有力补充

程序运行的 bug 往往出现在一些边界条件、异常情况下,比如,除数未判空、网络超时。而大部分异常情况都比较难在测试环境中模拟。而单元测试可以利用下一节课中讲到的 mock 方式控制 mock 的对象返回我们需要模拟的异常,来测试代码在这些异常情况的表现。

尽管单元测试无法完全替代集成测试,但如果我们能保证每个类、每个函数都能按照我们的预期来执行,底层 bug 少了,那组装起来的整个系统,出现问题的概率也就相应减少了。

4.写单元测试的过程本身就是代码重构的过程

上一节课《设计模式学习笔记 - 规范与重构 - 1.什么情况下要重构?重构什么?又该如何重构?》中,我们提到要把持续重构作为开发的一部分来执行,那些单元测试实际上就是落地执行持续重构的一个有效途径。设计和实现代码的时候,我们很难把所有问题都考虑清楚。

而编写单元测试就相当于对代码的一次自我 Code Review,在这个过程中,我们可以发现一些设计上的问题(比如代码设计的不可测试),以及代码编写方面的问题(比如一些边界条件处理不当)等,然后针对性的进行重构。

5.阅读单元测试能帮助你快速熟悉代码

阅读代码最有效的手段,就是先了解它的业务背景和设计思路,然后再去看代码,这样代码读起来就会轻松很多。但据我了解,程序员都不怎么喜欢写文档和注释,而大部分程序员写的代码又很难做到“不言自明”。在每文档和注释的情况下,单元测试就起到了替代作用。单元测试用例实际上就是用户用例,反映了代码的功能和如何使用。借助单元测试,我们不需要深入的阅读代码,便能知道代码实现了什么功能,有哪些特殊情况需要考虑,有哪些边界条件需要处理。

6.单元测试是 TDD 可落地执行的改进方案

测试驱动开发(Test-Driven Development,简称 TDD)是一个经常被提及但很少被执行的开发模式。它的核心指导思想是测试用例先于代码编写。不过,要让程序员能彻底地接受和习惯这种开发模式还是挺难的。

个人觉得,单元测试就是对 TDD 的一种改进方案,先写代码,紧接着写单元测试,最后根据单元测试反馈出来问题,再回过头去重构代码。这个开发流程更加容易被接受,更加容易执行落地,而且又兼顾了 TDD 的优点。

如何编写单元测试?

前面在讲什么是单元测试时,我们举了一个 toNumber() 函数写单元测试的例子。根据那个例子,我们可以总结得出,写单元测试就是针对代码设计覆盖各种输入、异常、边界条件的测试用例,并将这些测试用例翻译成代码的过程。

把测试用例翻译成代码的时候,可以利用单元测试框架,来检测测试代码的编写。比如,Java 中比较出名的单元测试框架有 Junit、TestNgG、Spring Test 等。这些框架提供了通用的执行流程(比如执行测试用例的 TestCaseRunner)和工具类库(比如各种 Assert 判断函数)等。借助它们,我们变系诶测试代码的时候,只需要关注测试用例本身的编写即可。

针对 toNumber() 函数的测试用例,我们利用 JUnit 单元测试框架爱重新实现以下,具体代码如下所示。和之前没有利用测试框架的实现方式对比以下,看看是否简化了很多呢?

import org.junit.Test;
import org.junit.Assert;public class TextTest {@Testpublic void testToNumber() {Text text = new Text("123");Assert.assertEquals(Integer.valueOf(123), text.toNumber());}@Testpublic void testToNumber_nullOrEmpty() {Text text1 = new Text(null);Assert.assertNull(text1.toNumber());Text text2 = new Text("");Assert.assertNull(text2.toNumber());}@Testpublic void testToNumber_containsLeadingAndTrailingSpaces() {Text text1 = new Text(" 123");Assert.assertEquals(Integer.valueOf(123), text1.toNumber());Text text2 = new Text("123 ");Assert.assertEquals(Integer.valueOf(123), text2.toNumber());Text text3 = new Text(" 123 ");Assert.assertEquals(Integer.valueOf(123), text3.toNumber());}@Testpublic void testToNumber_containsMultiLeadingAndTrailingSpaces() {Text text1 = new Text("  123");Assert.assertEquals(Integer.valueOf(123), text1.toNumber());Text text2 = new Text("123  ");Assert.assertEquals(Integer.valueOf(123), text2.toNumber());Text text3 = new Text("  123  ");Assert.assertEquals(Integer.valueOf(123), text3.toNumber());}@Testpublic void testToNumber_containsInvalidCharaters() {Text text1 = new Text("123a4");Assert.assertNull(text1.toNumber());Text text2 = new Text("123 4");Assert.assertNull(text2.toNumber());}
}

对于如何使用这些单元测试框架,大部分框架都给出了非常详细的官方文档,你可以自行查阅。这些东西理解和掌握起来没有太大难度,所以这不是本节课的重点。关于如何编写单元测试,我希望给你一些经验总结。

1.编写单元测试真的很耗时吗?

尽管单元测试的代码可能是被测试代码本身的 1~2 倍,写的过程很繁琐,但并不是很耗时。毕竟我们不需要考虑太多代码设计上的问题,测试代码实现起来也比较简单。不同测试用例之间的代码差别可能并不是很大,简答的 copy-paste 改改就行。

2.对单元测试的代码质量有什么要求吗?

单元测试比较不会在生产线上运行,而且每个类的测试代码也比较独立,基本不相互依赖。所以相对于被测代码,我们对单元测试代码的质量可以放低一些要求。命名稍微有些不规范,代码稍微有些重复也都是没有问题的。

3.单元测试只要覆盖率高就够了吗?

单元测试覆盖率是比较容量量化的指标,常常作为单元测试写得好坏的评判标准。有很多工具专门用来做覆盖率统计,比如,JaCoCo、Cobertura、Emma、Clover。覆盖率的计算方式有很多种,比较简单的是语句覆盖,稍微高级点的有:条件覆盖、判定覆盖、路径覆盖。

不管覆盖率的计算方式如何高级,将覆盖率作为衡量单元测试质量的唯一标准是不合理的。实际上,更重要的是要看测试用例是否覆盖了所有可能的情况,特别是一些 corner case。我来举个简单的例子解释下。

像下面这段代码,我们只需要一个测试用例就可以做到覆盖率 100%,比如 cal(10.0, 2.0),但并不代表测试足够全面了,我们还需要考虑,当除数等于 0 的情况下,代码执行是否符合预期。

public double cal(double a, double b) {if (b != 0.0) {return a/b;}
}

实际上过度关注单元测试的覆盖率会导致开发人员为了提高覆盖率,写很多没有必要的测试代码,比如 get、set 方法非常简单,没有必要测试。从过往的经验上来讲,一个项目的单元测试覆盖率在 60~70% 即可上线。如果项目对代码质量要求比较高,可以适当提高单元测试覆盖率的要求。

4.单元测试都需要了解代码的实现逻辑吗?

单元测试不要依赖被测试函数的具体实现逻辑,它只关心被测函数实现了什么功能。我们切不可为了追求覆盖率,逐行阅读代码,然后针对实现逻辑编写单元测试。否则,一旦对代码进行重构,在代码的外部行为不变的情况下,对代码的实现逻辑进行了修改,那原本的单元测试都会运行失败,也就起不到为重构保驾护航的作用了,也违背了我们写单元测试的初衷。

5.如何选择单元测试框架?

写单元测试本身不需要太复杂的技术,大部分单元测试框架都能满足。在公司内部,起码团队内部需要统一单元测试框架。

如果自己写的代码用已经选定的单元测试框架无法测试,那多半是代码写的不够好,代码的可测试性不够好。这个时候,我们要重构自己的代码,让其更容易测试,而不是去找另一个更加高级的框架。

如何在团队中推行单元测试?为什么单元测试难以落地执行?

虽然单元测试是保证重构不出错的有效手段;也有很多人已经认识到单元测试的测试的重要性。但是有多少项目是完善的、高质量的单元测试呢?据我了解非常少,包括一些大公司的项目。所以, 100% 落实执行单元测试是件 “知易行难” 的事。

写单元测试确实是一件考验耐心的活儿。一般情况下,单元测试的代码量要大于被测试代码量,甚至是要多出好几倍。很多人往往会就饿得写单元测试比较繁琐,并且没有太多挑战,而不愿意去做。很多团队和项目在刚开始推行单元测试的时候,还比较认真,执行得比较好。但当开发任务紧了之后,就开始放低对单元测试的要求,一旦出现 “破窗效应”,慢慢地大家就都不写了,这种情况很常见。

还有一种情况就是,由于历史遗留问题,原来的代码都没有写单元测试,代码已经堆砌到十万行了,不可能再一个一个去补单元测试。这种情况下,我们首先要保证新写的代码都要有单元测试,其次,每次在改动到某个类时,如果没有单元测试就补上,不过这要求工程师们有足够的主人翁意识。

此外,还有人觉得,有了测试团队,写单元测试就是浪费时间,没有必要。写好代码直接提交,然后丢给黑盒测试狠命去测,测出问题就反馈给开发团队再修改,测不出问题就留在线上除了问题再修复。

在这样的开发模式下,团队往往觉得没有必要写单元测试,但如果我们把单元测试写好、做好 Code Review,重视起代码质量,其实可以很大程度上减少黑盒测试的投入。

总结

1.什么是单元测试?

单元测试是代码层面的测试,由研发自己编写,用于测试 “自己” 编写的代码的逻辑的正确性。单元测试顾名思义是测试一个 “单元”,有别于集成测试,这个 “单元” 一般是类或函数,而不是模块或系统。

2.为什么要写单元测试?

写单元测试的过程本身就是代码 Code Review 和重构的过程,能有效地发现代码中的 bug 和代码设计上的问题。除此之外,单元测试还是对集成测试的有力补充,还能帮助我们快速熟悉代码,是 TDD 可落地执行的改进方案。

3.如何编写单元测试?

写单元测试就是针对代码设计各种测试用例,以覆盖各种输入、异常、边界情况,并将其翻译成代码。我们可以利用一些测试框架来简化单元测试的编写。除此之外,对于单元测试,我们需要建立以下正确的认知:

  • 编写单元测试尽管繁琐,但并不太耗时。
  • 我们可以稍微放低对单元测试代码质量的要求。
  • 覆盖率作为衡量单元测试的唯一标准是不合理的。
  • 单元测试不要依赖被测代码的具体实现逻辑。
  • 单元测试框架无法测试,多半是因为代码的可测试性不好。

4.单元测试为何难落地执行?

  • 一方面,写单元测试本身比较繁琐,技术挑战不大,很多程序员不愿意去写;
  • 另一方面,国内研发比较偏向 “快、糙、猛”,容易因为开发进度紧,导致单元测试的执行虎头蛇尾。
  • 最后关键问题还是团队没有建立对单元测试正确的认识,觉得可有可无,单靠督促很难执行得很好。

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

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

相关文章

2024 年中国高校大数据挑战赛赛题 D:行业职业技术培训能力评价完整思路以及源代码分享

中国是制造业大国,产业门类齐全,每年需要培养大量的技能娴 熟的技术工人进入工厂。某行业在全国有多所不同类型(如国家级、 省级等)的职业技术培训学校,进行 5 种技能培训。学员入校时需要 进行统一的技能考核&#xf…

数字化转型导师坚鹏:科技金融政策、案例及数字化营销

科技金融政策、案例及数字化营销 课程背景: 很多银行存在以下问题: 不清楚科技金融有哪些利好政策? 不知道科技金融有哪些成功案例? 不知道科技金融如何数字化营销? 课程特色: 以案例的方式解读原…

【个人开发】llama2部署实践(二)——基于GPU部署踩坑

折腾了一整天,踩了GPU加速的一堆坑,记录一下。 1.GPU加速方式 上篇已经写了llama2部署的大概流程:【【个人开发】llama2部署实践(一)】——基于CPU部署 针对llama.cpp文件内容,仅需再make的时候带上参数…

数字建筑欢乐颂,智慧工地共筑美好未来!

在解决农民工人欠薪这一长期困扰建筑业的难题上,某建筑公司响应政策,严格按照实名制管理,实施过程中发现并克服了传统管理模式的痛点:聊天群组的信息时,往往会被淹没在“收到”回复中,影响沟通效率&#xf…

Linux常用操作命令(2)

目录 echo:输出文本到终端或重定向到文件 date:显示当前日期和时间或者设置系统时间 find:查找文件或目录 which:查找命令的路径 kill/killall/pkill:终止进程 ln:创建硬链接或符号链接 sudo&#x…

探索React中的类组件和函数组件

🤍 前端开发工程师、技术日更博主、已过CET6 🍨 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 🕠 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 🍚 蓝桥云课签约作者、上架课程《Vue.js 和 E…

每日一题leetcode-找出数组的第K大和

一.题目解析 读完题目后我们知道,该题就是让我们在子序列中求和,我们要在不同的子序列中排序找到第K大的和。何为子序列? 子序列就是在一个数组中抽出一些元素构成一个新的数组即可,不要求一定是连续的; 例如&#x…

深入解析汽车MCU的软件架构

一、背景知识 电动汽车(EV)正在成为首选的交通方式,为传统内燃机汽车提供了一种可持续发展的环保型替代方案。在电动汽车复杂的生态系统中,众多电子控制单元(ECU)在确保其高效运行方面发挥着至关重要的作用…

《IAB视频广告标准:综合指南(2022)》之概述篇 - 我为什么要翻译介绍美国人工智能科技公司IAB 系列(2)

IAB平台,使命和功能 IAB成立于1996年,总部位于纽约市。 作为美国的人工智能科技巨头社会媒体和营销专业平台公司,互动广告局(IAB- the Interactive Advertising Bureau)自1996年成立以来,先后为700多家媒体…

最优算法100例之03-判断是否是栈的弹出序列

专栏主页:计算机专业基础知识总结(适用于期末复习考研刷题求职面试)系列文章https://blog.csdn.net/seeker1994/category_12585732.html 题目描述 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。 例…

20、电源管理入门之Hypervisor中的电源管理

目录 1. Hypervisor概念介绍 2. 汽车软件中的Hypervisor应用 3. QNX Hypervisor 4. Hypervisor中的多OS通信技术 5. 电源管理相关 参考: 很多时候听说Hypervisor,但是对底层软件技术不了解的人感觉挺神秘。本篇文章简单介绍下Hypervisor的基本概念,另外介绍下电影管理…

linux shell中return、break、continue、exit用法解释

1.return用法解释 linux shell中return用来返回函数的返回值 样例: [rootkibana ~]# cat return.sh #!/bin/bashnum$#function return_test() {if [ $num 0 ];thenreturn 22fi }return_test echo $? [rootkibana ~]# sh return.sh 22 [rootkibana ~]# sh ret…

从零开始:神经网络(2)——MP模型

声明:本文章是根据网上资料,加上自己整理和理解而成,仅为记录自己学习的点点滴滴。可能有错误,欢迎大家指正。 神经元相关知识,详见从零开始:神经网络——神经元和梯度下降-CSDN博客 1、什么是M-P 模型 人…

MySQL时间类型和Mybatis处理

MySQL时间类型和Mybatis处理 以后在Mysql中表示时间,统一用DateTime或者varchar类型 参考: MyBatis 处理 MySQL 时间类型 date 、datetime、timestamp Mysql-基础-时间存储(Date,Java 8 中的日期处理,Json&#xff…

《剑指 Offer》专项突破版 - 面试题 76 : 数组中第 k 大的数字(C++ 实现)

目录 详解快速排序 面试题 76 : 数组中第 k 大的数字 详解快速排序 快速排序是一种非常高效的算法,从其名字可以看出这种排序算法最大的特点是快。当表现良好时,快速排序的速度比其他主要对手(如归并排序)快 2 ~ 3 倍。 快速排…

浏览器与Node.js事件循环:异同点及工作原理

🤍 前端开发工程师、技术日更博主、已过CET6 🍨 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 🕠 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 🍚 蓝桥云课签约作者、上架课程《Vue.js 和 E…

记一次项目所学(中间件等)-动态提醒功能(RocketMQ)

记一次项目所学(中间件等)–动态提醒功能(RocketMQ) 订阅发布模式与观察者模式 RocketMQ:纯java编写的开源消息中间件 高性能低延迟分布式事务 Redis : 高性能缓存工具,数据存储在内存中,读写速度非常快 …

达梦审计功能

达梦审计 前言 根据达梦官网文档整理 一、为什么要做数据库审计 增强内部安全,可解答是哪个对象/人员更改了关键数据,并在何时进行的更改。 安全等保、内审等合规条件的基本需求 定责、追责等提供依据,起到威慑作用 二、谁去做审计 安…

Meta正打造一个巨型AI模型,旨在为其“整个视频生态系统”提供动力,一位高管透露

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗?订阅我们的简报,深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同,从行业内部的深度分析和实用指南中受益。不要错过这个机会,成为AI领…

Swift 入门学习:集合(Collection)类型趣谈-上

概览 集合的概念在任何编程语言中都占有重要的位置,正所谓:“古来聚散地,宿昔长荆棘;游人聚散中,一片湖光里”。把那一片片、一瓣瓣、一粒粒“可耐”的小精灵全部收拢、吸纳的井然有序、条条有理,怎能不让…