测试驱动开发(TDD)

测试驱动开发(TDD)是一种软件开发过程,它依赖于非常短的开发周期的重复:首先,开发人员编写一个(最初失败的)自动化测试用例,该用例定义了所需的改进或新功能,然后产生最小量代码以通过该测试,并最终将新代码重构为可接受的标准。

通常遵循以下步骤顺序:

  • 添加测试
  • 运行所有测试,看看新测试是否失败
  • 写一些代码
  • 运行测试
  • 重构代码
  • 重复

在TDD上有很多文章,而Wikipedia始终是一个好的开始。本文将重点介绍使用Roy Osherove Katas之一的变体进行的实际测试和实现。

在下面,您将找到与每个需求以及随后的实际实现相关的测试代码。尝试仅阅读一个需求,自己编写测试和实现,并将其与本文的结果进行比较。请记住,有许多不同的方式来编写测试和实现。本文只是众多可能的解决方案之一。

开始吧!

要求

  • 使用int Add(string numbers)方法创建一个简单的String计算器
  • 该方法可以接受0、1或2个数字,并将返回它们的总和(对于空字符串,它将返回0),例如“”,“ 1”或“ 1,2”
  • 允许Add方法处理未知数量的数字
  • 允许Add方法处理数字之间的新行(而不是逗号)。
  • 可以输入以下内容:“ 1 \ n2,3”(等于6)
  • 支持不同的定界符
  • 要更改定界符,字符串的开头将包含一个单独的行,如下所示:“ // [delimiter] \ n [numbers…]”,例如“ //; \ n1; 2”应返回三个默认值分隔符为“;” 。
  • 第一行是可选的。所有现有方案仍应受到支持
  • 用负数调用Add将引发异常“不允许使用负数”以及传递的负数。如果存在多个否定词,请在所有异常消息中显示所有否定词(如果您是初学者,请在此处停止)。
  • 大于1000的数字应忽略,因此加2 + 1001 = 2
  • 分隔符可以是任何长度,格式如下:“ // [delimiter] \ n”:“ // [-] \ n1-2-3”应返回6
  • 允许这样的多个定界符:“ // [delim1] [delim2] \ n”,例如“ // [-] [%] \ n1-2%3”应返回6。
  • 确保您还可以处理长度超过一个字符的多个定界符

即使这是一个非常简单的程序,仅查看那些要求也可能会令人不知所措。让我们采取另一种方法。忘记您刚刚阅读的内容,让我们一一满足您的要求。

创建一个简单的字符串计算器

要求1:该方法可以采用0、1或2个数字,并用逗号(,)分隔。

让我们编写第一组测试。

JAVA测试

package com.wordpress.technologyconversations.tddtest;
 
import org.junit.Test;
import com.wordpress.technologyconversations.tdd.StringCalculator;
 
public class StringCalculatorTest {
    @Test(expected = RuntimeException.class)
    public final void whenMoreThan2NumbersAreUsedThenExceptionIsThrown() {
        StringCalculator.add("1,2,3");
    }
    @Test
    public final void when2NumbersAreUsedThenNoExceptionIsThrown() {
        StringCalculator.add("1,2");
        Assert.assertTrue(true);
    }
    @Test(expected = RuntimeException.class)
    public final void whenNonNumberIsUsedThenExceptionIsThrown() {
        StringCalculator.add("1,X");
    }
}

最好以易于理解所测试内容的方式来命名测试方法。我更喜欢使用[操作]然后[验证]的BDD变体。在这种情况下,一种测试方法的名称是whenMoreThan2NumbersAreUsedThenExceptionIsThrown。我们的第一组测试验证了最多两个数字可以传递给计算器的add方法。如果有两个以上,或者其中一个不是数字,则应引发异常。在@Test批注中放入“ expected”会告诉JUnit运行器预期的结果是抛出指定的异常。为了简洁起见,从此处开始,将仅显示代码的修改部分。可以将整个代码分为需求,可以从GitHub存储库中获取(测试和实施)。

JAVA实现

public class StringCalculator {
    public static final void add(final String numbers) {
        String[] numbersArray = numbers.split(",");
        if (numbersArray.length > 2) {
            throw new RuntimeException("Up to 2 numbers separated by comma (,) are allowed");
        } else {
            for (String number : numbersArray) {
                Integer.parseInt(number); // If it is not a number, parseInt will throw an exception
            }
        }
    }
}

请记住,TDD背后的想法是做必要的最少工作,以使测试通过并重复该过程,直到实现整个功能为止。目前,我们仅对确保“该方法可以接受0、1或2个数字”。再次运行所有测试,然后查看它们是否通过。

要求2:对于空字符串,该方法将返回0

JAVA测试

@Test
public final void whenEmptyStringIsUsedThenReturnValueIs0() {
    Assert.assertEquals(0, StringCalculator.add(""));
}

JAVA实现

public static final int add(final String numbers) { // Changed void to int
    String[] numbersArray = numbers.split(",");
    if (numbersArray.length > 2) {
        throw new RuntimeException("Up to 2 numbers separated by comma (,) are allowed");
    } else {
        for (String number : numbersArray) {
            if (!number.isEmpty()) {
                Integer.parseInt(number);
            }
        }
    }
    return 0; // Added return
}

要使此测试通过,要做的就是将return方法从void更改为int并以返回0结束。

要求3:方法将返回它们的数字总和

JAVA测试

@Test
public final void whenOneNumberIsUsedThenReturnValueIsThatSameNumber() {
    Assert.assertEquals(3, StringCalculator.add("3"));
}
 
@Test
public final void whenTwoNumbersAreUsedThenReturnValueIsTheirSum() {
    Assert.assertEquals(3+6, StringCalculator.add("3,6"));
}

JAVA实现

public static int add(final String numbers) {
    int returnValue = 0;
    String[] numbersArray = numbers.split(",");
    if (numbersArray.length > 2) {
        throw new RuntimeException("Up to 2 numbers separated by comma (,) are allowed");
    }
    for (String number : numbersArray) {
        if (!number.trim().isEmpty()) { // After refactoring
            returnValue += Integer.parseInt(number);
        }
    }
    return returnValue;
}

在这里,我们在所有数字中添加了迭代以创建总和。

要求4:允许Add方法处理未知数量的数字

JAVA测试

//  @Test(expected = RuntimeException.class)
//  public final void whenMoreThan2NumbersAreUsedThenExceptionIsThrown() {
//      StringCalculator.add("1,2,3");
//  }
    @Test
    public final void whenAnyNumberOfNumbersIsUsedThenReturnValuesAreTheirSums() {
        Assert.assertEquals(3+6+15+18+46+33, StringCalculator.add("3,6,15,18,46,33"));
    }

JAVA实现

public static int add(final String numbers) {
    int returnValue = 0;
    String[] numbersArray = numbers.split(",");
    // Removed after exception
    // if (numbersArray.length > 2) {
    // throw new RuntimeException("Up to 2 numbers separated by comma (,) are allowed");
    // }
    for (String number : numbersArray) {
        if (!number.trim().isEmpty()) { // After refactoring
            returnValue += Integer.parseInt(number);
        }
    }
    return returnValue;
}

要实现此要求,我们要做的就是删除部分代码(如果有两个以上的数字,则会引发异常)。但是,一旦执行测试,第一个测试就会失败。为了满足此要求,需要删除whenMoreThan2NumbersAreUsedThenExceptionIsThrown时的测试。

要求5:允许Add方法处理数字之间的新行(而不是逗号)。

JAVA测试

@Test
public final void whenNewLineIsUsedBetweenNumbersThenReturnValuesAreTheirSums() {
    Assert.assertEquals(3+6+15, StringCalculator.add("3,6n15"));
}

JAVA实现

public static int add(final String numbers) {
    int returnValue = 0;
    String[] numbersArray = numbers.split(",|n"); // Added |n to the split regex
    for (String number : numbersArray) {
        if (!number.trim().isEmpty()) {
            returnValue += Integer.parseInt(number.trim());
        }
    }
    return returnValue;
}

我们要做的就是通过添加| \ n来扩展split regex。

要求6:支持不同的定界符

要更改定界符,字符串的开头将包含一个单独的行,如下所示:“ // [delimiter] \ n [numbers…]”,例如“ //; \ n1; 2”应采用1和2作为参数并返回3,默认分隔符为';' 。

JAVA测试

@Test
public final void whenDelimiterIsSpecifiedThenItIsUsedToSeparateNumbers() {
    Assert.assertEquals(3+6+15, StringCalculator.add("//;n3;6;15"));
}

JAVA实现

public static int add(final String numbers) {
    String delimiter = ",|n";
    String numbersWithoutDelimiter = numbers;
    if (numbers.startsWith("//")) {
        int delimiterIndex = numbers.indexOf("//") + 2;
        delimiter = numbers.substring(delimiterIndex, delimiterIndex + 1);
        numbersWithoutDelimiter = numbers.substring(numbers.indexOf("n") + 1);
    }
    return add(numbersWithoutDelimiter, delimiter);
}
 
private static int add(final String numbers, final String delimiter) {
    int returnValue = 0;
    String[] numbersArray = numbers.split(delimiter);
    for (String number : numbersArray) {
        if (!number.trim().isEmpty()) {
            returnValue += Integer.parseInt(number.trim());
        }
    }
    return returnValue;
}

这次有很多重构。我们将代码分为2种方法。初始方法解析输入以查找定界符,随后调用执行实际总和的新输入。由于我们已经具有涵盖所有现有功能的测试,因此可以安全地进行重构。如果有任何问题,则其中一项测试将发现问题。

要求7:负数将引发异常

用负数调用Add将引发异常“不允许使用负数”以及传递的负数。如果存在多个否定词,请在异常消息中显示所有否定词。

JAVA测试

@Test(expected = RuntimeException.class)
public final void whenNegativeNumberIsUsedThenRuntimeExceptionIsThrown() {
    StringCalculator.add("3,-6,15,18,46,33");
}
@Test
public final void whenNegativeNumbersAreUsedThenRuntimeExceptionIsThrown() {
    RuntimeException exception = null;
    try {
        StringCalculator.add("3,-6,15,-18,46,33");
    } catch (RuntimeException e) {
        exception = e;
    }
    Assert.assertNotNull(exception);
    Assert.assertEquals("Negatives not allowed: [-6, -18]", exception.getMessage());
}

有两个新测试。第一个检查是否存在负数时是否引发异常。第二个验证异常消息是否正确。

JAVA实现

private static int add(final String numbers, final String delimiter) {
    int returnValue = 0;
    String[] numbersArray = numbers.split(delimiter);
    List negativeNumbers = new ArrayList();
    for (String number : numbersArray) {
        if (!number.trim().isEmpty()) {
            int numberInt = Integer.parseInt(number.trim());
            if (numberInt < 0) {
                negativeNumbers.add(numberInt);
            }
            returnValue += numberInt;
        }
    }
    if (negativeNumbers.size() > 0) {
        throw new RuntimeException("Negatives not allowed: " + negativeNumbers.toString());
    }
    return returnValue;     
}

添加了此时间代码,该代码收集列表中的负数,如果有则抛出异常。

要求8:大于1000的数字应被忽略

示例:加2 + 1001 = 2

JAVA测试

@Test
public final void whenOneOrMoreNumbersAreGreaterThan1000IsUsedThenItIsNotIncludedInSum() {
    Assert.assertEquals(3+1000+6, StringCalculator8.add("3,1000,1001,6,1234"));
}

JAVA实现

private static int add(final String numbers, final String delimiter) {
        int returnValue = 0;
        String[] numbersArray = numbers.split(delimiter);
        List negativeNumbers = new ArrayList();
        for (String number : numbersArray) {
                if (!number.trim().isEmpty()) {
                        int numberInt = Integer.parseInt(number.trim());
                        if (numberInt < 0) {
                                negativeNumbers.add(numberInt);
                        } else if (numberInt <= 1000) {
                                returnValue += numberInt;
                        }
                }
        }
        if (negativeNumbers.size() > 0) {
                throw new RuntimeException("Negatives not allowed: " + negativeNumbers.toString());
        }
        return returnValue;                
}

这个很简单。我们移动了“ returnValue + = numberInt;” 在“ else if(numberInt <= 1000)”中。

还有3个需求。我鼓励您自己尝试。

要求9:定界符可以是任何长度

应使用以下格式:“ // [定界符] \ n”。示例:“ // [—] \ n1—2-3”应返回6

要求10:允许使用多个定界符

应该使用以下格式:“ // [delim1] [delim2] \ n”。示例“ // [-] [%] \ n1-2%3”应返回6。

要求11:确保您还可以处理长度超过一个字符的多个定界符

给TDD一个机会

对于TDD初学者来说,整个过程通常看起来不堪重负。常见的抱怨之一是TDD拖慢了开发过程。确实,起初要花时间才能加快速度。但是,在使用TDD流程进行一些实践开发之后,可以节省时间,进行更好的设计,可以轻松安全地进行重构,提高质量和测试覆盖率,并且最后但并非最不重要的一点是确保始终对软件进行测试。TDD的另一个巨大好处是,测试可以作为活动文档。查看测试即可知道每个软件单元应该做什么。只要所有测试通过,该文档就始终是最新的。用TDD进行的单元测试应为大多数代码提供“代码覆盖率”,并且应与验收测试驱动开发(ATDD)或行为驱动开发(BDD)。它们一起涵盖了单元测试和功能测试,并提供了完整的文档和要求。

TDD使您专注于您的任务,准确地编写所需的代码,从外部进行思考,最终成为一个更好的程序员。

参考

Technology Conversations

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,110评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,443评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,474评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,881评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,902评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,698评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,418评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,332评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,796评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,968评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,110评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,792评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,455评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,003评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,130评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,348评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,047评论 2 355

推荐阅读更多精彩内容