我正在尝试使用测试驱动开发来实现我的信号处理库。但我有点怀疑:假设我正在尝试实现一个正弦方法(我不是):
编写测试(伪代码)
assertEqual(0, sine(0))
编写第一个实现
function sine(radians)
return 0
第二次测试
assertEqual(1, sine(pi))
此时,我应该:
如果您选择第二个选项,我什么时候可以跳转到第一个选项?我最终必须这样做......
答案 0 :(得分:9)
此时,我应该:
实现在两个简单测试之外工作的实际代码?
实现更多最愚蠢的代码,只适用于两个简单的测试?
都不是。我不确定你从哪里得到“一次只编写一个测试”的方法,但这肯定是一个缓慢的方法。
重点是编写清晰的测试并使用明确的测试来设计您的程序。
因此,编写足够的测试来实际验证正弦函数。两项测试显然不足。
对于连续函数,您必须最终提供已知良好值的表。为什么要等?
但是,测试连续函数存在一些问题。你不能遵循愚蠢的TDD程序。
您无法测试0到2 * pi之间的所有浮点值。你无法测试一些随机值。
在连续功能的情况下,“严格,不假思索的TDD”不起作用。这里的问题是你知道你的正弦函数实现将基于一堆对称性。您必须根据您正在使用的那些对称规则进行测试。虫子隐藏在裂缝和角落里。边缘情况和角落情况是实施的一部分,如果你不假思索地遵循TDD,你无法测试。
但是,对于连续函数,您必须测试实现的边缘和边角情况。
这并不意味着TDD被破坏或不足。它表示,如果不考虑一下你真正的目标是什么,那么对“先测试”的盲目奉献就无法发挥作用。
答案 1 :(得分:5)
在严格的婴儿步TDD中,您可以实现哑方法以恢复绿色,然后重构哑代码中固有的重复(测试输入值是测试之间的一种重复)代码)通过生成一个真正的算法。使用这种算法感受TDD的难点在于你的验收测试真的就在你旁边(表格S. Lott建议),所以你总是要密切注意它们。在更典型的TDD中,单元与整体分离得足以使接受测试不能直接插入到那里,因此您不会开始考虑测试所有场景,因为所有场景都不明显。
通常,在一两个案例之后,您可能会有一个真实的算法。关于TDD的重要一点是它驱动设计而不是算法。一旦有足够的案例来满足设计需求,TDD中的值就会显着下降。然后测试更多地转换为覆盖角落情况,以确保您的算法在您能想到的所有方面都是正确的。因此,如果您对如何构建算法有信心,那就去吧。你所讨论的婴儿步骤的种类只有在你不确定时才适用。通过采取这些步骤,您可以开始构建代码必须涵盖的范围,即使您的实现尚未实现。但正如我所说,当你不确定如何构建算法时,这更有用。
答案 2 :(得分:5)
编写用于验证身份的测试。
对于sin(x)示例,请考虑双角公式和半角公式。
打开信号处理教科书。查找相关章节并将这些定理/推论中的每一个实现为适用于您的函数的测试代码。对于大多数信号处理功能,必须坚持输入和输出的标识。编写验证这些身份的测试,无论这些输入是什么。
然后考虑输入。
(注1)让它工作,使其正确,使其快速,使其便宜。 - 归功于Alan Kay
答案 3 :(得分:1)
我相信跳到第一个选项的步骤是当你看到你的代码中有太多“ifs”“只是为了通过测试”。情况并非如此,只有0和pi。
你会觉得代码开始闻起来,并愿意尽快重构。我不确定这是纯TDD所说的,但恕我直言,你在重构阶段(测试失败,测试通过,重构循环)这样做。我的意思是,除非你的失败测试要求不同的实现。
答案 4 :(得分:1)
您应该在一次点击中编写所有单元测试(在我看来)。虽然只创建专门涵盖必须测试内容的测试的想法是正确的,但是您的特定规范需要一个正常运行的sine()
函数,而不是一个适用于0的sine()
函数和PI。
找到你信任的来源(数学家朋友,数学书背面的表或已经实现了正弦函数的其他程序)。
我选择了bash/bc
因为我懒得手工输入所有内容:-)。如果是 sine()
函数,我只需运行以下程序并将其粘贴到测试代码中。我也会把这个脚本的副本放在那里作为评论,所以我可以重新使用它,如果有什么变化(例如在这种情况下超过20度所需的分辨率,或者你想要的PI值)使用)。
#!/bin/bash
d=0
while [[ ${d} -le 400 ]] ; do
r=$(echo "3.141592653589 * ${d} / 180" | bc -l)
s=$(echo "s(${r})" | bc -l)
echo "assertNear(${s},sine(${r})); // ${d} deg."
d=$(expr ${d} + 20)
done
输出:
assertNear(0,sine(0)); // 0 deg.
assertNear(.34202014332558591077,sine(.34906585039877777777)); // 20 deg.
assertNear(.64278760968640429167,sine(.69813170079755555555)); // 40 deg.
assertNear(.86602540378430644035,sine(1.04719755119633333333)); // 60 deg.
assertNear(.98480775301214683962,sine(1.39626340159511111111)); // 80 deg.
assertNear(.98480775301228458404,sine(1.74532925199388888888)); // 100 deg.
assertNear(.86602540378470305958,sine(2.09439510239266666666)); // 120 deg.
assertNear(.64278760968701194759,sine(2.44346095279144444444)); // 140 deg.
assertNear(.34202014332633131111,sine(2.79252680319022222222)); // 160 deg.
assertNear(.00000000000079323846,sine(3.14159265358900000000)); // 180 deg.
assertNear(-.34202014332484051044,sine(3.49065850398777777777)); // 200 deg.
assertNear(-.64278760968579663575,sine(3.83972435438655555555)); // 220 deg.
assertNear(-.86602540378390982112,sine(4.18879020478533333333)); // 240 deg.
assertNear(-.98480775301200909521,sine(4.53785605518411111111)); // 260 deg.
assertNear(-.98480775301242232845,sine(4.88692190558288888888)); // 280 deg.
assertNear(-.86602540378509967881,sine(5.23598775598166666666)); // 300 deg.
assertNear(-.64278760968761960351,sine(5.58505360638044444444)); // 320 deg.
assertNear(-.34202014332707671144,sine(5.93411945677922222222)); // 340 deg.
assertNear(-.00000000000158647692,sine(6.28318530717800000000)); // 360 deg.
assertNear(.34202014332409511011,sine(6.63225115757677777777)); // 380 deg.
assertNear(.64278760968518897983,sine(6.98131700797555555555)); // 400 deg.
显然,您需要将此答案映射到您的真实功能的目的。我的观点是测试应该完全验证此迭代中代码的行为。如果这个迭代产生的sine()
函数仅适用于0和PI,那就没问题。但在我看来,这将是对迭代的严重浪费。
可能是你的功能太复杂了,必须在几次迭代中完成。然后你的方法二是正确的,测试应该在 next 迭代中更新,你可以在其中添加额外的功能。否则,找到一种方法快速添加此迭代的所有测试,然后您不必担心经常在实际代码和测试代码之间切换。
答案 5 :(得分:0)
严格遵循TDD,您可以先实现最有效的最完整代码。为了跳转到第一个选项(实现真实代码),添加更多测试:
assertEqual(tan(x), sin(x)/cos(x))
如果您实施的测试超出了测试的绝对要求,那么您的测试将无法完全涵盖您的实施。例如,如果你只使用上面的两个测试来实现整个sin()
函数,你可能会通过返回一个三角函数(几乎看起来像一个正弦函数)意外“破坏”它,你的测试将无法检测错误。
对于数值函数,您将不得不担心的另一件事是“相等”的概念,并且必须处理浮点计算中固有的精度损失。这就是我想到的在阅读标题后你的问题将会是什么。 :)
答案 6 :(得分:0)
请注意(在NUnit中)您也可以
Assert.That(2.1 + 1.2, Is.EqualTo(3.3).Within(0.0005);
当你处理浮点相等时。
我记得读过的一条建议就是尝试重构你实施中的神奇数字。
答案 7 :(得分:0)
我不知道你使用的语言是什么,但是当我处理数字方法时,我通常会先编写一个像你一样的简单测试,以确保大纲正确,然后我提供更多的值来覆盖案例我怀疑事情可能会出错。在.NET中,NUnit 2.5有一个很好的功能,称为[TestCase]
,你可以在这里将多个输入值提供给同一个测试:
[TestCase(1,2,Result=3)]
[TestCase(1,1,Result=2)]
public int CheckAddition(int a, int b)
{
return a+b;
}
答案 8 :(得分:0)
简短回答。
您似乎遇到的另一个问题是,您应该编写多少测试。你需要测试直到恐惧(功能可能不起作用)变成无聊。所以,一旦你测试了所有有趣的输入输出组合,你就完成了。