原创

测试驱动开发(TDD):样例实践(翻译)

温馨提示:
本文最后更新于 2019年09月17日,已超过 1,849 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我
本文为Test Driven Development (TDD): Example Walkthrough的中文译本,如有翻译不妥的地方,请在评论区留言或直接联系站长Simon,谢谢!

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

通常遵循以下步骤顺序:

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

有很多关于TDD和维基百科的文章总是一个好的开始。本文将重点介绍使用Roy Osherove Katas之一的变体进行的实际测试和实施。在完成本文之前,请勿单击该链接。如果事先并未知道所有要求,则最好进行此练习。

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

开始吧!

需求:

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

即使这是一个非常简单的程序,只要看看这些要求就可能是压倒性的。我们采取不同的方法。忘记刚才读到的内容,让我们逐一完成要求。


创建一个简单的String计算器

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

让我们编写第一组测试。

[JAVA TEST]

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方法。如果有两个以上或者如果其中一个不是数字,则应该抛出异常。将“expected”放在@Test注释中告诉JUnit运行器,预期的结果是抛出指定的异常。从这里开始,为简洁起见,仅显示代码的修改部分。可以从GitHub存储库(测试和实现)获得分为需求的整个代码。

[JAVA IMPLEMENTATION]

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]
@Test
public final void whenEmptyStringIsUsedThenReturnValueIs0() {
Assert.assertEquals(0, StringCalculator.add(""));
}

[JAVA IMPLEMENTATION]

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
}

所有要做的测试都是将返回方法从void更改为int并以返回零结束它。

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

[JAVA TEST]

@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 IMPLEMENTATION]

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]

//  @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 IMPLEMENTATION]

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;
}

完成此要求所需要做的就是删除部分代码,如果有超过2个数字则抛出异常。但是,一旦执行测试,第一次测试失败。为了满足这个要求,需要删除测试whenMoreThan2NumbersAreUsedThenExceptionIsThrown时。

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

[JAVA TEST]

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

[JAVA IMPLEMENTATION]

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来扩展拆分正则表达式。

要求6:支持不同的分隔符

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

[JAVA TEST]

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

[JAVA IMPLEMENTATION]

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;
}

这次有很多重构。我们将代码分为两种方法。 Initial方法解析输入以查找分隔符,然后调用执行实际和的新输入。由于我们已经有涵盖所有现有功能的测试,因此进行重构是安全的。如果出现任何问题,其中一个测试会发现问题。

要求7:否定数字将引发异常

使用负数调用Add将抛出异常“不允许负数” - 以及传递的负数。如果有多个底片,请在异常消息中显示所有底片。

[JAVA TEST]

@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 IMPLEMENTATION]

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;
}

添加了此时间代码,用于收集List中的负数,如果有则会抛出异常。

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

示例:添加2 + 1001 = 2

[JAVA TEST]

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

[JAVA IMPLEMENTATION]

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:分隔符可以是任意长度

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

要求10:允许多个分隔符

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

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

给TDD一个机会

对于TDD初学者来说,整个过程往往看起来势不可挡。其中一个常见的抱怨是TDD会减慢开发过程。确实,起初需要时间才能加快速度。但是,经过一些实践开发后,使用TDD过程可以节省时间,产生更好的设计,允许简单安全的重构,提高质量和测试覆盖率,最后但并非最不重要的是,确保软件始终经过测试。 TDD的另一个好处是测试可以作为一个活文档。只需查看测试就可以知道每个软件单元应该做什么。只要所有测试都通过,该文档始终是最新的。使用TDD生成的单元测试应为大多数代码提供“代码覆盖”,并且它们应与验收测试驱动开发(ATDD)或行为驱动开发(BDD)一起使用。它们共同涵盖单元和功能测试,作为完整的文档和要求。

TDD让您专注于您的任务,准确编写您需要的代码,从外部思考,最终成为更好的程序员。


正文到此结束