如果我有一个使用复杂方法的相当复杂的对象,并且编写了我的测试和最低要求以使其通过(在第一次失败后为红色)。我什么时候回去写真实的代码?在重新测试之前我要写多少实际代码?我猜最后一个是更多的直觉。
编辑:感谢所有回答。您的所有回答都极大地帮助了我。关于我所问或困惑的事情似乎有不同的想法,也许有,但是我所要问的是,说我有一座学校的申请。
在我的设计中,我有一个要开始的体系结构,即用户故事,依此类推。在这里,我将获取这些用户故事,然后创建一个测试以测试用户故事。用户说,我们有人入学并支付注册费。因此,我想到了一种使失败的方法。为此,我为X类(可能是学生)设计了一个测试类,该类将失败。然后,我创建类“学生”。也许我不知道是“学校”。但是,无论如何,TD设计迫使我思考这个故事。如果我可以使测试失败,那么我知道为什么会失败,但这以我可以通过测试为前提。这与设计有关。
我将其比作对递归的思考。递归并不是一个很难的概念。可能很难真正掌握在脑海中,但是实际上,最难的部分是知道递归何时“中断”,何时停止(当然,我的看法。)所以我必须考虑停止什么递归优先。这只是一个不完美的类比,它假定每个递归迭代都是一次“通过”。再次,只是一个意见。
在实施中,学校很难见到。在可以使用简单算术的意义上,数字分类帐和银行分类帐是“容易的”。我可以看到a + b并返回0,依此类推。对于一个人系统来说,我必须更加认真地思考如何实现它。我有失败,通过,重构的概念(主要是由于学习和这个问题。)
我不知道的原因是缺乏经验。我不知道如何无法注册新学生。我不知道如何使某人输入姓氏失败并将其保存到数据库。我知道如何为简单的数学做一个+1,但是对于像一个人这样的实体,我不知道我是否只是在测试是否有人返回一个数据库的唯一ID或其他东西。数据库,或者两者都不存在。
或者,这也许表明我仍然感到困惑。
#1 楼
如果我有一个使用复杂方法的相当复杂的对象,并且我编写了
测试和最基本的条件以使其通过(在第一次失败后,
红色)。我什么时候回去写真实的代码?在重新测试之前我要写多少真实代码?我猜想最后一个是
更直观。这都是真实的代码。您要做的是返回并添加另一个测试,该测试迫使您更改代码以通过新测试。
在重新测试之前要编写多少代码?没有。您编写零代码而没有失败的测试,该测试迫使您编写更多的代码。
注意图案吗?
让我们通过另一个简单的示例,希望对您有所帮助。
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();
}
很酷。这将适用于所有非FizzBuzz编号。下一个将迫使生产代码更改的输入是什么?
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();
}
我们现在已经覆盖了三个的所有倍数(不是倍数)五分之一,我们会记下来然后再回来)。
我们还没有为“嗡嗡声”编写测试,所以让我们来编写它。
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();
}
这看起来像“真实代码”吗?更重要的是,在什么时候它不再是“虚幻的代码”,而是过渡到“真实的”?这是需要考虑的问题...
因此,我能够通过寻找我知道不会在每个步骤中通过的测试来简单地做到这一点,但是我已经做了很多练习。当我在工作时,事情从来都不是那么简单,而且我可能并不总是知道哪种测试将迫使变革。有时候我会写一个测试,惊讶地发现它已经通过了!我强烈建议您在开始之前养成创建“测试列表”的习惯。该测试列表应包含您可以想到的所有“有趣”输入。您可能不会全部使用它们,并且可能会随时添加案例,但是此列表可作为路线图。我的FizzBuzz测试列表如下所示。
负数
零
一个
两个
三个
四个
五个
六个(非平凡的3的倍数) )
九(3平方)
十(5的非平凡倍数)
15(3和5的非凡倍数)
30(3和5的非平凡倍数)
评论
评论不作进一步讨论;此对话已移至聊天。
– Maple_shaft♦
17年7月27日在13:42
除非我完全误解了这个答案:“如果n == 1,我们可以做一些愚蠢的事情,但我们将跳到理智的解决方案。” -整个事情都很愚蠢。如果您预先知道要使用
– GManNickG
17年7月27日在21:47
指出此答案和TDD的主要缺陷的评论已转移到聊天室。如果您正在考虑使用TDD,请阅读“聊天”。不幸的是,“质量”评论现在隐藏在大量的聊天中,以供将来的学生阅读。
–user3791372
17年7月28日在0:07
@GManNickG我相信关键是要获得正确数量的测试。事先编写测试很容易错过应该测试的特殊情况,从而导致测试中的情况没有得到充分覆盖,或者导致相同的情况被测试中无意义的时间负载所覆盖。如果没有这些中间步骤就可以做到,那就太好了!但是,并非所有人都可以这样做,这需要实践。
–hvd
17年7月30日在9:08
这是肯特·贝克(Kent Beck)关于重构的一句话:“现在测试运行了,我们可以实现summary()了(如“实现”)”。然后,他继续将常量更改为变量。我觉得这句话很符合这个问题。
–克里斯·沃勒特(Chris Wohlert)
17年8月4日在17:57
#2 楼
“真实”代码是您编写以通过测试的代码。真。就这么简单。当人们谈论编写最低限度的测试以使测试变为绿色时,这仅意味着您的实际代码应遵循YAGNI原则。
重构步骤只是在满足您的要求之后就清理您编写的内容。
,只要您编写的测试实际上包含您的产品要求,则这些测试通过那么代码就完成了。考虑一下,如果您的所有业务需求都具有测试并且所有这些测试都是绿色的,那么还有什么要写的呢? (好吧,在现实生活中,我们往往没有完整的测试范围,但是理论是合理的。)
评论
单元测试实际上无法涵盖产品需求,甚至是相对琐碎的需求。充其量,他们对输入-输出空间进行了采样,其想法是(正确地)概括为整个输入-输出空间。当然,您的代码可能只是一个很大的开关,每个单元测试都有一个案例,该案例将通过所有测试而对其他任何输入均失败。
–德里克·埃尔金斯离开东南
17年7月24日在22:58
@DerekElkins TDD要求测试失败。不失败的单元测试。
–塔米尔
17年7月25日在5:56
@DerekElkins这就是为什么您不只是编写单元测试的原因,也是为什么人们普遍认为您要制造的东西不只是假的而已!
– jonrsharpe
17年7月25日在6:37
@jonrsharpe按照这种逻辑,我永远不会编写琐碎的实现。例如。在RubberDuck的答案的FizzBuzz示例中(仅使用单元测试),第一个实现显然是“只是伪造的”。我对问题的理解恰恰是在编写您知道不完整的代码与您真正相信将实现要求的代码(即“实际代码”)之间的这种二分法。我的“大选择”是“写最低限度使测试变成绿色”的逻辑极端。我认为OP的问题是:在TDD中,避免这种大转变的原理是什么?
–德里克·埃尔金斯离开东南
17年7月25日在6:59
@GenericJon我的经历有点太乐观了:)首先,有些人喜欢漫不经心的重复工作。与“复杂的决策”相比,他们拥有更大的转换声明会更快乐。而要丢掉他们的工作,他们要么需要有人用这项技术来呼吁他们(而且他们最好有充分的证据证明实际上正在失去公司的机会/金钱!),或者做得非常糟糕。在接管了许多此类项目的维护工作之后,我可以告诉我们,幼稚的代码很容易持续数十年,只要它能使客户满意(并付款)即可。
–罗安
17年7月25日在10:34
#3 楼
简短的答案是“真实代码”是使测试通过的代码。如果您可以通过真实代码以外的其他方式通过测试,请添加更多测试!我同意很多有关TDD的教程都过于简单。那对他们不利。对于计算3 + 8的方法而言,过于简单的测试实际上别无选择,只能计算3 + 8并比较结果。这样就好像您将要遍历整个代码一样,并且测试是毫无意义的,容易出错。额外的工作。应用程序以及如何编写代码。如果您在提出明智,有用的测试方面遇到困难,则可能应该重新考虑一下您的设计。设计良好的系统易于测试-意味着明智的测试易于思考和实施。他们通过了,这是确保所有代码都有相应测试的准则。在编码时,我不会从容地遵循该规则。我经常在事实之后写测试。但是首先进行测试有助于保持诚实。有了一些经验,即使当您不首先编写测试时,也会开始注意到何时将自己编码到一个角落。
评论
就个人而言,我要编写的测试将是assertEqual(plus(3,8),11),而不是assertEqual(plus(3,8),my_test_implementation_of_addition(3,8))。对于更复杂的情况,除了在测试中动态计算正确的结果并检查相等性之外,您总是寻找一种证明结果正确的方法。
–史蒂夫·杰索普(Steve Jessop)
17年7月26日在13:59
因此,对于本示例而言,这样做是一种非常愚蠢的方式,您可以证明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)
17年7月26日在14:02
...这样可以避免很大的恐惧,那就是在编写测试时,我们将在实时代码中对“如何添加10”这一主题犯同样的错误。因此,该测试谨慎地避免编写任何将10加到任何代码的代码,而在测试中plus()可以将10加到所有代码。当然,我们仍然依赖于程序员验证的初始循环值。
–史蒂夫·杰索普(Steve Jessop)
17年7月26日在14:08
只是要指出,即使事后编写测试,也要确保测试失败仍然是一个好主意。找到对您正在从事的工作来说似乎至关重要的代码部分,对其进行一些调整(例如,用-替换+,或其他方法),运行测试并观察它们是否失败,撤消更改并观察它们通过。我做了很多次测试实际上并没有失败,这使它变得比没用更糟:不仅不测试任何东西,还使我对正在测试的东西充满信心!
–Warbo
17年7月28日在12:40
#4 楼
有时关于TDD的一些示例可能会误导您。正如其他人之前指出的那样,您编写的用于使测试通过的代码是真实的代码。代码看起来像魔术一样-这是错误的。
您需要对要达到的目标有更好的了解,然后需要从最简单的案例和极端案例开始进行相应的测试。
例如,如果您需要编写词法分析器,则从空字符串开始,然后从一堆空白开始,然后是一个数字,然后是被空格包围的数字,然后是错误的数字,等等。这些小的转换将使您正确的算法,但是您不会从最简单的情况过渡到为完成实际代码而笨拙地选择的高度复杂的情况。
Bob Martin在这里完美地解释了它。
#5 楼
当您很累并且想回家时,重构部分会清理。当您要添加功能时,重构部分就是您在下一次测试之前所做的更改。您可以重构代码以为新功能腾出空间。当您知道新功能将是什么时,就可以执行此操作。并非只是在想象中。
这就像在创建
GreetImpl
类(添加测试后)以添加将打印“ Hi Mom”的功能之前将GreetWorld
重命名为GreetMom
一样简单。 #6 楼
但是实际代码将出现在TDD阶段的重构阶段。即
每次更改时都应运行测试。
TDD生命周期的座右铭是:红色绿色REFACTOR
RED:编写测试
GREEN:诚实尝试获得尽快通过测试的功能代码:重复的代码,名称晦涩难懂的变量顺序等。
REFACTOR:清理代码,正确命名变量。干燥代码。
评论
我知道您在说“绿色”阶段,但是这意味着硬连接返回值以使测试通过可能是适当的。以我的经验,“绿色”应该是诚实的尝试,以使工作代码满足要求,它可能不是完美的,但它应与开发人员可以在第一遍管理的过程中一样完整和“可交付”。进行更多的开发之后,重构可能最好在一段时间后再进行,并且第一遍的问题变得更加明显,并且出现DRY的机会也越来越多。
– mcottle
17年7月25日在5:32
@mcottle:您可能会惊讶于在代码库中有多少个仅获取存储库的实现可以硬编码为值。 :)
–布莱恩·博特彻(Bryan Boettcher)
17年7月25日在15:06
为什么我可以编写废话代码并将其清理干净,而我却可以打字出几乎和键入一样快的优质生产质量代码? :)
–卡兹
17年7月25日在19:14
@Kaz因为这样,您冒着添加未经测试的行为的风险。确保对每个所需行为进行测试的唯一方法是进行简单的可能更改,而不管它有多糟糕。有时以下重构带来了您未曾想到的新方法...
–提莫西·卡特勒(Timothy Truckle)
17年7月27日在20:49
@TimothyTruckle如果花50分钟才能找到最简单的更改,而只花5分钟才能找到第二个最简单的更改呢?我们选择第二简单的还是继续寻找最简单的?
–卡兹
17年7月27日在23:08
#7 楼
什么时候在TDD中编写“真实”代码?
红色阶段是编写代码的地方。
在重构阶段,主要目标是删除代码。在红色阶段,您需要做任何事情,以使测试尽快通过,并且不惜一切代价。您完全无视关于良好编码实践或设计模式的任何经验。使测试变成绿色就很重要。
在重构阶段,您将清理刚刚产生的混乱。现在,您首先看看您所做的更改是否是“转换优先级”列表中最重要的更改,并且如果有任何代码重复,您可以通过应用设计模式将其删除。通过重命名标识符并将魔术数字和/或文字字符串提取为常量来提高可读性。
它不是红色分解,而是红色-绿色分解。 – Rob Kinyon
谢谢你指出这一点。您编写可执行规范的红色阶段...
评论
它不是红色重构,而是红色-绿色重构。 “红色”是您将测试套件从绿色(所有测试通过)变为红色(一个测试失败)。 “绿色”是您将测试套件从红色(一个测试失败)变为绿色(所有测试通过)的地方。在“重构”中,您可以获取代码并使代码漂亮,同时保持所有测试通过。
–Rob Kinyon
17年7月27日在18:46
#8 楼
您一直在编写真实代码。您在编写代码的每一步都是为了满足您的代码满足将来对您代码的调用者(可能是您或不是您)的条件。
您认为您没有在编写有用的(真实的)代码,因为您可能会暂时重构它。分解-不更改其外部行为。
这意味着即使您正在更改代码,代码满足的条件也保持不变。以及为验证您的代码而实施的检查(测试)是否已存在,以验证您的修改是否有任何更改。因此,您一直编写的代码都以不同的方式存在于其中。
您可能会认为这不是真正的代码,另一个原因是您正在编写示例,其中最终程序可能已经由您预见了。 。这非常好,因为它表明您已经了解要编程的领域。他们不知道最终结果是什么,而TDD是一种逐步编写程序的技术,它记录了我们对该系统应该如何工作的知识,并验证了我们的代码是否可以那样工作。
当我阅读The关于TDD的书(*),对我而言,最重要的功能是:TODO列表。它向我表明,TDD还是一种技术,可以帮助开发人员一次专注于一件事情。因此,这也是对您的问题的答案,要写多少实际代码?我会说足够多的代码一次只关注一件事。
(*)肯特·贝克(Kent Beck)的“测试驱动开发:通过示例”
评论
肯特·贝克(Kent Beck)的“测试驱动开发:通过示例”
– Robert Andrzejuk
17年7月27日在20:51
#9 楼
您不是在编写使测试失败的代码。您在编写测试以定义成功的样子,而最初应该全部失败,因为您尚未编写将通过的代码。
编写最初失败的测试的整个目的是要做两件事:
覆盖所有情况-所有名义情况,所有边缘情况,等等
验证您的测试。如果只看到它们通过,您如何确定它们在发生故障时能够可靠地报告故障?知道编写的用于通过测试的代码是正确的,并且可以让您放心地重构,以便一旦出现问题,测试将立即通知您,因此您可以立即返回并对其进行修复。
以我自己的经验(C#/。NET),纯测试优先是一个无法实现的理想,因为您无法编译对尚不存在的方法的调用。因此,“首先测试”实际上是首先对接口进行编码和存根实现,然后针对存根(最初会失败)编写测试,直到存根被正确充实。我从来没有写过“失败的代码”,只是从存根开始构建。
#10 楼
我认为您可能在单元测试和集成测试之间感到困惑。我相信可能还会有验收测试,但这取决于您的过程。一旦测试了所有小的“单元”,然后就将它们组装或“集成”了。通常这是整个程序或库。
在我编写的代码中,集成测试使用各种测试程序来测试一个库,这些测试程序读取数据并将其馈送到该库,然后检查结果。然后我用线程来做。然后,我在中间使用线程和fork()进行处理。然后运行它并在2秒后杀死-9,然后启动它并检查其恢复模式。我把它弄糊涂了。我以各种方式折磨它。
所有这些都还在测试中,但是结果没有漂亮的红色/绿色显示。它要么成功,要么我探究了几千行错误代码以找出原因。
这就是您测试“真实代码”的地方。这样,但是也许您不知道应该什么时候编写单元测试。当您的测试执行您指定的所有工作时,您就完成了单元测试的编写。有时,您可能会在所有错误处理和边缘情况中失去对这一点的了解,因此您可能希望创建一个很好的测试组,该测试组只需简单地通过规范即可。
评论
(它=所有格,它=“它是”或“它具有”。例如参见如何使用它和它。)
– Peter Mortensen
17年7月27日在16:08
#11 楼
在回答问题标题:“什么时候在TDD中编写“真实”代码?”时,答案是:“几乎不会”或“非常缓慢”。学生,所以我会像建议学生一样回答。
您将学习很多编码“理论”和“技术”。它们非常适合在高价的学生课程上打发时间,但对您却几乎没有好处,因为您半个小时都无法读一本书。
编码员的工作仅仅是为了产生代码。代码效果很好。这就是为什么编码人员在头脑中,在纸上,在合适的应用程序等中计划代码的原因,并计划在编码之前通过逻辑和横向思考来预先解决可能的缺陷/漏洞。
但是您需要知道如何破坏您的应用程序才能设计出不错的代码。例如,如果您不了解Little Bobby Table(xkcd 327),那么在使用数据库之前您可能不会清理输入,因此您将无法围绕该概念保护数据。 br />
TDD只是一种工作流,旨在通过在编写应用程序之前创建可能出问题的测试来最大程度地减少代码中的错误,因为引入的代码越多,编码就会成倍增加,而您忘记的错误就越多你曾经想过。一旦您认为自己已经完成了应用程序,就可以运行测试并获得成功,希望测试中能够捕获bug。代码,编写另一个测试,以最少的代码获得通过,等等。相反,这是一种帮助您自信地进行编码的方法。不断重构代码以使其能够与测试一起使用的理想是白痴,但这在学生中是一个不错的概念,因为它使他们在添加新功能时仍感觉很好,并且仍在学习如何编写代码...
请不要落入这个陷阱,看看您的编码作用是什么-编码器的工作仅仅是产生代码。效果很好的代码。现在,请记住,您将是一名专业的编码员,这将使您全天候工作,并且您的客户端将不在乎您是否编写了100,000个断言(即0)。他们只希望代码有效。真的很好,
评论
我什至没有一个学生,但是我确实阅读并尝试应用好的技术并变得专业。所以从这个意义上说,我是一个“学生”。我只是问一些非常基本的问题,因为这就是我的方式。我想确切地知道为什么我在做什么。事件的核心。如果我不明白这一点,我将不喜欢它并开始提出问题。我需要知道为什么,如果我要使用它。 TDD在某些方面直观上不错,例如知道您需要创建什么并仔细考虑事情,但是实现很难理解。我想我现在有了更好的掌握。
–约翰尼
17年7月25日在21:34
1.除非要通过失败的单元测试,否则不允许编写任何生产代码。 2.不允许编写任何足以导致失败的单元测试;编译失败就是失败。 3.不允许编写任何足以通过一项失败的单元测试的生产代码。
–RubberDuck
17年7月27日在0:07
这些是TDD的规则。您可以随意编写代码,但是,如果不遵循这三个规则,则不会执行TDD。
– Sean Burton
17年7月27日在10:10
一个人的“规则”? TDD是帮助您编码的建议,而不是宗教信仰。这是可悲的,看到这么多的人坚持一个想法,所以肛门。甚至TDD的起源也是有争议的。
–user3791372
17年7月27日在12:14
@ user3791372 TDD是一个非常严格且明确定义的过程。即使许多人认为这仅意味着“在编程时进行一些测试”,但事实并非如此。让我们在这里不要混淆术语,这个问题是关于TDD的过程,而不是一般的测试。
– Alex
17年7月28日在9:21
评论
在TDD之后,人们回家过夜。为什么您认为编写的代码不是真实的?
@RubberDuck比其他答案更多。我相信我会尽快参考。它仍然是外国的,但我不会放弃。你说的很有道理。我只是想使其在我的上下文或常规业务应用程序中有意义。也许是库存系统或类似系统。我必须考虑一下。不过,我感谢您的宝贵时间。谢谢。
答案已经打在了头上,但是只要您所有的测试都通过了,并且您不需要任何新的测试/功能,就可以假定您已经完成了代码,这很容易。
该问题中的一个假设可能是“我有一个使用复杂方法的相当复杂的对象”。在TDD中,您首先要编写测试,所以从一个相当简单的代码开始。这将迫使您编写一个需要模块化的,易于测试的结构。因此,将通过组合更简单的对象来创建复杂的行为。如果您以相当复杂的对象或方法结尾,那么就是重构