跳转到主要内容
Chinese, Simplified

关键要点

 

  1. 人们普遍认为,我们创建的任何软件都必须经过测试,以确保其满足功能要求,但我们还必须测试非功能性需求,如安全性,可用性和 - 关键性 - 可维护性。
  2. 测试驱动开发(TDD)是一种成熟的技术,可以随着时间的推移更快速,更可持续地提供更好的软件。
  3. TDD基于一个简单的想法:在编写生产代码之前编写失败的测试。然而,这种“简单”的想法需要技巧和判断才能做得好。
  4. TDD实际上是一种设计技术。 TDD的基础重点是使用小型测试以紧急方式从头开始设计系统,并在建立系统信心的同时快速获得价值。这种技术的更好名称可能真的是测试驱动设计。
  5. 通常,为特定问题开发解决方案的第一步,无论其复杂程度如何,都要分析它,然后将其分解为更小的组件。这些组件可以通过一系列步骤实现,其中输入场景与下一步的输出应该一起考虑。

我们需要进行软件测试,以确保软件满足要求,正确响应输入(输入验证),在可接受的时间内执行(性能测试),用户可以安装和运行它(部署测试),以及它符合利益相关者的目标。这些目标可能是业务结果或功能,如安全性,可用性,可维护性和其他类型的功能。

测试类型包括:

  1. 烟雾和健康测试检查软件是否以基本形式运行。
  2. 连续测试在每次迭代中运行,例如我们运行Maven时。
  3. 当我们添加新的编程代码或更改现有代码时,我们使用回归测试。我们希望确保其他代码继续有效。
  4. 性能测试测量运行软件所需的时间。
  5. 验收测试检查利益相关者是否对软件感到满意并且他们是否愿意支付账单。

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

集成测试更容易实现。我们将测试一个包含所有依赖项的类。在这里,我们可以确保通过软件的路径正常工作,但是当它失败时,我们不知道哪个类失败了。系统测试检查整个系统,包括硬件操作系统,Web服务等。

测试应该是可读的,因为非程序员应该能够阅读或更改测试。在敏捷团队中,程序员与测试人员和分析人员一起工作,测试和规范是共同点,因此每个人都应该能够阅读测试,甚至在必要时更改测试。

TDD:关于设计和生产力


测试驱动开发(TDD)是一种可持续交付更好软件的成熟技术。 TDD基于一个简单的想法:在编写生产代码之前编写失败的测试。需要新的行为?写一个失败的测试。然而,这个看似简单的想法需要技巧和判断才能做好。

TDD实际上是一种设计技术。 TDD的基础是使用小型测试以紧急方式自下而上设计,并在建立系统信心的同时快速达到某种价值。更好的名称可能是测试驱动的设计。

作为一种设计方法,它的重点和简洁性。目标是防止开发人员编写多余的代码,这些代码对于交付价值是不必要的。它是关于编写解决问题所需的最少量代码。

许多文章都夸耀了TDD的所有优点,许多技术会议的谈话告诉我们做测试以及它们做得多酷。他们是对的(不一定是关于酷的部分,但关于有用的部分)。测试是必须的! TDD典型列出的优势是真实的:

  1. 你写的软件更好。
  2. 你避免过度工程。
  3. 在引入新功能时,您可以防止破坏世界。
  4. 您的软件是自我记录的。

即使我总是同意这些优点,也有一段时间我认为我不需要TDD来编写好的和可维护的软件。当然,现在,我知道我错了,但为什么我有这个想法尽管闪亮的神奇好处?成本!

TDD成本很高!如果我们不进行测试,任何人都认为成本更高是正确的,但这些成本来自不同的时间。如果我们做TDD,我们会立即付费。如果我们不做TDD,我们的成本将在未来出现。

完成任务的最有效方法是尽可能自然地完成。人们的本性是懒惰(软件开发人员可能是这方面表现最好的)并且贪婪,所以我们必须找到一种降低成本的方法。这很容易说,但很难做到!

围绕TDD有许多理论,维度和观点,但我更倾向于展示我们如何在实践中进行TDD。最后,我们将从我们开始和前进的位置看到我们将要拥有的最后一件艺术品,这将仅通过使用TDD来实现。

以下是我的演示文稿“单元测试和TDD概念与最佳实践指南”的链接,其中包含以下主题:

  1. 为什么我们需要测试,
  2. 测试类型,
  3. 如何以及何时使用每种类型,
  4. 按测试级别覆盖,
  5. 测试策略,和
  6. TDD在实践中。

它还包括指导和最佳实践,以指导在测试时做什么和不做什么。

“实践中的TDD”部分和引入的概念通常适用于任何语言,但我使用Java进行演示。我们的目标是展示我们在设计和创作令人兴奋的艺术时应该如何思考,而不仅仅是编写代码。

 

分析问题


解决任何问题而不管其复杂性的第一步是分析它,然后将其分解为小的连续和完整的步骤,考虑输入场景和输出应该是什么。我们审查这些步骤,以确保我们在没有深入了解实施细节的情况下,从业务角度来看,相对于原始要求没有差距 - 不多也不少。

这是关键的一步;其中一个最重要的步骤是能够识别手头上给定问题的所有要求,以简化即将到来的实施阶段。通过这些较小的步骤,我们将拥有一个干净,易于实现,可测试的代码。

TDD是开发和维护此类步骤的关键,直到我们涵盖手头问题的所有案例。

让我们想象一下,我们被要求开发一个转换工具库,将任何罗马数字转换为其等效的阿拉伯数字。作为开发人员,我将执行以下操作:

  1. 创建一个库项目。
  2. 创建类。
  3. 可能潜入创建转换方法。
  4. 想一想可能存在的问题场景以及可以做些什么。
  5. 为这个任务编写一个测试用例,以确保我已经编写了无聊的测试用例(这往往导致不编写任何测试用例),而几乎已经像往常一样在main方法中测试了该任务。

这种发展很糟糕。

为了在开发代码时正确启动流程并将TDD付诸实践,请按照这些实际步骤成功完成最终项目,并提供一套测试案例,以保护未来开发的时间和成本。

可以从我的GitHub存储库克隆此示例的代码。启动终端,指向您喜欢的位置,然后运行以下命令:

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


我已经管理了项目,以便为每个TTD红色/绿色/蓝色更改提交,因此在导航提交时,我们可以注意到最终项目要求的更改和重构。

我正在使用Maven构建工具,Java SE 12和JUnit 5。

TDD在实践中


为了开发我们的转换器,我们的第一步是使用一个测试用例将Roman 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){
        抛出新的IllegalArgumentException();
 }
运行测试用例。我们应该看到红条。

第4步(绿色状态)


在这一步中,我们需要再次运行测试用例,但这次要看绿色条。我们将用最少量的代码实现该方法以满足测试用例的绿色。所以,该方法应该返回1。

public int convertRomanToArabicNumber(String roman){
        返回1;
 }
运行测试用例。我们应该看到绿色的酒吧。

第5步(重构状态)


现在是重构的时候了(如果有的话)。我想强调的是,重构过程不仅涉及生产代码,还涉及测试代码。

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

再次运行测试用例以查看蓝色条。呵呵呵 - 我在开玩笑,没有蓝条。如果重构后一切仍然按预期工作,我们应该再次看到绿色条。

删除未使用的代码是主要的简单重构方法之一,它可以提高代码的可读性和类的大小,从而提高项目的大小。

第6步


从这一点开始,我们将遵循从红色到绿色到蓝色的一致过程。我们通过编写新的需求或问题的一个步骤作为失败的测试用例来传递TDD的第一个点,即红色状态,然后继续直到我们完成整个功能。

请注意,我们从基本要求或步骤开始,然后一步一步地继续,直到我们完成所需的功能。这样,我们就有了明确的步骤,从最简单到复杂。

最好这样做而不是跳过去,花费大量时间一次性考虑整个实施,这可能会导致我们过度思考和覆盖我们甚至不需要的案例。这引入了过度编码。而且效率低下。

我们的下一步是将II转换为2。

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

方法:convertII()
当我们运行测试用例时,它会转到红色条,因为convertII()方法失败了。 convertI()方法保持绿色,这很好。

 

第7步


现在我们需要运行测试用例并生成一个绿色测试栏。让我们实现一种方法来满足这两种情况。我们可以使用简单的if / else if / else检查来处理这两种情况,在其他情况下,我们抛出IllegalArgumentException。

public int convertRomanToArabicNumber(String roman){
        if(roman.equals(“I”)){
            返回1;
        } else if(roman.equals(“II”)){
            返回2;
        }
        抛出新的IllegalArgumentException();
    }
这里要避免的一个问题是从一行代码(例如roman.equals(“I”))上升的空指针异常。要修复它,只需将相等转换为“I”.equals(罗马)。

再次运行测试用例,我们应该看到所有情况下的绿色栏。

第8步


现在,我们有机会寻找重构案例,这里有代码味道。在重构中,我们通常会以下列形式查找代码异味:

方法很长,
重复的代码,
很多if / else代码,
开关盒恶魔,
逻辑简化,和
设计问题。
这里的代码味道(你找到了吗?)是if / else语句和许多返回。

也许我们应该通过引入一个sum变量来重构这里,并使用for循环来遍历罗马字符的字符数组。如果一个字符是I,它将为sum加1,然后返回总和。

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

public int convertRomanToArabicNumber(String roman){
        int sum = 0;
        for(char ch:roman.toCharArray()){
            if(ch =='I'){
                sum + = 1;
            } else {
                抛出新的IllegalArgumentException();
            }
        }
        返回0;
    }
在Java 10及更高版本中,我们可以使用var来定义变量,给出var sum = 0;而不是int sum = 0;。

我们再次运行测试以确保我们的重构不会改变任何代码功能。

哎哟 - 我们看到一个红色的酒吧。哦!所有测试用例都返回0,我们错误地返回0而不是sum。

我们解决这个问题,现在看到美丽的绿色。

这表明无论改变多么微不足道,我们都需要在它之后运行测试。在重构时总是有机会引入错误,我们的帮助程序是测试用例,我们运行它来捕获错误。这显示了回归测试的力量。
再看一下代码,还有另一种气味(你看到了吗?)。这里的例外不是描述性的,因此我们必须提供有意义的错误

非法罗马字符%s,ch。的消息

抛出新的IllegalArgumentException(String.format(“非法罗马字符%s”,ch));
再次运行测试用例,我们应该看到所有情况下的绿色栏。

第9步


我们添加另一个测试用例。让我们将III转换为3。

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

方法:convertIII()

再次运行测试用例,我们看到所有情况下的绿色条。我们的实现处理这种情况。

 

第10步


现在我们需要将V转换为5。

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

方法:convertV()

运行测试用例会转到红色条,因为convertV()失败而其他是绿色。

通过将else / if添加到main if语句来实现该方法,并检查如果char ='v'则sum + = 5;。

for(char ch:roman.toCharArray()){
            if(ch =='I'){
                sum + = 1;
            } else if(ch =='V'){
                总和+ = 5;
            } else {
                抛出新的IllegalArgumentException(String.format(“非法罗马字符%s”,ch));
  }}
我们有机会在这里重构,但我们不会在这一步中采取它。在实施阶段,我们唯一的目标是让测试通过并给我们一个绿色条。我们目前没有注意简单的设计,重构或具有良好的代码。当代码通过时,我们可以回来重构。

在重构状态下,我们只专注于重构。一次只关注一件事,以避免分心变得不那么富有成效。

测试用例应该进入绿色状态。

我们有时需要一串if / else语句;为了优化它,从最常访问的案例到最少访问的案例来命令if语句测试用例。如果适用,更好的是更改为switch-case语句以避免测试案例并直接达到案例。

第11步


我们处于绿色状态,我们处于重构阶段。回到方法,我们可以做一些我不喜欢的if / else。
或许不使用if if / else,我们可以引入查找表并将罗马字符存储为键,将阿拉伯语等值存储为值。

让我们删除if语句并写入sum + = symbols.get(chr);在它的位置。右键单击灯泡,然后单击以引入实例变量。

private final Hashtable <Character,Integer> romanSymbols = new Hashtable <Character,Integer>(){
        {
            放('我',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 {
                抛出新的IllegalArgumentException(
                        String.format(“非法罗马字符%s”,ch));
            }
        }
        回报;
    }
运行测试用例,它应该进入绿色状态。

这是另一种代码味道,但对于设计,性能和干净的代码。最好使用HashMap而不是Hashtable,因为与Hashtable相比,HashMap实现是不同步的。对这种方法的大量调用会损害性能。

设计提示始终使用通用接口作为目标类型,因为这是更容易维护的更干净的代码。在不影响代码使用的情况下,可以轻松更改细节的实现。在我们的例子中,我们将使用Map。

private static Map <Character,Integer> romanSymbols = new HashMap <Character,Integer>(){
        private static final long serialVersionUID = 1L;
        {
            放('我',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步


让我们添加一些更有趣的值来转换。我们回到我们的测试类来实现将Roman VI转换为6。

方法:convertVI()

我们运行测试用例并看到它通过。看起来我们放入的逻辑允许我们自动覆盖这种情况。我们再次免费获得这个,没有实现。

 

第13步


现在我们需要将IV转换为4,它可能不会像VI到6那样顺利。

方法:convertIV()

我们运行测试用例,结果是预期的红色。

我们需要让它通过我们的测试用例。我们认识到,在罗马表示中,较小值的字符(例如I)预先附加到较大值的字符(例如V)会减少总数值 -  IV等于4而VI等于6。

我们已经构建了我们的代码以总是对值进行求和,但是为了通过测试,我们需要创建减法。我们应该有一个条件做出决定:如果前一个字符的值大于或等于当前字符的值,那么它是一个求和,否则它是减法。

编写满足问题的逻辑,而不必担心变量的声明,这是很有成效的。只需完成逻辑,然后创建满足您的实现的变量。正如我之前所描述的那样,这是按意图编程。这样,我们将始终以更快的方式引入最少的代码,而无需预先考虑所有事情 -  MVP概念。

我们目前的实施是:

public int convertRomanToArabicNumber(String roman){
        roman = roman.toUpperCase();
        int sum = 0;
        for(char chr:roman.toCharArray()){
            if(romanSymbols.containsKey(chr))
                sum + = romanSymbols.get(chr);
            其他
                 抛出新的IllegalArgumentException(
                        String.format(“无效的罗马字符%s”,chr));
        }
        回报;
    }
在检查罗马字符有效性时,我们开始新的逻辑。我们正常编写逻辑,然后在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 + =(当前 - 上一个);
                }
            } else {
                抛出新的IllegalArgumentException(
                        String.format(“无效的罗马字符%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 + =(当前 - 上一个);
                }} else {
                抛出新的IllegalArgumentException(
                        String.format(“无效的罗马字符%s”,roman.charAt(index)));
            }
现在我们在添加这个新功能后运行测试用例,然后变绿。完善。

第14步


在绿色状态下,我们已准备好进行重构。我们将尝试一个更有趣的重构。

我们的重构策略是始终寻求简化代码。我们在这里可以看到romanSymbols.get(roman.charAt(index))的行出现两次。

让我们将重复的代码提取到这里要使用的方法或类,以便将来的任何更改都集中在一个地方。

突出显示代码,右键单击并选择NetBeans重构工具> introduction>方法。将其命名为getSymbolValue并将其保留为私有方法,然后单击“确定”。

此时,我们需要运行测试用例,看看我们的小型重构没有引入任何错误,我们的代码没有破坏。我们发现它仍处于绿色状态。

第15步


我们将进行更多的重构。声明条件romanSymbols.containsKey(roman.charAt(index))很难阅读,并且要弄清楚它应该测试什么来传递if语句并不容易。让我们简化代码,使其更具可读性。

虽然我们理解现在这行代码是什么,但我保证在六个月内很难理解它正在尝试做什么。
可读性是我们应该在TDD中不断提高的核心代码质量之一,因为在敏捷中我们经常快速地更改代码 - 为了快速更改,代码必须是可读的。任何改变都应该是可测试的。

让我们将这行代码提取到一个方法中,该方法的名称为doesSymbolsContainsRomanCharacter,它描述了它的作用。我们将像以前一样在NetBeans重构工具的帮助下完成这项工作。

这样做可以使我们的代码更好。条件是:如果符号包含罗马字符,则执行逻辑,否则抛出无效字符的非法参数异常。

我们再次重新运行所有测试并找不到新的错误。

请注意,一旦我引入任何小的重构更改,我会经常运行测试。我不等到我完成所有我打算做的重构,然后再次运行所有测试用例。这在TDD中至关重要。我们需要快速反馈并运行我们的测试用例是我们的反馈循环。它允许我们尽可能早地以小步骤检测任何错误,而不是长时间检测。

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

如果我们重构了一百行代码然后运行失败的测试用例,我们必须花时间进行调试,以便准确检测出问题所在。在筛选一百行时,比查看五行或十行代码更改时更难找到引入错误的代码行。

第16步


我们在引入的两个私有方法和异常消息中有重复的代码,这个重复的行是roman.charAt(index),因此我们将使用名称为getCharValue的NetBeans将其重构为一个新方法(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 + =(当前 - 上一个);
                }
            } else {
                抛出新的IllegalArgumentException(
                        String.format(“无效的罗马字符%s”,
                                getCharValue(roman,index)));
            }
        }
        回报;
    }
通过改进这一行,我们可以节省几个无用的CPU周期:

 previous = index == 0? 0:getSymboleValue(roman,index  -  1);
我们不必获取前一个字符,因为前一个字符只是for循环结束时的当前字符。我们可以删除这一行并将其替换为previous = current;在其他条件结束后计算。

运行测试用例应该为我们带来绿色。

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

for(int index = roman.length() -  1; index> = 0; index--){
            if(doesSymbolsContainsRomanCharacter(roman,index)){
                current = getSymboleValue(roman,index);
                 if(当前<上一个){
                    sum  -  = current;
                } else {
                    sum + = current;
                }
                previous = current;
            } else {
                抛出新的IllegalArgumentException(
                        String.format(“无效的罗马字符%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();在方法的开头。再次运行测试用例,它将为绿色。这里没有重构。
  9. 在这一点上,我们完成了我们的艺术(实施)。考虑到TDD,我们需要最少的代码更改来传递所有测试用例并通过重构满足所有要求,从而确保我们具有出色的代码质量(性能,可读性和设计)。

我希望你和我一样喜欢这个,并鼓励你在下一个项目甚至手头的任务中开始使用TDD。请通过分享,喜欢文章,并在GitHub上的代码中添加星号来帮助传播这个词。

 

关于作者


Mohamed Taman是高级企业架构师@Comtrade数字服务,Java冠军,Oracle开创性大使,采用Java SE.next(),JakartaEE.next(),JCP成员,JCP执行委员会成员,JSR 354,363& 373专家组成员,EGJUG领导者,Oracle埃及建筑师俱乐部董事会成员,讲Java,热爱移动,大数据,云,区块链,DevOps。 国际演讲者,书籍和视频作者“JavaFX essentials”,“清洁代码入门,Java SE 9”和“动手实践Java 10编程与JShell”,以及一本新书“Java冠军的秘密”, 赢得Duke的2015年,2014年奖项和JCP杰出的采用参与者2013年奖项。

 

原文:https://www.infoq.com/articles/test-driven-design-java

讨论: 知识星球【数字化和智能转型】

Article
知识星球
 
微信公众号
 
视频号