您何时在TDD中编写“真实”代码?


147

我在培训视频上阅读和看到的所有示例都具有简单的示例。但是我看不到绿色后如何执行“真实”代码。这是“重构”部分吗?

如果我有一个使用复杂方法的相当复杂的对象,并且编写了测试和最低要求以使其通过(在它第一次失败后为红色)。我什么时候回去写真实的代码?在重新测试之前我要写多少实际代码?我猜最后一个是直觉。

编辑: 感谢所有回答。您的所有回答都极大地帮助了我。关于我所问或困惑的事情似乎有不同的想法,也许有,但是我所要问的是,说我有建校的申请。

在我的设计中,我有一个要开始的架构,即“用户故事”,依此类推。在这里,我将获取这些用户故事,然后创建一个测试以测试用户故事。用户说,我们有人入学并支付注册费。因此,我想一种使失败的方法。为此,我为X类(可能是学生)设计了一个测试类,该类将失败。然后,我创建“学生”类。也许我不知道“学校”。

但是,无论如何,TD 设计迫使我思考整个故事。如果我可以使测试失败,那么我知道为什么会失败,但这以我可以通过测试为前提。这与设计有关。

我将此比作考虑递归。递归并不是一个很难的概念。可能很难真正掌握在脑海中,但是实际上,最难的部分是知道递归何时“中断”,何时停止(当然,我的看法。)因此,我必须考虑停止什么递归优先。这只是一个不完美的类比,它假定每个递归迭代都是一次“通过”。再次,只是一个意见。

在实施中,学校很难见到。在可以使用简单算术的意义上,数字分类帐和银行分类帐是“容易的”。我可以看到a + b并返回0,依此类推。对于一个人系统来说,我必须更加思考如何实现它。我有失败,通过,重构的概念(主要是由于学习和这个问题。)

我认为,我不知道的原因是缺乏经验。我不知道如何无法注册新学生。我不知道如何使某人输入姓氏并将其保存到数据库中。我知道如何为简单的数学做一个+1,但是对于像一个人这样的实体,我不知道我是否只是在测试是否有人返回一个数据库的唯一ID或其他东西。数据库或两者兼而有之。

或者,也许这表明我仍然感到困惑。


193
TDD之后,人们回家过夜。
hobbs

14
为什么您认为编写的代码不是真实的?
Goyo

2
@RubberDuck比其他答案更多。我相信我会尽快参考。它仍然是外国的,但我不会放弃。你说的很有道理。我只是想使其在我的上下文或常规业务应用程序中有意义。也许是库存系统或类似系统。我必须考虑一下。不过,我感谢您的宝贵时间。谢谢。
约翰尼

1
答案已经触动了脑筋,但是只要您所有的测试都通过了,并且您不需要任何新的测试/功能,就可以假定您已经完成了代码,这很简单。
ESR

3
问题中的一个假设可能是“我有一个使用复杂方法的相当复杂的对象”。在TDD中,您首先要编写测试,所以从一个相当简单的代码开始。这将迫使您编写一个需要模块化的,易于测试的结构。因此,将通过组合更简单的对象来创建复杂的行为。如果您以一个相当复杂的对象或方法结束,那么就是重构的时候
Borjab

Answers:


243

如果我有一个使用复杂方法的相当复杂的对象,并且编写了测试和最低要求以使其通过(在它第一次失败后为红色)。我什么时候回去写真实的代码?在重新测试之前我要写多少实际代码?我猜最后一个是直觉。

您无需“返回”并编写“真实代码”。这都是真实的代码。您要做的是返回并添加另一个测试,该测试迫使更改代码以通过新测试。

至于在重新测试之前要编写多少代码?没有。您编写代码而没有失败的测试,该测试迫使您编写更多的代码。

注意到模式了吗?

让我们来看一个(另一个)简单的示例,希望对您有所帮助。

Assert.Equal("1", FizzBuzz(1));

容易豆豆。

public String FizzBuzz(int n) {
    return 1.ToString();
}

不是您所说的真实代码,对吗?让我们添加一个强制更改的测试。

Assert.Equal("2", FizzBuzz(2));

我们可以做一些愚蠢的事情,例如if n == 1,但是我们将跳到理智的解决方案。

public String FizzBuzz(int n) {
    return n.ToString();
}

凉。这将适用于所有非FizzBu​​zz编号。下一个将迫使生产代码更改的输入是什么?

Assert.Equal("Fizz", FizzBuzz(3));

public String FizzBuzz(int n) {
    if (n == 3)
        return "Fizz";
    return n.ToString();
}

然后再次。编写尚未通过的测试。

Assert.Equal("Fizz", FizzBuzz(6));

public String FizzBuzz(int n) {
    if (n % 3 == 0)
        return "Fizz";
    return n.ToString();
}

现在,我们涵盖了三个的所有倍数(不是五个的倍数,我们会注意并返回)。

我们还没有为“ Buzz”编写测试,因此让我们来编写它。

Assert.Equal("Buzz", FizzBuzz(5));

public String FizzBuzz(int n) {
    if (n % 3 == 0)
        return "Fizz";
    if (n == 5)
        return "Buzz"
    return n.ToString();
}

再说一次,我们知道还需要处理另一种情况。

Assert.Equal("Buzz", FizzBuzz(10));

public String FizzBuzz(int n) {
    if (n % 3 == 0)
        return "Fizz";
    if (n % 5 == 0)
        return "Buzz"
    return n.ToString();
}

现在我们可以处理5的所有倍数,而不是3的倍数。

到目前为止,我们一直忽略重构步骤,但是我看到了一些重复。让我们现在清理它。

private bool isDivisibleBy(int divisor, int input) {
    return (input % divisor == 0);
}

public String FizzBuzz(int n) {
    if (isDivisibleBy(3, n))
        return "Fizz";
    if (isDivisibleBy(5, n))
        return "Buzz"
    return n.ToString();
}

凉。现在,我们删除了重复项,并创建了一个命名良好的函数。我们可以编写的下一个将迫使我们更改代码的测试是什么?好吧,我们一直在避免数字被3和5整除的情况。让我们现在来编写它。

Assert.Equal("FizzBuzz", FizzBuzz(15));

public String FizzBuzz(int n) {
    if (isDivisibleBy(3, n) && isDivisibleBy(5, n))
        return "FizzBuzz";
    if (isDivisibleBy(3, n))
        return "Fizz";
    if (isDivisibleBy(5, n))
        return "Buzz"
    return n.ToString();
}

测试通过了,但是我们有更多重复项。我们有选项,但是我要多次应用“提取局部变量”,以便我们重构而不是重写。

public String FizzBuzz(int n) {

    var isDivisibleBy3 = isDivisibleBy(3, n);
    var isDivisibleBy5 = isDivisibleBy(5, n);

    if ( isDivisibleBy3 && isDivisibleBy5 )
        return "FizzBuzz";
    if ( isDivisibleBy3 )
        return "Fizz";
    if ( isDivisibleBy5 )
        return "Buzz"
    return n.ToString();
}

我们已经涵盖了所有合理的输入,但是不合理的输入呢?如果传递0或负数会怎样?编写那些测试用例。

public String FizzBuzz(int n) {

    if (n < 1)
        throw new InvalidArgException("n must be >= 1);

    var isDivisibleBy3 = isDivisibleBy(3, n);
    var isDivisibleBy5 = isDivisibleBy(5, n);

    if ( isDivisibleBy3 && isDivisibleBy5 )
        return "FizzBuzz";
    if ( isDivisibleBy3 )
        return "Fizz";
    if ( isDivisibleBy5 )
        return "Buzz"
    return n.ToString();
}

这看起来像“真实代码”了吗?更重要的是,在什么时候它不再是“虚幻的代码”,而是过渡到“真实的”?这是值得深思的...

因此,我可以通过寻找一个我知道不会在每个步骤中通过的测试来简单地做到这一点,但是我已经做了很多练习。当我在工作时,事情从来都不是那么简单,而且我可能并不总是知道哪种测试将迫使变革。有时我会写一个测试,惊讶地发现它已经通过了!我强烈建议您在开始之前养成创建“测试列表”的习惯。该测试列表应包含您可以想到的所有“有趣”输入。您可能不会全部使用它们,并且可能会随时添加案例,但是此列表可作为路线图。我的FizzBu​​zz测试清单如下所示。

  • 四个
  • 六(3的非平凡倍数)
  • 九(3平方)
  • 十(5的非平凡倍数)
  • 15(3和5的倍数)
  • 30(3和5的非平凡倍数)

3
评论不作进一步讨论;此对话已转移至聊天
maple_shaft

47
除非我完全误解了这个答案:“如果n == 1,我们可以做一些愚蠢的事情,但是我们将跳到理智的解决方案。” -整个事情都很愚蠢。如果您事先知道要使用<spec>的功能,请为<spec>编写测试,并跳过编写显然未通过<spec>的版本的部分。如果在<spec>中发现错误,请确定:首先编写测试以确认您可以在修复之前进行练习,并观察修复之后的测试通过。但是,无需伪造所有这些中间步骤。
GManNickG

16
指出此答案和TDD的主要缺陷的评论已转移到聊天室。如果您正在考虑使用TDD,请阅读“聊天”。不幸的是,“质量”注释现在隐藏在聊天中,供将来的学生阅读。
user3791372

2
@GManNickG我相信关键是要获得正确数量的测试。事先编写测试很容易错过应该测试的特殊情况,从而导致测试中未充分涵盖的情况或测试中无意义地涵盖了相同情况。如果您没有这些中间步骤就可以做到,那就太好了!但是,并非所有人都可以这样做,这需要实践。
hvd

1
这是肯特·贝克(Kent Beck)关于重构的一句话:“现在测试运行了,我们可以实现summary()了(就像在“ make real ”中一样)”。然后,他继续将常量更改为变量。我觉得这句话很符合这个问题。
克里斯·沃勒特

46

“真实”代码是您编写以通过测试的代码。真的。就这么简单。

当人们谈论编写使测试变得绿色的最低限度时,这仅意味着您的实际代码应遵循YAGNI原则

重构步骤的想法是,只要您满意满足要求,就清理您编写的内容。

只要您编写的测试实际上涵盖了您的产品要求,一旦这些要求通过,便可以完成代码。考虑一下,如果您的所有业务需求都具有测试并且所有这些测试都是绿色的,还有什么要写的呢?(好吧,在现实生活中,我们往往没有完整的测试范围,但是理论是合理的。)


45
单元测试实际上甚至不能涵盖相对较小的需求的产品需求。充其量,他们对输入输出空间进行了采样,其想法是(正确地)概括为整个输入输出空间。当然,您的代码switch对于每个单元测试来说都是很大的,它将通过所有测试而对其他任何输入均失败。
德里克·埃尔金斯

8
@DerekElkins TDD要求测试失败。不失败的单元测试。
Taemyr

6
这就是为什么@DerekElkins不仅编写单元测试的原因,而且还有一个普遍的假设,即您要尝试制作的东西不只是假的而已!
jonrsharpe

36
@jonrsharpe按照这种逻辑,我永远不会编写琐碎的实现。例如,在RubberDuck的答案中的FizzBu​​zz示例(仅使用单元测试)中,第一个实现显然是“只是伪造的”。我对问题的理解恰恰是在编写您知道不完整的代码与您真正相信将实现要求的代码(即“实际代码”)之间的这种二分法。我的“大switch”意图是“写出最低限度以使测试变成绿色”的逻辑极端。我认为OP的问题是:在TDD中,避免这么大的原理是switch什么?
德里克·埃尔金斯

2
@GenericJon根据我的经验,这有点太乐观了:)首先,有些人喜欢漫无目的的重复性工作。与“复杂的决策”相比,他们拥有更大的转换声明会更快乐。而要丢掉他们的工作,他们要么需要有人请他们使用这项技术(而且他们最好有充分的证据证明实际上正在失去公司的机会/金钱!),或者做得非常糟糕。在接管了许多此类项目的维护工作之后,我可以告诉我们,很幼稚的代码可以轻松持续数十年,只要它能让客户满意(并付款)即可。
a安

14

简短的答案是“真实代码”是使测试通过的代码。如果您可以通过真实代码以外的其他方式通过测试,请添加更多测试!

我同意很多有关TDD的教程都过于简单。那对他们不利。对于计算3 + 8的方法而言,过于简单的测试实际上别无选择,只能计算3 + 8并比较结果。这看起来就像您将要遍历整个代码一样,并且测试是毫无意义,容易出错的额外工作。

当您擅长测试时,这将通知您如何构造应用程序以及如何编写代码。如果您在提出明智,有用的测试方面遇到困难,则应该重新考虑一下您的设计。精心设计的系统易于测试-意味着明智的测试易于考虑和实施。

当您首先编写测试时,看着它们失败,然后编写使它们通过的代码,这是确保所有代码都具有相应测试的准则。在编码时,我不会从容地遵循该规则。我经常在事实之后写测试。但是首先进行测试有助于保持诚实。有了一些经验,即使当您不首先编写测试时,也会开始注意到将自己编码为一个角落的时间。


6
就我个人而言,我要编写的测试assertEqual(plus(3,8), 11)不是assertEqual(plus(3,8), my_test_implementation_of_addition(3,8))。对于更复杂的情况,除了在测试中动态计算正确的结果并检查相等性之外,您总是寻找一种证明结果正确的方法。
史蒂夫·杰索普

因此,对于本示例而言,这样做是一种非常愚蠢的方法,您可以plus(3,8)通过从中减去3,从中减去8,然后将结果与0进行比较assertEqual(plus(3,8), 3+8),来证明返回了正确的结果。有点荒谬,但是如果要测试的代码正在构建的东西不仅仅是整数,那么获取结果并检查每个部分的正确性通常是正确的方法。或者,类似for (i=0, j=10; i < 10; ++i, ++j) assertEqual(plus(i, 10), j)
Steve Jessop

...这样可以避免很大的恐惧,那就是在编写测​​试时,我们将在实时代码中对“如何添加10”这一主题犯同样的错误。因此,测试应谨慎地避免编写任何会增加10的代码,而在测试中plus()可能会增加10的代码。当然,我们仍然依赖于程序员验证的初始循环值。
史蒂夫·杰索普

4
只是想指出,即使事后编写测试,也要确保测试失败仍然是一个好主意。找到对您正在从事的工作来说似乎至关重要的代码部分,对其进行一些调整(例如,用-替换+,或执行其他操作),运行测试,然后观察它们是否失败,撤消更改并看着它们通过。我做了很多次测试实际上并没有失败,这使它变得比没用更糟:不仅不测试任何东西,还使我对正在测试的东西有虚假的信心!
Warbo

6

有时,有关TDD的一些示例可能会产生误导。正如其他人之前指出的那样,为使测试通过而编写的代码是真实的代码。

但是不要以为真正的代码看起来就像魔术一样-错了。您需要对要达到的目标有更好的了解,然后需要从最简单的案例和极端案例开始进行相应的测试。

例如,如果您需要编写一个词法分析器,则从空字符串开始,然后从一堆空白开始,然后是一个数字,然后是一个被空白包围的数字,然后是一个错误的数字,等等。这些小的转换会使您进入正确的算法,但是您不会从最简单的情况过渡到为完成实际代码而笨拙地选择的高度复杂的情况。

鲍勃·马丁(Bob Martin)在这里完美地解释了这一点


5

重构部分是在您累了要回家时清理的。

当您要添加功能时,重构部分就是您在下一次测试之前所做的更改。您可以重构代码以为新功能腾出空间。当您知道新功能将是什么时,便可以执行此操作。并非只是在想象时。

这很简单,只要重命名GreetImplGreetWorld创建GreetMom类(添加测试后)以添加将打印“ Hi Mom”的功能的名称即可。


1

但是实际代码将出现在TDD阶段的重构阶段。即应成为最终版本一部分的代码。

每次进行更改时都应运行测试。

TDD生命周期的座右铭是:红色绿色REFACTOR

红色:编写测试

绿色:诚实地尝试获取可以尽快通过测试的功能代码:重复的代码,名称模糊不清的最高级别的变量hack等。

REFACTOR:清理代码,正确命名变量。烘干代码。


6
我知道您在说“绿色”阶段,但是这意味着硬连接返回值以使测试通过可能是适当的。以我的经验,“绿色”应该是诚实的尝试,以使工作代码满足要求,它可能不是完美的,但它应与开发人员可以在第一遍管理的过程中一样完整和“可交付”。进行更多的开发后,重构可能最好在一段时间后再进行,并且第一遍的问题变得更加明显,并且出现了DRY的机会。
mcottle '17

2
@mcottle:您可能会惊讶于在代码库中有多少个仅获取存储库的实现可以硬编码为值。:)
Bryan Boettcher's

6
为什么我可以编写废话代码并清理它,而我却可以打字出几乎和键入一样快的优质生产质量代码?:)
Kaz

1
@Kaz因为这样,您冒着添加未经测试的行为的风险。确保对每个所需行为进行测试的唯一方法是进行简单的可能更改,而不管它有多糟糕。有时以下重构带来了您未曾想到的新方法……
Timothy Truckle

1
@TimothyTruckle如果花50分钟才能找到最简单的更改,而只花5分钟才能找到第二个最简单的更改呢?我们选择第二简单的还是继续寻找最简单的?
卡兹(Kaz)

1

您何时在TDD中编写“真实”代码?

红色阶段是你写的代码。

重构阶段,主要目标是删除代码。

红色阶段,您将采取一切措施使测试尽快通过并且不惜一切代价。您完全无视关于良好编码实践或设计模式的任何经验。使测试变为绿色至关重要。

重构阶段,您将清理刚刚产生的混乱情况。现在,您首先查看您刚刚进行的更改是否是“ 转换优先级”列表中最重要的更改,并且如果有任何代码重复,则可以通过应用设计模式将其删除。

最后,您可以通过重命名标识符并将魔术数字和/或文字字符串提取为常量来提高可读性。


它不是红色重构,而是红色-绿色重构。– Rob Kinyon

感谢您指出这一点。

所以这是您编写实际代码绿色阶段

红色阶段,您编写可执行规范 ...


它不是红色重构,而是红色-绿色重构。“红色”是您将测试套件从绿色(所有测试通过)变为红色(一个测试失败)。“绿色”是您将测试套件从红色(一个测试失败)变为绿色(所有测试通过)的地方。在“重构”中,可以使代码变得漂亮,同时保持所有测试通过。
Rob Kinyon

1

您一直在编写Real Code

在每一步中,您都在编写代码,以满足您的代码将来对代码调用者的要求(可能是您也可能不是)。

您认为您没有在编写有用的(真实的)代码,因为稍后您可能会对其进行重构。

代码重构 是在不更改其外部行为的情况下重构现有计算机代码(更改因子)的过程。

这意味着即使您正在更改代码,代码满足的条件也将保持不变。然后执行检查(测试)以验证您的代码是否已经存在,以验证您的修改是否更改了任何内容。因此,您一直编写的代码就在那里,只是以一种不同的方式。

您可能会认为它不是真正的代码的另一个原因是,您正在做一些示例,其中您已经可以预见到最终程序。这是非常好的,因为它表明你对知识的领域你是编程的。
但是很多时候程序员都在一个新的未知他们。他们不知道最终结果是什么,而TDD是一种逐步编写程序的技术,它记录了我们对该系统应该如何工作的知识,并验证了我们的代码是否可以那样工作。

当我阅读TDD上的Book(*)时,对我来说最重要的功能是:TODO list。它向我表明,TDD也是一种帮助开发人员一次专注于一件事情的技术。因此,这也是对您的问题的答案,要写多少实际代码?我会说足够的代码来一次专注于一件事情。

(*)肯特·贝克(Kent Beck)的“测试驱动开发:以示例”


2
肯特·贝克(Kent Beck)的“测试驱动的开发:
以身作则

1

您不是在编写使测试失败的代码。

您编写测试来定义成功的外观,而最初应该失败的所有测试都是失败的,因为您尚未编写将通过的代码。

编写最初失败的测试的全部要点是要做两件事:

  1. 涵盖所有情况-所有名义情况,所有边缘情况等
  2. 验证您的测试。如果您只看到它们通过,您如何确定它们发生时会可靠地报告故障?

red-green-refactor背后的观点是,首先编写正确的测试可以使您有信心知道为通过测试而编写的代码是正确的,并使您可以放心地重构,以便测试能够尽快通知您。出现问题,因此您可以立即返回并进行修复。

以我自己的经验(C#/。NET),纯测试优先是一个无法实现的理想,因为您无法编译对尚不存在的方法的调用。因此,“首先测试”实际上是先对接口进行编码和存根实现,然后针对存根(最初会失败)编写测试,直到存根被正确充实为止。我从来没有写过“失败的代码”,只是从存根中构建了代码。


0

我认为您可能在单元测试和集成测试之间感到困惑。我相信可能还会有验收测试,但这取决于您的过程。

一旦测试了所有小的“单元”,就可以测试它们是否已组装或“集成”。通常是整个程序或库。

在我编写的代码中,集成测试了一个包含各种测试程序的库,这些程序读取数据并将其馈送到该库,然后检查结果。然后我用线程来做。然后,我在中间使用线程和fork()进行处理。然后运行它并在2秒后杀死-9,然后启动它并检查其恢复模式。我把它弄糊涂了。我以各种方式折磨它。

所有这些都还在测试中,但是结果没有漂亮的红色/绿色显示。它要么成功,要么我挖掘几千行错误代码以找出原因。

那就是您测试“真实代码”的地方。

我只是想到了这一点,但是也许您不知道何时应该完成编写单元测试。当测试执行您指定的所有工作时,就完成了单元测试的编写。有时,您可能会在所有错误处理和边缘情况之间失去对这一点的了解,因此您可能希望创建一个很好的测试组,该测试组可以简单地直接通过规范,进行愉快的路径测试。


(它=所有格,它=“它是”或“它具有”。例如参见如何使用它和它。)
Peter Mortensen17年

-6

在回答问题标题:“何时在TDD中编写“真实”代码?”时,答案是:“几乎不会”或“非常缓慢”。

您听起来像一个学生,所以我会像给学生一个建议一样回答。

您将学习很多编码“理论”和“技术”。它们非常适合在高价的学生课程上打发时间,但是对您却几乎没有好处,因为您半个小时都无法看书。

编码员的工作仅是产生代码。效果很好的代码。这就是编码人员在头脑中,在纸上,在合适的应用程序等中计划代码的原因,并且您计划在编码之前通过逻辑和横向思考来预先解决可能存在的缺陷/漏洞。

但是您需要知道如何破坏您的应用程序才能设计出不错的代码。例如,如果您不了解Little Bobby Table(xkcd 327),那么在使用数据库之前,您可能不会清理输入,因此您将无法围绕该概念保护数据。

TDD只是一个工作流,旨在通过在编写应用程序之前创建可能出问题的测试来最大程度地减少代码中的错误,因为引入的代码越多,编码就会成倍增加难度,而您会忘记曾经想到的错误。一旦您认为自己的应用程序已经完成,就可以运行测试并迅速发展,希望在测试中捕获错误。

TDD不是-像某些人所相信的那样-编写测试,以最少的代码通过测试,编写另一个测试,以最少的代码通过测试等。相反,TDD可以帮助您自信地进行编码。连续重构代码以使其能够与测试一起使用的理想是愚蠢的,但这在学生中是一个不错的概念,因为它使他们在添加新功能时仍感觉很好,并且仍在学习如何编写代码...

请不要落入这个陷阱,看看您的编码作用是什么-编码器的工作仅仅是产生代码。效果很好的代码。现在,请记住,您将是一名专业的编码员,这将使您全天候工作,并且您的客户端将不在乎您是否编写了100,000个断言(即0)。他们只希望代码有效。真的很好,事实上。


3
我什至没有一个学生,但是我确实阅读并尝试运用好的技术并变得专业。所以从这个意义上说,我是一个“学生”。我只是问一些非常基本的问题,因为这就是我的方式。我想确切地知道为什么我在做自己在做的事情。事件的核心。如果我不明白这一点,我将不喜欢它并开始提出问题。我需要知道为什么,如果我要使用它。TDD在某些方面直观上不错,例如知道您需要创建什么并仔细考虑事情,但是实现很难理解。我想我现在有了更好的掌握。
约翰尼


4
这些是TDD的规则。您可以随意编写代码,但是,如果不遵循这三个规则,则不会执行TDD。
肖恩·伯顿

2
一个人的“规则”?TDD是帮助您编码的建议,而不是宗教信仰。这是可悲的,看到这么多的人坚持一个想法,所以肛门。甚至TDD的起源也是有争议的。
user3791372

2
@ user3791372 TDD是一个非常严格且定义明确的过程。即使许多人认为这仅意味着“在编程时进行一些测试”,但事实并非如此。让我们在这里不要混淆术语,这个问题是关于TDD的过程,而不是一般的测试。
亚历克斯(Alex)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.