极客时间已完结课程限时免费阅读

28 | 理论二:为了保证重构不出错,有哪些非常能落地的技术手段?

28 | 理论二:为了保证重构不出错,有哪些非常能落地的技术手段?-极客时间

28 | 理论二:为了保证重构不出错,有哪些非常能落地的技术手段?

讲述:冯永吉

时长19:24大小15.55M

上一节课中,我们对“为什么要重构、到底重构什么、什么时候重构、该如何重构”,做了概括性介绍,强调了重构的重要性,希望你建立持续重构意识,将重构作为开发的一部分来执行。
据我了解,很多程序员对重构这种做法还是非常认同的,面对项目中的烂代码,也想重构一下,但又担心重构之后出问题,出力不讨好。确实,如果你要重构的代码是别的同事开发的,你不是特别熟悉,在没有任何保障的情况下,重构引入 bug 的风险还是很大的。
那如何保证重构不出错呢?你需要熟练掌握各种设计原则、思想、模式,还需要对所重构的业务和代码有足够的了解。除了这些个人能力因素之外,最可落地执行、最有效的保证重构不出错的手段应该就是单元测试(Unit Testing)了。当重构完成之后,如果新的代码仍然能通过单元测试,那就说明代码原有逻辑的正确性未被破坏,原有的外部可见行为未变,符合上一节课中我们对重构的定义。
那今天我们就来学习一下单元测试。今天的内容主要包含这样几个内容:
什么是单元测试?
为什么要写单元测试?
如何编写单元测试?
如何在团队中推行单元测试?
话不多说,让我们现在就开始今天的学习吧!

什么是单元测试?

单元测试由研发工程师自己来编写,用来测试自己写的代码的正确性。我们常常将它跟集成测试放到一块来对比。单元测试相对于集成测试(Integration Testing)来说,测试的粒度更小一些。集成测试的测试对象是整个系统或者某个功能模块,比如测试用户注册、登录功能是否正常,是一种端到端(end to end)的测试。而单元测试的测试对象是类或者函数,用来测试一个类和函数是否都按照预期的逻辑执行。这是代码层级的测试。
这么说比较理论,我举个例子来解释一下。
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() 函数,我们需要设计下面这样几个测试用例。
如果字符串只包含数字:“123”,toNumber() 函数输出对应的整数:123。
如果字符串是空或者 null,toNumber() 函数返回:null。
如果字符串包含首尾空格:“ 123”,“123 ”,“ 123 ”,toNumber() 返回对应的整数:123。
如果字符串包含多个首尾空格:“ 123 ”,toNumber() 返回对应的整数:123;
如果字符串包含非数字字符:“123a4”,“123 4”,toNumber() 返回 null;
当我们设计好测试用例之后,剩下的就是将其翻译成代码了。翻译成代码的过程非常简单,我把代码贴在下面了,你可以参考一下(注意,我们这里没有使用任何测试框架)。
public class Assert {
public static void assertEquals(Integer expectedValue, Integer actualValue) {
if (actualValue != expectedValue) {
String message = String.format(
"Test failed, expected: %d, actual: %d.", expectedValue, actualValue);
System.out.println(message);
} else {
System.out.println("Test succeeded.");
}
}
public static boolean assertNull(Integer actualValue) {
boolean isNull = actualValue == null;
if (isNull) {
System.out.println("Test succeeded.");
} 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)。我在 Google 工作的时候,写了大量的单元测试代码,结合我的这些开发经验,我总结了以下几点单元测试的好处。尽管有些听起来有点“务虚”,但如果你认真写过一些单元测试的话,应该会很有共鸣。

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

能否写出 bug free 的代码,是判断工程师编码能力的重要标准之一,也是很多大厂面试考察的重点,特别是像 FLAG 这样的外企。即便像我这样代码写了十几年,逻辑还算缜密、清晰的人,通过单元测试也常常会发现代码中的很多考虑不全面的地方。
在离开 Google 之后,尽管我就职的很多公司,其开发模式都是“快、糙、猛”,对单元测试根本没有要求,但我还是坚持为自己提交的每一份代码,都编写完善的单元测试。得益于此,我写的代码几乎是 bug free 的。这也节省了我很多 fix 低级 bug 的时间,能够有时间去做其他更有意义的事情,我也因此在工作上赢得了很多人的认可。可以这么说,坚持写单元测试是保证我的代码质量的一个“杀手锏”,也是帮助我拉开与其他人差距的一个“小秘密”。

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

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

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

程序运行的 bug 往往出现在一些边界条件、异常情况下,比如,除数未判空、网络超时。而大部分异常情况都比较难在测试环境中模拟。而单元测试可以利用下一节课中讲到的 mock 的方式,控制 mock 的对象返回我们需要模拟的异常,来测试代码在这些异常情况的表现。
除此之外,对于一些复杂系统来说,集成测试也无法覆盖得很全面。复杂系统往往有很多模块。每个模块都有各种输入、输出、异常情况,组合起来,整个系统就有无数测试场景需要模拟,无数的测试用例需要设计,再强大的测试团队也无法穷举完备。
尽管单元测试无法完全替代集成测试,但如果我们能保证每个类、每个函数都能按照我们的预期来执行,底层 bug 少了,那组装起来的整个系统,出问题的概率也就相应减少了。

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

上一节课中,我们提到,要把持续重构作为开发的一部分来执行,那写单元测试实际上就是落地执行持续重构的一个有效途径。设计和实现代码的时候,我们很难把所有的问题都想清楚。而编写单元测试就相当于对代码的一次自我 Code Review,在这个过程中,我们可以发现一些设计上的问题(比如代码设计的不可测试)以及代码编写方面的问题(比如一些边界条件处理不当)等,然后针对性的进行重构。

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

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

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

测试驱动开发(Test-Driven Development,简称 TDD)是一个经常被提及但很少被执行的开发模式。它的核心指导思想就是测试用例先于代码编写。不过,要让程序员能彻底地接受和习惯这种开发模式还是挺难的,毕竟很多程序员连单元测试都懒得写,更何况在编写代码之前先写好测试用例了。
我个人觉得,单元测试正好是对 TDD 的一种改进方案,先写代码,紧接着写单元测试,最后根据单元测试反馈出来问题,再回过头去重构代码。这个开发流程更加容易被接受,更加容易落地执行,而且又兼顾了 TDD 的优点。

如何编写单元测试?

前面在讲什么是单元测试的时候,我们举了一个给 toNumber() 函数写单元测试的例子。根据那个例子,我们可以总结得出,写单元测试就是针对代码设计覆盖各种输入、异常、边界条件的测试用例,并将这些测试用例翻译成代码的过程。
在把测试用例翻译成代码的时候,我们可以利用单元测试框架,来简化测试代码的编写。比如,Java 中比较出名的单元测试框架有 Junit、TestNG、Spring Test 等。这些框架提供了通用的执行流程(比如执行测试用例的 TestCaseRunner)和工具类库(比如各种 Assert 判断函数)等。借助它们,我们在编写测试代码的时候,只需要关注测试用例本身的编写即可。
针对 toNumber() 函数的测试用例,我们利用 Junit 单元测试框架重新实现一下,具体代码如下所示。你可以拿它跟之前没有利用测试框架的实现方式对比一下,看是否简化了很多呢?
import org.junit.Assert;
import org.junit.Test;
public class TextTest {
@Test
public void testToNumber() {
Text text = new Text("123");
Assert.assertEquals(new Integer(123), text.toNumber());
}
@Test
public void testToNumber_nullorEmpty() {
Text text1 = new Text(null);
Assert.assertNull(text1.toNumber());
Text text2 = new Text("");
Assert.assertNull(text2.toNumber());
}
@Test
public void testToNumber_containsLeadingAndTrailingSpaces() {
Text text1 = new Text(" 123");
Assert.assertEquals(new Integer(123), text1.toNumber());
Text text2 = new Text("123 ");
Assert.assertEquals(new Integer(123), text2.toNumber());
Text text3 = new Text(" 123 ");
Assert.assertEquals(new Integer(123), text3.toNumber());
}
@Test
public void testToNumber_containsMultiLeadingAndTrailingSpaces() {
Text text1 = new Text(" 123");
Assert.assertEquals(new Integer(123), text1.toNumber());
Text text2 = new Text("123 ");
Assert.assertEquals(new Integer(123), text2.toNumber());
Text text3 = new Text(" 123 ");
Assert.assertEquals(new Integer(123), text3.toNumber());
}
@Test
public 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) {
return a / b;
}
}
实际上,过度关注单元测试的覆盖率会导致开发人员为了提高覆盖率,写很多没有必要的测试代码,比如 get、set 方法非常简单,没有必要测试。从过往的经验上来讲,一个项目的单元测试覆盖率在 60~70% 即可上线。如果项目对代码质量要求比较高,可以适当提高单元测试覆盖率的要求。

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

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

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

写单元测试本身不需要太复杂的技术,大部分单元测试框架都能满足。在公司内部,起码团队内部需要统一单元测试框架。如果自己写的代码用已经选定的单元测试框架无法测试,那多半是代码写得不够好,代码的可测试性不够好。这个时候,我们要重构自己的代码,让其更容易测试,而不是去找另一个更加高级的单元测试框架。

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

虽然很多书籍中都会讲到,单元测试是保证重构不出错的有效手段;也有非常多人已经认识到单元测试的重要性。但是有多少项目有完善的、高质量的单元测试呢?据我了解,真的非常非常少,包括 BAT 这样级别公司的项目。如果不相信的话,你可以去看一下国内很多大厂开源的项目,有很多项目完全没有单元测试,还有很多项目的单元测试写得非常不完备,仅仅测试了逻辑是否运行正确而已。所以,100% 落实执行单元测试是件“知易行难”的事。
写单元测试确实是一件考验耐心的活儿。一般情况下,单元测试的代码量要大于被测试代码量,甚至是要多出好几倍。很多人往往会觉得写单元测试比较繁琐,并且没有太多挑战,而不愿意去做。有很多团队和项目在刚开始推行单元测试的时候,还比较认真,执行得比较好。但当开发任务紧了之后,就开始放低对单元测试的要求,一旦出现破窗效应,慢慢的,大家就都不写了,这种情况很常见。
还有一种情况就是,由于历史遗留问题,原来的代码都没有写单元测试,代码已经堆砌了十几万行了,不可能再一个一个去补单元测试。这种情况下,我们首先要保证新写的代码都要有单元测试,其次,每次在改动到某个类时,如果没有单元测试就顺便补上,不过这要求工程师们有足够强的主人翁意识(ownership),毕竟光靠 leader 督促,很多事情是很难执行到位的。
除此之外,还有人觉得,有了测试团队,写单元测试就是浪费时间,没有必要。程序员这一行业本该是智力密集型的,但现在很多公司把它搞成劳动密集型的,包括一些大厂,在开发过程中,既没有单元测试,也没有 Code Review 流程。即便有,做的也是差强人意。写好代码直接提交,然后丢给黑盒测试狠命去测,测出问题就反馈给开发团队再修改,测不出的问题就留在线上出了问题再修复。
在这样的开发模式下,团队往往觉得没有必要写单元测试,但如果我们把单元测试写好、做好 Code Review,重视起代码质量,其实可以很大程度上减少黑盒测试的投入。我在 Google 的时候,很多项目几乎没有测试团队参与,代码的正确性完全靠开发团队来保障,线上 bug 反倒非常少。
以上是我对单元测试的认知和实践心得。现在互联网信息如此的公开透明,网上有很多文章可以参考,对于程序员这个具有很强学习能力的群体来说,学会如何写单元测试并不是一件难事,难的是能够真正感受到它的作用,并且打心底认可、能 100% 落地执行。这也是我今天的课程特别想传达给你的一点。

重点回顾

好了,今天的内容到此就讲完了。我们来一块总结回顾一下,你需要掌握的重点内容。
1. 什么是单元测试?
单元测试是代码层面的测试,由研发自己来编写,用于测试“自己”编写的代码的逻辑的正确性。单元测试顾名思义是测试一个“单元”,有别于集成测试,这个“单元”一般是类或函数,而不是模块或者系统。
2. 为什么要写单元测试?
写单元测试的过程本身就是代码 Code Review 和重构的过程,能有效地发现代码中的 bug 和代码设计上的问题。除此之外,单元测试还是对集成测试的有力补充,还能帮助我们快速熟悉代码,是 TDD 可落地执行的改进方案。
3. 如何编写单元测试?
写单元测试就是针对代码设计各种测试用例,以覆盖各种输入、异常、边界情况,并将其翻译成代码。我们可以利用一些测试框架来简化单元测试的编写。除此之外,对于单元测试,我们需要建立以下正确的认知:
编写单元测试尽管繁琐,但并不是太耗时;
我们可以稍微放低对单元测试代码质量的要求;
覆盖率作为衡量单元测试质量的唯一标准是不合理的;
单元测试不要依赖被测代码的具体实现逻辑;
单元测试框架无法测试,多半是因为代码的可测试性不好。
4. 单元测试为何难落地执行?
一方面,写单元测试本身比较繁琐,技术挑战不大,很多程序员不愿意去写;另一方面,国内研发比较偏向“快、糙、猛”,容易因为开发进度紧,导致单元测试的执行虎头蛇尾。最后,关键问题还是团队没有建立对单元测试正确的认识,觉得可有可无,单靠督促很难执行得很好。

课堂讨论

今天的课堂讨论有以下两个:
你参与的项目有没有写单元测试?单元测试是否足够完备?贯彻执行写单元测试的过程中,遇到过哪些问题?又是如何解决的?
在面试中,我经常会让候选人写完代码之后,列举几个测试用例,以此来考察候选人考虑问题是否全面,特别是针对一些边界条件的处理。所以,今天的另一个课堂讨论话题就是:写一个二分查找的变体算法,查找递增数组中第一个大于等于某个给定值的元素,并且为你的代码设计完备的单元测试用例。
欢迎在留言区写下你的答案,和同学一起交流和分享。如果有收获,也欢迎你把这篇文章分享给你的朋友。
分享给需要的人,Ta购买本课程,你将得29
生成海报并分享

赞 59

提建议

上一篇
27 | 理论一:什么情况下要重构?到底重构什么?又该如何重构?
下一篇
29 | 理论三:什么是代码的可测试性?如何写出可测试性好的代码?
 写留言

精选留言(129)

  • ちよくん
    2020-01-19
    我就比较喜欢写单元测试,所以基本上是无bug 。身边的同事测基本上都是写完往哪一扔,或者丢给测试,然后bug 一堆,慢慢的我就成了团队的核心负责人。😂
    共 23 条评论
    191
  • Aaron
    2020-01-06
    半年前,因为团队项目太多太乱已经很难维护和协作开发(10人的开发团队,每人负责一些项目,水平参差不齐,各自独立开发),作为团队中的资深者,我被leader要求开发一套通用的底层框架。 为保证代码质量,刚开始时对自己要求严格,每个方法都必须要有多种case的单元测试;然后发现有时候写出来的单元测试代码比被测试的方法的代码量多很多,在一定程度上影响了开发速度。另外leader还经常安排我去修复一些仍在艰难运行的旧系统的故障(大多是累积下来的技术债),导致框架开发进展一再拖延。同时团队其他人很少有写单元测试代码,测试工作完全依赖测试人员完成,对自己也就逐渐放松了要求,单元测试不再追求完备,只在核心的方法中加入常规的实现逻辑测试,其他代码写完多看两遍确认无bug就提交。 争哥的这节课程我完全理解,单元测试的重要性毋庸置疑,可是在实际开发过程中完全落实存在一定的困难,遇到这种问题我还真没啥解决的办法除了让自己拼命的加班,真的太难了。。。
    展开
    共 28 条评论
    162
  • potato00fa
    2020-01-07
    单元测试很重要,但是为什么大多人都会放弃?我个人觉得最主要的原因并不是代码量大,难以编写等,而是跑单元测试的次数少。很多单元测试都是为了写而写,写完一次可能都不去运行或者只偶尔运行一两次。如果是每次改完代码,都跑一遍单元测试,单元测试的效果会越来越显现。如果只是为了运行一两次或者干脆为了写而写,很容易就会放弃继续写单元测试。

    作者回复: 可以集成到代码管理仓库git中,强制跑单元测试成功之后才能提交

    43
  • 2020-01-06
    之前公司要求新研的代码要达到百分之85的覆盖率,导致纯粹为了覆盖率堆砌了一堆单元用例。有没有单元测试写的比较好的开源框架推荐下?

    作者回复: 哈哈,那就看我的项目啊:https://github.com/wangzheng0822 下面有个ratelimiter4j

    共 4 条评论
    38
  • 刘大明
    2020-01-06
    说起来真的是难受。整个项目中就我一个人写单元测试。每次做的功能都有单元测试覆盖。而且项目中junit包都是我导入的。更加奇葩的是我的功能单元测试领导还不让我提交。说是你的测试代码为什么要提交,我瞬间无语了。

    作者回复: 😂 可以考虑跳槽了

    共 10 条评论
    30
  • 拙言
    2020-01-06
    打卡~ 这里问下王争老师,如果到了具体的业务代码,该怎么写单元测试呢,单元测试正确标准是什么呢,以sql查询到的结果吗?

    作者回复: 涉及到数据库的项目,特别是重度依赖数据库的,确实比较难写单元测试。一种方式使用DBUNIT这样的测试框架来解耦合真正的数据库,另一种方式专门维护一个供单元测试用的数据库。

    共 6 条评论
    29
  • yaomon
    2020-01-06
    程序员这一行业本该是智力密集型的,但现在很多公司把它搞成劳动密集型的,包括一些大厂,在开发过程中,既没有单元测试,也没有 Code Review 流程。即便有,做的也是差强人意。 ---------------------------------------------------------------------------------------- 差强人意:指尚能使人满意。根据文章上文,明显是不能使人满意的意思。处理为语病。
    展开

    编辑回复: 差强人意是勉强使人满意,不是十分使人满意。所以这里没问题呢~

    共 12 条评论
    14
  • 李小四
    2020-01-06
    设计模式_28 1. 有过一次失败的单元测试经验:好不容易申请到了2周的预研时间,我开开心心地研究怎么把JUnit引入项目,刚开始了两天,新的开发任务打断了我的计划,然后就再也没有继续了。。。 2. 代码: /** * 查找递增数组中第一个大于等于某个给定值的元素 * @return -1: 未找到 */ public int findFirstEqualOrLargerIndex(int[] array, int num) { if (array == null || array.length == 0) return -1; int start = 0; int end = array.length - 1; while (start != end) { int middle = start + (end - start) / 2; if (array[middle] >= num) { if (start == middle) return middle; else if (array[middle - 1] < num) return middle; else end = middle -1; } else { start = middle + 1; } } //start == end if (array[start] >= num) { return start; } else { return -1; } } 测试用例: findFirstEqualOrLargerIndex(null, 1) findFirstEqualOrLargerIndex(new int [0], 1) findFirstEqualOrLargerIndex(new int [] {0}, 1) findFirstEqualOrLargerIndex(new int [] {1}, 1) findFirstEqualOrLargerIndex(new int [] {0, 0}, 1) findFirstEqualOrLargerIndex(new int [] {0, 1}, 1) findFirstEqualOrLargerIndex(new int [] {1, 1}, 1) findFirstEqualOrLargerIndex(new int [] {0, 1, 2}, 1) findFirstEqualOrLargerIndex(new int [] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, 1) findFirstEqualOrLargerIndex(new int [] {0, 1, 1, 1, 1, 1, 6, 7, 8, 9}, 1) findFirstEqualOrLargerIndex(new int [] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, 10) 我估计应该有漏洞,请老师和同学们指正~
    展开
    共 9 条评论
    11
  • Miaozhe
    2020-01-08
    项目是服务端项目中,使用的是Spring test,立足于能满足自测工具的诉求,能保证用例有资产继承,而不是使用postman。用例主要是以接口层(Cnotrol)为主,services层为补充。 好处很多,特别是微重构时,老用例一通过,自己的心就踏实了一半。 另外,有一个体会,如果认真写了单元测试,转测后,测试基本测试不出问题。一个月度版本,bug可以控制在1个以内。
    展开
    共 1 条评论
    8
  • Frank
    2020-01-06
    以前在开发中,没有写单元测试的意识。开发完功能后,直接去测试一个完整的流程。即前端发请求,服务端处理,看数据库数据。如果功能正确就过。这是从一个功能宏观去考虑测试。而单元测试是更细粒度的测试,它在保证各个“单元”都测试通过的情况下整个功能模块就测试通过了。这样的方式对于我们自己来说对代码可控粒度更细。更能比较清楚的理解某个“单元”在整个功能模块调用链路上的位置,承担什么职责,以及有什么行为。而不是一开始就站在模块宏观角度来思考。通过一个个单元测试的编写,将整个功能模块串联起来,最终达到整个功能模块的全局认知。 这也体现了任务分解的思想。通过单元测试,可以从另外一方面实现对已编写的代码的CodeReview,重新梳理流程。也为以后有重构需求打下基础。 目前参与的项目中有单元测试,但是不够完备。可能由于某些原因(开发人员意识问题,团队对单元测试的执行落地程度不够等)。在写单元测试的过程中,遇到单元测试依赖数据库查询问题,因为存在多套环境,如开发环境,仿真环境,线上环境。对于依赖数据查询的单元测试,只能自己造假数据来解决。不知道还有什么好的解决办法。
    展开

    作者回复: 涉及到数据库的确实比较难写单元测试,而且如果重度依赖数据库,业务逻辑又不复杂,单元测试确实没有太大意义。这个时候,集成测试可能更有意义些。

    共 6 条评论
    8
  • 逍遥思
    2020-01-06
    独立开发者,项目代码量 10W 行以内,在可以预见的未来不会超过 20W 行 以前试过 git 各种最佳实践,最后发现一个分支基本就够用了 所以还是忍不住想问问老师,如果项目真没那么大,是否需要单元测试?

    作者回复: 需要的,这个跟项目大不大没太多关系的。单元测试还是为了保证代码少些低级bug

    共 12 条评论
    7
  • Jxin
    2020-01-06
    1.tdd是以终为始的开发模式。即先确定好验收标准,再根据标准去开发。如此一来设计出来的代码跟验收标准能更好的关联。至于单元测试,单元测试的case与tdd的终并不是直接一一对应的,但也可以算是一个终拆解出来的细力度的子终。但单元测试是实现层面的自检方案,tdd是设计层面的衡量指标,感觉是两个层面的概念,形似而神不同。 2.我接手的项目,没几行单元测试,且年久失修也基本全部无用。起初也是坚守写单元测试,补充涉及到的业务的单元测试。但坚持补了40%左右后也就放弃了。原因,1.补别人的测试用例太耗时,而且不全面(短时间了解并不透彻,也不该花太多时间都了解透彻)。2.个中价值不被认可(在一个快糙猛的大环境下,逆行总归异类。你可以接受额外的加班,但很难在他人评价上坚守初心)3.事出必有因,快糙猛也没有错,毕竟技术债务这东西是可以不还的(遗憾的是,有利可图时还不知道还债,硬是要债高难还时再推倒重做)。
    展开
    7
  • 辣么大
    2020-01-06
    关于问题2,尝试写了一下单元测试: https://github.com/gdhucoder/Algorithms4/tree/master/designpattern/u28
    7
  • 再见孙悟空
    2020-01-06
    确实单元测试只在一开始才写过,后来发现拖慢了开发的进度,就渐渐放弃了,现在我们的开发团队里几乎没什么单元测试,除非一些涉及到优惠券,订单奖励计算等和钱挂钩的业务,我觉得一方面是因为消耗时间,另一方面业务代码没写好,很多时候很不利于进行单测,要造各种数据。我觉得单测最好是在开发一个方法或者函数之后就进行,且要在测试介入之前,否则测试介入以后再补充单元测试,有可能会改动到已写好的业务,那么就又需要回归测试一遍,对开发,测试都是很消耗的。
    展开
    共 4 条评论
    6
  • RD
    2021-03-04
    说到测试,让我想起了 leetcode ~ 自己写的算法,自己也写了单元测试,觉得没啥问题,然后 leetcode 上一跑发现,我去对啊,这个边界没考虑到。然后修改,觉得没啥问题了,在跑。。。我去。。对啊。。还有这种情况呢。。。果然,面试考算法没毛病啊~
    5
  • FIGNT
    2020-01-07
    争哥。马上过年了,过年期间不知道能否多发表几篇在过年期间学习?

    编辑回复: 春节期间正常发布,惊喜待定😂

    4
  • 平风造雨
    2020-01-06
    个人觉得,对于大多数公司而言,单元测试没有强制要求,对自己有要求的工程师往往加班写单元测试。
    4
  • 小晏子
    2020-01-06
    参与的项目都有单元测试,不过有些不够完备,缺少了一些corner case的覆盖,感觉后面要注意加上。遇到的一个问题是对于repository层(数据访问层)的测试,用了一个取巧的办法解决的,因为要依赖数据库,可是又没找到对应数据库的mock方式,所以使用了docker镜像的方式来做的,感觉也不错,避免了构造数据的麻烦。不知道还有没有更好的办法?

    作者回复: DBunit了解一下~

    共 8 条评论
    5
  • 2020-01-06
    我感觉我写单测最大的问题在于很难把代码写成那种细粒度可测的模样,而不是要去写。
    4
  • 番茄炒西红柿
    2020-01-08
    问一下单元测试中的依赖问题只能用mock来模拟吗?这样不会导致对下层方法依赖太强,而且用mock模拟感觉代码量也很多,心里感觉也不一定对。如果加入依赖(先倒入测试数据),那不就变成集成测试了吗?

    作者回复: 解耦依赖目前来看就只能用mock的方式。这是跟集成测试最大的区别。

    共 3 条评论
    2