InfoQ观点:测试驱动开发其实是设计技能

本文要点

  • 我们知道,任何软件都必须通过测试确保其功能符合要求;此外我们还要测试软件的非功能性指标,如安全性、可用性,尤其是可维护性。
  • 测试驱动开发(TDD)是一种成熟的技术,可以帮助开发者交付更优秀的软件、缩短交付周期,并使交付周期更加稳定。
  • TDD的理念很简单:在编写生产代码之前先编写测试代码。但要实践好这种“简单”的理念也需要技巧和判断力。
  • TDD实际上是一种设计技能。TDD的本质是利用一系列小型测试来从零开始一步步设计系统,并在系统逐渐成型的过程中快速获得价值。这种技能其实改名叫测试驱动设计更合适些。
  • 一般来说,为某个问题开发解决方案时,第一步就是不管问题有多复杂,先分析问题的结构并把它拆分成许多小需求。之后这些需求就可以逐步实现,解决每一小步时都要同时考虑输入和输出场景。

我们需要测试软件以确保其满足要求;软件要正确响应输入(输入验证),在可接受的时间内完成任务(性能测试),可以被用户正常安装和运行(部署测试),还要达成利益相关方的目标。这些目标可能是业务成果或业务功能,诸如安全性、可用性、可维护性等。

测试类型包括:

  • 烟雾测试和可用性测试,检查软件的基本功能运行情况。
  • 持续测试,每次迭代时运行,比如运行Maven时做测试。
  • 回归测试,每次添加新的编程代码或改动现有代码时使用。我们希望通过测试确认其它代码依旧正常工作。
  • 性能测试,测量软件完成任务所需的时间。
  • 验收测试,考察利益相关者是否满意软件的表现,是否愿意支付费用。

单元测试是一组测试中最小的模块。编程代码中的每个类都伴随有一个单元测试类。可通过模拟方法调用将测试与其它类隔离。

集成测试更容易实现,我们会测试一个包含所有依赖项的类。测试成功的话我们就知道软件内的路径是畅通的,但如果测试失败,我们也没法判断到底是哪个类出了问题。系统测试则会检查整个系统,包括硬件操作系统、Web服务等等。

测试应该是可读的,以便非程序员阅读或改动测试内容。在敏捷团队中,程序员与测试人员和分析人员协同工作,测试和规范则属于协作内容,因此大家都应该能看懂测试,乃至在必要时改动测试内容。

TDD:重点在于设计和生产力

测试驱动开发(TDD)是一种可持续交付优秀软件的成熟技术。TDD的理念很简单:在编写生产代码之前先编写测试代码。想要加一项功能?那就先写一项测试吧。但要实践好这种看似简单的理念也需要技巧和判断力。

TDD实际上是一种设计技能。TDD的本质是利用一系列小型测试来从零开始一步步设计系统,并在系统逐渐成型的过程中快速获得价值。这种技能其实改名叫测试驱动设计更合适些。

作为一种设计方法,TDD的重点在于专注和简洁。它的目标是避免开发者编写多余的代码,有些代码是不产生交付价值的。TDD的理念是用最少的代码来解决问题。

有很多文章称赞TDD,列举出了它的众多优势;很多技术会议上也会宣传做测试的好处。这些宣传并没有夸大其辞,测试的确是非常必要的!TDD的优势真实存在:

  • 它能让你写出更好的软件。
  • 让你避免多余的劳动。
  • 当你引入新功能时,TDD能防止你搞砸一切。
  • 你的软件因此能实现自我记录。

虽说我一直很认可这些优势,我也曾有一段时间觉得就算不用TDD我也能写出优秀、可维护的软件。当然现在我知道自己错了,但为什么TDD好处这么多我还会有那种念头呢?是因为成本!

TDD的成本太高了! 诚然,不做测试的话总成本的确会更高,但那种情况下成本会分摊在不同的阶段。如果我们开始使用TDD就要立刻增加投入,相反,不用TDD的话代价在未来才会体现出来。

顺其自然完成工作是最理想的状态。人的本性很懒惰(软件开发者大概是懒惰的极致了)还贪婪,所以我们希望降低成本的手段能立刻生效——说得容易,做起来就难了!

围绕TDD有许多理论、维度和视角,但我更想谈一谈TDD的实践应用。最后,随着我们一步步分析研究,我们会发现有些事情是只有用TDD才能做到的。

这里是我的演讲:“单元测试和TDD理念与最佳实践指南”,其中包含以下主题:

  • 为什么我们需要测试,
  • 测试类型,
  • 每种类型应该如何及何时使用,
  • 测试级别简介,
  • 测试策略简介,
  • TDD的实践应用。

演讲内容还包括指导和最佳实践,介绍测试时的注意事项。

“TDD的实践应用”这部分和演讲中提到的理念是适用于所有编程语言的,而我演示时用的是Java。我们要展示的是设计和创作优秀作品时的思维过程,不是教人写代码那么简单。

分析问题

不管解决什么问题,第一步就是不管问题有多复杂,先分析问题的结构并把它拆分成许多连续而完整的的解决步骤,同时考虑输入场景和输出的内容。接下来我们检查这些步骤,从业务角度确保它们的结果和原始需求是一致的,不多也不少。这时候先不管具体的实现细节。

这是关键的一步;其中最重要的是要能识别出手头上给定问题的所有需求,以减轻之后具体实现阶段的负担。将任务拆分成许多小步骤后,我们将获得干净、易于实现、可测试的代码。

TDD是开发和维护这些步骤的关键,我们要通过TDD来覆盖手头问题的所有可能情况。

假设我们需要开发一个转换工具库,功能是将罗马数字转换为等效的阿拉伯数字。作为开发者,我要做的事情有:

  1. 创建一个库项目。
  2. 创建类。
  3. 应该会具体研究一下,开发出转换的方法。
  4. 想一想可能存在的问题场景和应对方式。
  5. 为这个任务编写一个测试用例,先把写测试的任务搞定(其实很可能写不出来什么东西),同时已经用老办法几乎做完测试工作了。

这种流程可太糟心了。

为了在编写代码时正确启动流程并将TDD付诸实践,请遵照下面这些实践步骤行事,这样就能成功做好一个项目了;同时你还会得到一套测试用例,减轻未来开发工作的时间和成本负担。

这个示例的代码可以从我的GitHub库克隆下来。启动终端,指向自己喜欢的位置,然后运行以下命令:

$ git clone https://github.com/mohamed-taman/TDD.git

我已经做好了设置,项目的每次TTD红色/绿色/蓝色改动都会提交一次,因此在查看提交历史时我们就能发现哪些改动和重构是符合最终项目需求的方向了。

我用的是Maven构建工具、Java SE 12和JUnit 5。

TDD的实践应用

为了开发上文所说的转换器,我们要做的第一步是写一个测试用例,将罗马数字I转换为阿拉伯数字1。

这里需要创建转换器类和方法实现,以使测试用例满足我们的第一个需求。

等等,等等!稍等一下!这里有一条实践中总结的经验,最好记住这条规则再开始干活儿:首先不要创建源代码,而是先创建测试用例中的类和方法。这叫意图编程,因为我们在命名新类和将要使用新类的新方法时,必须要考虑我们正在编写的这段代码的用途和用法,这样当然就会设计出更出色、更干净的API了。

第1步

首先创建一个测试包,还有类、方法和测试实现:

包:rs.com.tm.siriusxi.tdd.roman
类:RomanConverterTest
方法:convertI()
实现:assertEquals(1, new RomanConverter().convertRomanToArabicNumber(“I”));

第2步

这里没有失败的测试用例:这是一个编译错误。所以先用IDE提示在Java源文件夹中创建包、类和方法。

包:rs.com.tm.siriusxi.tdd.roman
类:RomanConverter
方法:public int convertRomanToArabicNumber(String roman)

第3步(红色状态)

我们需要确认我们指定的类和方法正确无误,测试用例也能正常工作。

通过实现convertRomanToArabicNumber方法来抛出IllegalArgumentException,我们应该就能达到红色状态了。

public int convertRomanToArabicNumber(String roman) {
        throw new IllegalArgumentException();
 }

然后运行测试用例。我们应该能看到红色指示条。

第4步(绿色状态)

这一步我们需要再次运行测试用例,这次要看到绿色指示条。我们要用最少的代码量满足测试用例转绿的要求。所以方法应该返回1这个值。

public int convertRomanToArabicNumber(String roman) {
        return 1;
 }

运行测试用例。我们应该能看到绿条了。

第5步(重构阶段)

现在该做重构了(如果有能重构的代码的话)。需要强调的是不仅生产代码要重构,测试代码也得重构。

从测试类中删除未使用的导入。

再次运行测试用例,这次应该看到蓝条——哈哈哈,开个玩笑,哪来的蓝条。如果重构代码后一切正常,我们应该能再次看到绿条。

删除未使用的代码是常用的简单重构方法,可以提高代码的可读性,减少类的占用空间,从而优化项目的存储需求。

第6步

这一步开始,我们的流程就会统一成红到绿到蓝的顺序。我们先写一个问题的新需求或新步骤作为失败的测试用例来达成TDD的第一步,也就是红色状态,然后重复这一过程,直到我们完成整个功能。

注意,我们要从基本的需求或步骤起步,然后一步步走下去,直到我们完成所需的功能。这样我们的路线就很清楚了,是由简单到复杂的顺序。

这样的流程是最好的,不要花费大量时间从一开始就考虑整体实现,因为这可能会让我们想得太多,甚至搞出来一些不必要的功能。这样做会导致过度编程,效率自然低下。

下一步工作是将罗马数字II转换为阿拉伯数字2。

在同一个测试类中,我们创建了一个新的测试方法及其实现,如下所示:

方法:convertII()

当我们运行测试用例时会看到红条,因为convertII()方法测试失败了。convertI()方法还是绿色状态,这样就很好,说明其它代码没有受新代码影响。

第7步

现在我们需要让测试用例跑出绿色状态。我们来实现一种可以同时满足两种情况的方法。我们可以用简单的if/else if/else检查来处理这两种情况,在else情况下我们会抛出IllegalArgumentException。

public int convertRomanToArabicNumber(String roman) {
        if (roman.equals("I")) {
            return 1;
        } else if (roman.equals("II")) {
            return 2;
        }
        throw new IllegalArgumentException();
    }

这里要避免的一个问题是某行代码(例如roman.equals(“I”))导致的空指针异常。要修复这个问题只需将相等情况转换为”I”.equals(roman)。

再次运行测试用例,所有情况都应该是绿条了。

第8步

现在我们可以设法重构案例了,闻一闻哪些代码“味道不香”。重构时,我们通常会找出下面类型的代码做调整:

  • 方法很长,
  • 重复代码,
  • 很多if/else代码,
  • switch-case语句,
  • 需要简化的逻辑
  • 设计问题。

这个示例中的代码问题(你找到了吗)是if/else语句和返回太多。

也许我们应该引入一个sum变量,并使用for循环来遍历罗马字符的字符数组。如果一个字符是I,它将对sum加1,然后return变量sum。

但我喜欢防御式编程,所以我会将throw子句移动到if语句的一个else语句中,以便覆盖所有无效字符。

public int convertRomanToArabicNumber(String roman) {
        int sum = 0;
        for (char ch : roman.toCharArray()) {
            if (ch == 'I') {
                sum += 1;
            } else {
                throw new IllegalArgumentException();
            }
        }  
        return 0;
    }

在Java 10及更高版本中,我们可以使用var来定义变量,写var sum = 0;代替int sum = 0;。

然后再次运行测试以确保我们的重构不会影响任何程序功能。

糟糕——我们看到了一个红条。哦!原来所有测试用例都返回0,我们的错误是没返回sum却返回了0。

解决这个问题后就能看到美丽的绿条了。

这说明无论是多小的变动,都需要在添加改动后运行测试。重构时很容易引入错误,而测试用例就是我们的辅助工具;运行测试就能找出错误。这就是回归测试的力量。

再看一下代码,还有另一种问题(你看到了吗?)。这里的异常不是描述性的,因此我们必须提供有意义的错误

message of Illegal roman character %s, ch.

throw new IllegalArgumentException(String.format("Illegal roman character %s", ch));

再次运行测试用例,所有情况都应该是绿条。

第9步

我们添加另一个测试用例,将罗马数字III转换为3。

在同一个测试类中,我们创建一个新的测试方法及其实现:

方法:convertIII()

再次运行测试用例,全绿。我们的实现能覆盖这种情况。

第10步

现在我们需要将罗马数字V转换为5。

在同一个测试类中,我们创建一个新的测试方法及其实现:

方法:convertV()

运行测试用例会看到红条,convertV()失败了,其它部分还是绿色。

将else/if添加到主if语句来实现这个方法,并加入检查,如果char = 'v’则sum+=5;。

for (char ch : roman.toCharArray()) {
            if (ch == 'I') {
                sum += 1;
            } else if (ch == 'V') {
                sum += 5;
            } else {
                throw new IllegalArgumentException(String.format("Illegal roman character %s", ch));
  } }

这里我们可以重构,但不要在这一步做。在实现阶段,我们唯一的目标是让测试通过,看到一个绿条。现在我们不关心简化设计、重构或代码优化这些事情。当代码通过测试后,我们可以回来再做重构。

在重构状态下,我们只关心重构工作。一次只关注一件事以避免分心、提高效率。

测试用例应该是绿色状态。

我们有时需要一串if/else语句;为了优化它,可以按用例访问频率来对if语句测试用例排序。如果可以的话,改为switch-case语句来跳过测试就更好了。

第11步

现在我们处于绿色状态,轮到重构阶段了。再来看方法,我们可以改动一些让人讨厌的if/else。

也许可以不用if/else,我们可以引入查询表并将罗马字符存储为键,将对应的阿拉伯数字存储为值。

那就删除if语句并替换成sum += symbols.get(chr);。右击气泡,然后点击引入实例变量。

private final Hashtable<Character, Integer> romanSymbols = new Hashtable<Character, Integer>() {
        {
            put('I', 1);
            put('V', 5);
        }
    };

我们需要像之前一样检查无效符号,因此我们让代码确定romanSymbols是否包含特定键,如果不包含就抛出异常。

public int convertRomanToArabicNumber(String roman) {
        int sum = 0;
        for (char ch : roman.toCharArray()) {
            if (romanSymbols.containsKey(ch)) {
                sum += romanSymbols.get(ch);
            } else {
                throw new IllegalArgumentException(
                        String.format("Illegal roman character %s", ch));
            }
        }
        return sum;
    }

运行测试用例,应该是绿色状态。

这是另一种代码问题,优化它是为了更好的设计和性能,让代码更干净。最好使用HashMap而不是Hashtable,因为与后者相比前者的实现是不同步的。对这种方法大量调用会损害性能。

一个设计思路是始终使用通用接口作为目标类型,因为这能带来更容易维护,更干净的代码,且在不影响代码使用的情况下轻松调整实现细节。这个示例中我们将使用Map。

private static Map<Character, Integer> romanSymbols = new HashMap<Character, Integer>() {
        private static final long serialVersionUID = 1L;
        {
            put('I', 1);
            put('V', 5);
        }
    };

如果你用的是Java 9或更高版本,则可以使用新的HashMap<>()替换新的HashMap <Character,Integer>(),因为菱形运算符可以与Java 9中的匿名内部类一起使用。

或者你可以使用更简单的Map.of()。

Map<Character, Integer> romanSymbols = Map.of('I', 1, 'V', 5,'X', 10, 'L', 50,'C', 100, 'D', 500,'M', 1000);

java.util.Vector和java.util.Hashtable已过时。虽然它们仍然受支持,但这些类已被JDK 1.2集合类淘汰,新的应用不应该继续用它们了。

重构之后我们需要检查一切是否正常,确认没有破坏任何东西。太棒了,是绿条!

第12步

这次找一些更有意思的数字做转换。我们回到我们的测试类,实现将罗马数字VI转换为6。

方法:convertVI()

我们运行测试用例,正常通过。看来我们编写逻辑能自动覆盖这种情况。这样就不用单独写实现了。

第13步

现在我们需要将IV转换为4,这次可能就没有VI转成6那么顺利了。

方法:convertIV()

运行测试用例,果然出来的是红条。

我们得设法让它跑通了。众所周知,在罗马数字中较小数字的字符(例如I)如果附加到较大数字的字符(例如V)前面,相当于用大数减去小数——所以IV等于4,反过来VI等于6。

我们现有的代码都是对数值求和运算的,但这一次我们需要做减法。我们应该做一个条件判定:如果前一个字符的值大于或等于后面字符的值,那么就求和,反之就做减法。

先专心编写满足问题需要的逻辑,同时不考虑变量的声明是很方便的做法。只要写好逻辑,然后创建实现所需的变量就行了。如前所述,这就是意图编程。这样一来,我们就能更快地写出最简洁的代码,不用事事都提前操心了——这才是最棒的理念。

我们目前的实现是:

public int convertRomanToArabicNumber (String roman) {
        roman = roman.toUpperCase();
        int sum = 0;
        for (char chr : roman.toCharArray()) {
            if (romanSymbols.containsKey(chr))
                sum += romanSymbols.get(chr);
            else
                 throw new IllegalArgumentException(
                        String.format("Invalid roman character %s ", chr));
        }
        return sum;
    }

为了检查罗马字符有效性,我们开始写新的逻辑。先编写一般性的逻辑,然后在IDE提示的帮助下创建一个局部变量。另外我们对变量的类型有一种感觉:它们要么是方法的本地变量,要么是实例/类变量。

int sum = 0, current = 0, previous = 0;
        for (char chr : roman.toCharArray()) {
            if (romanSymbols.containsKey(chr)) {
                if (previous >= current) {
                    sum += current;
                } else {
                    sum -= previous;
                    sum += (current - previous);
                }
            } else {
                throw new IllegalArgumentException(
                        String.format("Invalid roman character %s ", chr));
            }

现在我们需要将for循环更改为基于索引以访问当前和先前的变量,因此我们调整实现以满足新改动的要求,以便正常编译。

for (int index = 0; index < roman.length(); index++) {
            if (romanSymbols.containsKey(roman.charAt(index))) {
                  current = romanSymbols.get(roman.charAt(index));
                  previous = index == 0 ? 0 : romanSymbols.get(roman.charAt(index-1));
                if (previous >= current) {
                    sum += current;
                } else {
                    sum -= previous;
                    sum += (current - previous);
                }} else {
                throw new IllegalArgumentException(
                        String.format("Invalid roman character %s ", roman.charAt(index)));
            }

添加这个新功能后我们运行测试用例,绿条——很完美。

第14步

现在我们是绿色状态,该做重构了。我们将尝试做一个更有趣的重构。

我们的重构策略是尽量简化代码。观察发现romanSymbols.get(roman.charAt(index))这行出现了两次。

那就把重复的代码提取到这里要使用的方法或类中,让将来的所有改动都集中在一起。

高亮代码,右键单击并选择NetBeans重构工具>introduce>方法。将其命名为getSymbolValue并保留为私有方法,然后点“确定”。

现在需要运行测试用例,看看这个小型重构有没有引入错误。结果代码没有出问题,我们发现它仍处于绿色状态。

第15步

我们将做更多重构。声明条件romanSymbols.containsKey(roman.charAt(index))很难阅读,并且很难搞清它应该测试什么来传递if语句。那就简化代码,使其更具可读性。

虽然我们现在理解这行代码的作用,但我保证半年后我们就很难理解它要做什么了。

可读性是我们应该通过TDD不断提高的一项代码质量关键指标,因为在敏捷环境中我们经常要快速改动代码——为此代码必须有很好的可读性。另外任何改动都应该是可测试的。

现在把这行代码提取到一个方法中,该方法的名称为doesSymbolsContainsRomanCharacter,名字就描述了它的作用。我们还是用NetBeans重构工具完成这项工作。

这样做可以改善代码的可读性。这里的条件是:如果符号包含罗马字符,则执行逻辑,否则抛出无效字符的非法参数异常。

我们再次重新运行所有测试,没出现新的错误。

请注意,不管引入多小的重构改动,都要跑一遍测试。我不会等做完所有的重构之后才运行所有测试用例。这在TDD中是非常重要的。我们需要即时反馈,运行测试用例就是我们的反馈循环。它让我们尽早找出每个小步骤的错误,而不是对着一大串步骤的出错信息发呆。

因为每个重构步骤都可能引入新的错误,所以代码改动和代码测试之间的间隔时间越短,我们就能越快地分析代码并修复新错误。

如果我们重构了一百行代码然后运行测试用例结果不成功,就必须花时间调试以准确检测出问题所在。在一百行代码中找错误要比五行或十行代码困难得多。

第16步

我们在引入的两个私有方法和异常消息中有重复的代码,重复的这行是roman.charAt(index),因此我们使用NetBeans将其重构为一个新方法,名为getCharValue(String roman, int index)。

重新运行所有测试,全部绿条。

第17步

现在做一些重构来优化代码并提高性能。我们可以简化转换方法的计算逻辑。目前的逻辑是:

int convertRomanToArabicNumber(String roman) {
        roman = roman.toUpperCase();
        int sum = 0, current = 0, previous = 0;
        for (int index = 0; index < roman.length(); index++) {
            if (doesSymbolsContainsRomanCharacter(roman, index)) {
                current = getSymboleValue(roman, index);
                previous = index == 0 ? 0 : getSymboleValue(roman, index - 1);
                if (previous >= current) {
                    sum += current;
                } else {
                    sum -= previous;
                    sum += (current - previous);
                }
            } else {
                throw new IllegalArgumentException(
                        String.format("Invalid roman character %s ",
                                getCharValue(roman, index)));
            }
        }
        return sum;
    }

改进下面这一行可以节省几个多余的CPU周期:

previous = index == 0 ? 0 : getSymboleValue(roman, index - 1);

不必获取前一个字符,因为前一个字符只是for循环结束时的当前字符。可以删除这一行并将其替换为previous = current;,放在else条件末尾的计算后面。

运行测试用例,结果应该是绿色。

现在我们简化计算以节省另外几个多余的计算周期。我会还原if语句测试用例的计算,并反转for循环。最终的代码应该是:

for (int index = roman.length() - 1; index >= 0; index--) {
            if (doesSymbolsContainsRomanCharacter(roman, index)) {
                current = getSymboleValue(roman, index);
                 if (current < previous) {
                    sum -= current;
                } else {
                    sum += current;
                }
                previous = current;
            } else {
                throw new IllegalArgumentException(
                        String.format("Invalid roman character %s ",
                                getCharValue(roman, index)));
            }

运行测试用例,应该还是绿色。

由于该方法不会更改任何对象状态,因此可以将其设置为静态方法。此外该类是一个工具类,因此应该关闭它以便继承。虽然所有方法都是静态的,但我们不应该允许实例化类。添加一个私有默认构造函数来修复此问题,并将该类标记为final。

现在我们会在测试类中出现编译错误。一旦解决了这个问题,运行测试用例应该会再次全绿。

第18步

最后一步是添加更多测试用例,以确保我们的代码涵盖所有需求。

  1. 添加一个convertX()测试用例,它应该返回10,因为罗马数字X=10。这时运行测试会失败并返回IllegalArgumentException,所以将X=10添加到符号映射里。再次运行测试就通过了。这里没有重构内容。
  2. 添加convertIX()测试用例,它应该返回9,因为IX=9。测试应该会通过。
  3. 在符号映射中加入这些值:L = 50,C = 100,D = 500,M = 1000。
  4. 添加一个convertXXXVI()测试用例,它应该返回36,因为XXXVI=36。运行测试会正常通过。这里没有重构。
  5. 添加一个convertMMXII()测试用例,它应该返回2012。运行测试将通过。这里没有重构。
  6. 添加convertMCMXCVI()测试用例,它应该返回1996。运行测试将通过。这里没有重构。
  7. 添加一个convertInvalidRomanValue()测试用例,它应该抛出IllegalArgumentException。运行测试将通过。这里没有重构。
  8. 添加一个convertVII()测试用例,它应该返回7,因为VII=7。但我们用小写的vii测试时,测试将失败并抛出IllegalArgumentException,因为我们只处理了大写字母。为了解决这个问题,我们在方法开头添加一行roman = roman.toUpperCase();。再次运行测试用例将通过。这里没有重构。

到这一步我们已经完成了任务(实现)。基于TDD的理念,我们用最少的代码改动通过了所有测试用例,并通过重构满足了所有需求,从而确保我们具有出色的代码质量(性能、可读性和设计)。

我希望大家像我一样享受这个过程,希望这能鼓励你在下一个项目甚至现在做的任务中开始使用TDD。喜欢本文的话请点击分享、喜欢,并在GitHub中点星来帮我传播吧。

作者介绍

Mohamed Taman是Comtrade数字服务公司的高级企业架构师、Java冠军、甲骨文开拓大使。他是JCP成员,曾是JCP执行委员会成员,JSR 354、363、373专家组成员、EGJUG领导者、甲骨文埃及建筑师俱乐部董事会成员。他主讲Java,热爱移动、大数据、云、区块链、DevOps。他是国际演讲者,书籍和视频“JavaFX essentials”“清洁代码入门——Java SE 9”“动手实践Java 10编程与JShell”的作者,还出了一本新书“Java冠军的秘密”。他赢得2014、2015年杜克选择奖项和JCP杰出参与者2013年奖项。

查看英文原文Test-Driven Development: Really, It’s a Design Technique

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章