当一个产品功能有数百万个测试用例时,TDD是如何工作的
本文关键字:TDD 工作 测试用例 何工作 数百万 一个 功能 百万个 | 更新日期: 2023-09-27 18:13:36
在TDD中,您选择一个测试用例并实现该测试用例,然后编写足够的产品代码,以便测试通过,重构代码,然后再次选择一个新的测试用例,循环继续。
我在这个过程中遇到的问题是,TDD说你只需要编写足够的代码来通过你刚刚编写的测试。我确切指的是,如果一个方法可以有100万个测试用例,你能做什么?显然没有写一百万个测试用例?!
让我用下面的例子更清楚地解释我的意思:
internal static List<long> GetPrimeFactors(ulong number)
{
var result = new List<ulong>();
while (number % 2 == 0)
{
result.Add(2);
number = number / 2;
}
var divisor = 3;
while (divisor <= number)
{
if (number % divisor == 0)
{
result.Add(divisor);
number = number / divisor;
}
else
{
divisor += 2;
}
}
return result;
}
上面的代码返回给定数字的所有素数因子。Ulong有64位,这意味着它可以接受0到18,446,744,073,709,551,615之间的值!
那么,当一个产品功能有数百万个测试用例时,TDD是如何工作的呢?
我的意思是有多少测试用例足以让我说我使用了TDD来实现这个产品代码?
在TDD中,你应该只写足够的代码来通过你的测试,这个概念对我来说似乎是错误的,可以从上面的例子中看到。
适可而止?
我自己的想法是,我只选择了一些测试用例,例如上层带,下层带和更多的,例如5个测试用例,但这不是TDD,是吗?
非常感谢您对这个示例的TDD的想法。
这是一个有趣的问题,与认识论中的可证伪性有关。使用单元测试,你并不是真的要证明系统是有效的;你正在构建实验,如果它们失败了,将证明系统的工作方式与你的期望/信念不一致。如果你的测试通过了,你并不知道你的系统是否正常工作,因为你可能忘记了一些没有经过测试的边缘情况;你所知道的是,到目前为止,你没有理由相信你的系统有问题。
科学史上的经典例子是"所有的天鹅都是白色的吗?"不管你找到多少只不同的白天鹅,你都不能说"所有的天鹅都是白的"这个假设是正确的。另一方面,给我带来一只黑天鹅,我就知道这个假设是不正确的。一个好的TDD单元测试应该是这样的;如果它通过了,它不会告诉你一切都是正确的,但如果它失败了,它会告诉你你的假设哪里不正确。在这种情况下,对每个数字进行测试并不是那么有价值:一种情况应该是足够的,因为如果在这种情况下不起作用,那么您就知道出了问题。
问题的有趣之处在于,不像天鹅,你不能枚举世界上的每一只天鹅,以及它们未来的孩子和父母,你可以枚举每一个整数,这是一个有限集合,并验证每一种可能的情况。此外,程序在很多方面更接近于数学而不是物理,在某些情况下,您还可以真正验证语句是否为真——但在我看来,这种验证类型并不是TDD所追求的。TDD追求的是好的实验,其目的是捕获可能的失败案例,而不是证明某些事情是正确的。
你忘记了第三步:
- 红色绿色
编写测试用例会让您陷入困境。
编写足够的代码使这些测试用例通过,可以让您获得绿色。
将你的代码泛化,使其不仅仅适用于你所写的测试用例,同时又不破坏其中任何一个,这就是重构。
您似乎将TDD视为黑盒测试。它不是。如果是黑盒测试,那么只有一个完整的(数百万个测试用例)测试集才能满足您,因为任何给定的用例都可能未经过测试,因此黑盒中的恶魔将能够逃脱欺骗。
但它并不是你代码里黑盒子里的恶魔。是你,装在白盒子里。你知道自己是否在作弊。"Fake It until You Make It"的实践与TDD密切相关,有时会混淆。是的,您编写虚假的实现来满足早期的测试用例——但是您知道您在伪造它。你也知道什么时候你不再装了。你知道什么时候你有了一个真正的实现,并且你已经通过渐进迭代和测试驱动到达了那里。
所以你的问题真的放错地方了。对于TDD,您需要编写足够的测试用例来驱动您的解决方案的完成和正确性;您不需要对每一组可能的输入都使用测试用例。
从我的观点来看,重构步骤似乎没有发生在这段代码上…
在我的书中,TDD并不意味着为每一个可能的输入/输出参数的每一个可能的排列编写测试用例…
但是要写所有的测试用例,以确保它做了它被指定要做的事情,即对于这样一个方法,所有边界用例加上一个测试,从包含已知正确结果的数字列表中随机选择一个数字。如果需要,您可以随时扩展此列表以使测试更彻底…
TDD只有在你不抛弃常识的情况下才能在现实世界中发挥作用…
to
在TDD中,只写足够的代码来通过测试
指的是"不作弊的程序员"…如果你有一个或多个"作弊程序员",例如,他们只是硬编码测试用例的"正确结果"到方法中,我怀疑你手上有一个比TDD更大的问题…
顺便说一句,"测试用例构建"是你练习得越多越擅长的东西——没有任何一本书/指南可以告诉你哪个测试用例最适合任何给定的情况……当涉及到构建测试用例时,经验的回报是巨大的……
如果您愿意,TDD确实允许您使用常识。定义你的TDD版本是愚蠢的是没有意义的,这样你就可以说"我们不是在做TDD,我们在做一些不那么愚蠢的事情"。
您可以编写一个单独的测试用例,多次调用被测函数,传递不同的参数。这可以防止"编写代码来分解1","编写代码来分解2","编写代码来分解3"成为单独的开发任务。
要测试多少不同的值实际上取决于运行测试所需的时间。你想测试任何可能是极端情况的东西(所以在分解的情况下,至少有0,1,2,3,LONG_MAX+1
,因为它有最多的因子,无论哪个值有最多的不同的因子,一个卡迈克尔数,以及一些具有各种素数因子的完全平方数)加上尽可能大的值范围,希望涵盖一些你没有意识到是极端情况的东西,但它是。这很可能意味着编写测试,然后编写函数,然后根据观察到的性能调整范围的大小。
您还可以阅读函数规范,并实现函数,就好像要测试的值比实际要测试的值多。这与"只执行已测试的内容"并不矛盾,它只是承认在发布日期之前没有足够的时间来运行所有2^64个可能的输入,所以实际测试是"逻辑"测试的代表性样本,如果你有时间,你会运行它。您仍然可以编写您想要测试的代码,而不是您实际上有时间测试的代码。
您甚至可以测试随机选择的输入(通常作为安全分析师"模糊测试"的一部分),如果您发现您的程序员(即您自己)被确定为不正常的,并继续编写只解决测试输入的代码,而不是其他的。显然,随机测试的可重复性存在问题,所以使用PRNG并记录种子。你可以在竞赛节目、在线裁判程序等类似的项目中看到类似的事情,以防止作弊。程序员并不确切地知道要测试哪些输入,因此必须尝试编写解决所有可能输入的代码。因为你无法对自己保密,所以随机输入也能起到同样的作用。在现实生活中,使用TDD的程序员不会故意作弊,但可能会因为同一个人编写测试和代码而意外作弊。有趣的是,测试忽略了代码所做的同样困难的极端情况。
对于接受字符串输入的函数,问题更加明显,有远远超过2^64
可能的测试值。选择最好的,也就是程序员最有可能出错的,充其量是一门不精确的科学。
你也可以让测试员作弊,超越TDD。首先编写测试,然后编写代码以通过测试,然后回去编写更多的白盒测试,其中包括(a)在实际编写的实现中看起来可能是边缘情况的值;(b)包含足够的值以获得100%的代码覆盖率,无论您有时间和意志去实现什么代码覆盖率度量。开发过程的TDD部分仍然有用,它有助于编写代码,但随后需要进行迭代。如果这些新测试中的任何一个失败,您可以称之为"添加新需求",在这种情况下,我认为您所做的仍然是纯粹的TDD。但这仅仅是一个你如何称呼它的问题,实际上你并没有添加新的需求,你是在比编写代码之前更彻底地测试原始需求。
当您编写测试时,您应该使用有意义的用例,而不是所有用例。有意义的案例包括一般案例、特殊案例……
你不能为每一个单独的情况写一个测试(否则你可以把值放在一个表上并回答它们,这样你就可以100%确定你的程序会工作:p)。
希望有帮助。
这是任何测试的第一个问题。TDD在这里不重要。
是的,有很多很多的情况;此外,如果您开始构建系统,还存在各种情况的组合和组合。它确实会把你引向组合爆炸。
这是个好问题。通常,您选择等价类,您的算法可能对其工作相同-并为每个类测试一个值。
下一步是,测试边界条件(记住,CS中最常见的两个错误是一个错误)。
下一个……好吧,考虑到各种实际原因,就到这里吧。不过,看看这些课堂笔记:http://www.scs.stanford.edu/11au-cs240h/notes/testing.html
p。顺便说一下,在数学问题中"按书"使用TDD并不是一个很好的主意。Kent Beck在他的TDD书中证明了这一点,实现了计算斐波那契数的函数的最糟糕的实现。如果你知道一个封闭的表单——或者有一篇文章描述了一个已被证明的算法,那么就像上面描述的那样进行完整性检查,而不要在整个重构周期中使用TDD——这会节省你的时间。
pp。实际上,有一篇很棒的文章(令人惊讶!)提到了斐波那契问题和TDD问题。
没有上百万的测试用例。只有几个。您可能会喜欢尝试PEX,它可以让您找出算法中不同的真实的测试用例。当然,您只需要测试这些。
我从来没有做过任何TDD,但是你问的不是关于TDD:而是关于如何编写一个好的测试套件。
我喜欢设计模型(在纸上或在我的脑海中)每个代码片段可能处于的所有状态。我把每一行都看作是状态机的一部分。对于每一行,我确定可以进行的所有转换(执行下一行,分支或不分支,抛出异常,溢出表达式中的任何子计算,等等)。
从那里我得到了我的测试用例的基本矩阵。然后确定每个状态转换的边界条件,以及每个边界之间有趣的中点。然后我得到了我的测试用例的变化。
从这里开始,我尝试想出有趣的和不同的流或逻辑组合-"这个if语句,加上那个-在列表中有多个项目",等等。
由于代码是一个流,除非为一个不相关的类插入一个mock是有意义的,否则您通常不能在中间中断它。在这些情况下,我经常对我的矩阵进行相当大的简化,因为有些条件你就是不能满足,或者因为变化被另一段逻辑掩盖而变得不那么有趣。
之后,我大概累了一天,然后回家:)我可能有大约10-20个测试用例,每个良好分解和合理简短的方法,或50-100个算法/类。而不是10000000年。
我可能会提出太多无趣的测试用例,但至少我通常是过度测试而不是测试不足。我通过尝试将我的测试用例很好地分解以避免代码重复来减轻这种情况。
关键字:
- 建模你的算法/对象/代码,至少在你的头脑中。你的代码更像是一个树而不是一个脚本
- 详尽地确定该模型中的所有状态转换(可以独立执行的每个操作,以及每个表达式的每个部分都得到评估)
- 利用边界测试,这样你就不必想出无限的变化
- 当你可以模仿
不,你不需要写FSM的图纸,除非你喜欢这样做。我没有:)
你通常做的是,它测试"测试边界条件",以及一些随机条件。
例如:ulong。分钟,ulong。最大值和一些值。你为什么要制作GetPrimeFactors?你喜欢一般地计算它们,还是做一些特殊的事情?测试一下你为什么要这么做。
你也可以这样做Assert for result。计数,而不是所有单独的项目。如果你知道你应该得到多少项,以及一些特定的情况,你仍然可以重构你的代码,如果这些情况和总数是相同的,假设函数仍然有效。
如果你真的想测试那么多,你也可以看看白盒测试。例如Pex and mole就很不错。
TDD不是一种检查函数/程序在每种可能的输入排列上是否正确工作的方法。我的看法是,我编写特定测试用例的概率与我在这种情况下代码是否正确的不确定性成正比。
这基本上意味着我在两种情况下编写测试:1)我编写的一些代码很复杂和/或有太多的假设,2)在生产中发生了错误。
一旦您了解了导致bug的原因,通常在测试用例中编写代码就很容易了。从长远来看,这样做会产生一个健壮的测试套件。