如何使用TDD正确地实施数值计算方法?(How to use TDD correctly to im

2019-07-19 02:03发布

我试图使用测试驱动开发来实现我的信号处理库。 但我有一个小的疑问:假设我想实现一个正弦方法(我不是):

  1. 写测试(伪代码)

     assertEqual(0, sine(0)) 
  2. 写第一个实现

     function sine(radians) return 0 
  3. 二测

     assertEqual(1, sine(pi)) 

在这一点上,我应该:

  1. 实现智能的代码,将圆周率等数值工作,或
  2. 落实,将工作只为0和pi的最愚蠢的代码?

如果您选择了第二个选项,我什么时候能跳到第一个选项? 我最终还是不得不做...

Answer 1:

在这一点上,我应该:

  1. 实现真正的代码将在两个简单的测试外工作?

  2. 落实,将工作只为两个简单的测试更多的最愚蠢的代码?

都不是。 我不知道你在哪里得到了方法的“一次编写只是一个测试”,但可以肯定的是走一个缓慢的方式。

关键是要写清测试和使用,明确测试设计程序。

因此,编写足够的测试,以测试是否正弦函数。 两次测试显然是不够的。

在连续函数的情况下,你必须提供已知良好值的表也说不定。 干嘛要等?

然而,测试连续函数有一些问题。 你就不能按照一个愚蠢的TDD过程。

你可以不考0到2 * pi之间的所有浮点值。 你不能测试一些随机值。

在连续函数的情况下,“严谨,没头没脑TDD”不工作。 这里的问题是,你知道你的正弦函数的实现将基于一堆对称性。 你必须根据你使用的对称规则进行测试。 错误隐藏在缝隙和角落。 边缘案例和角案件执行工作的一部分,如果你不假思索遵循TDD你不能测试。

然而,对于连续函数,你必须在测试执行的边缘和角落的情况。

这并不意味着TDD损坏或不足。 它说的盲目崇拜“测试第一”不能没有你真正的目标是什么一些思考工作。



Answer 2:

在一种严格的婴儿步TDD的,你可以实现哑方法要回绿色,然后重构的哑代码所固有的重复(测试为输入值是一种测试和代码之间的重复的)通过产生一个真正的算法。 有关获取用于TDD的感觉与这样的算法最困难的部分是你的验收测试真的坐在你旁边的(表S.洛特建议),让你有种密切关注他们的全部时间。 更典型的TDD,单位是从验收试验不能只在插在那里,这样你就不会开始考虑测试对所有情景整个离婚不够的,因为所有的情况并不明显。

通常情况下,你可能有一两个病例后,真正的算法。 关于TDD重要的是,它是促进设计的,而不是算法。 一旦你有足够的情况下,以满足设计需要,在TDD值显著下降。 然后,测试更加转化为覆盖角落的情况下,以确保你的算法是你能想到的所有方面是正确的。 所以,如果你是如何建立的算法,去了自信。 当你不确定该种你正在谈论婴儿的步骤只适合。 通过采取这样的婴儿学步开始打造出一个什么样的代码已经覆盖到边界,即使你的代码是不是真正的呢。 但正如我所说,这是更多的时候你不能确定如何构建算法。



Answer 3:

编写验证身份的测试。

对于的sin(x)例如,考虑双角式和半角公式。

打开一个信号处理教科书。 找到相关的章节和执行这些定理/推论为适用于您的功能测试代码的每一个。 对于大多数信号处理功能存在必须被维护为输入和输出的身份。 编写验证这些身份,不管是什么这些投入可能是测试。

再想想输入。

  • 分执行过程中进入不同的阶段。 每个阶段都应该有一个目标。 每个阶段的测试将验证目标。 (注1)
    1. 第一阶段的目标是成为“大致正确”。 对于罪(x)的例子,这就像使用二进制搜索和一些数学身份的一种幼稚的做法。
    2. 第二阶段的目标是成为“不够准确”。 你会尝试计算同一功能的不同方式,看看哪一个得到更好的结果。
    3. 第三阶段的目标是成为“有效”。

(注1),使其工作,使之正确,使其快速,让它便宜。 - 归属于艾伦凯



Answer 4:

我相信当你跳到第一个选项的步骤是,当你看到有太多的“如果”在您的代码“只是为了通过测试”。 这不会是这样呢,只是0和pi。

你会感觉代码开始闻,并愿意尽快重构它。 我不知道如果这就是纯粹的TDD说,但恕我直言,你在重构阶段做(测试失败,测试通过,重整循环)。 我的意思是,除非你失败的测试要求不同的实现。



Answer 5:

你应该编写了一重击的所有单元测试(在我看来)。 虽然只有创建测试具体覆盖哪些还需要进行测试的思路是正确的,您的特定规格要求一个正常运作的sine()函数,而不是一个sine()函数,对于0和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,那么这很好。 但是,这将是在我看来,一个迭代的严重浪费。

这可能是因为你的函数是如此复杂,它必须通过多次迭代来完成。 然后你的方法有两个是正确的,测试应该在您添加额外的功能, 下一次迭代更新。 否则,找到一个方法来快速地添加所有的测试本次迭代,那么你将不必担心真正的代码和测试代码之间的切换频繁。



Answer 6:

严格按照TDD,可以先执行,将工作的最愚蠢的代码。 为了跳转到第一个选项(实现真正的代码),添加更多的测试:

assertEqual(tan(x), sin(x)/cos(x))

如果要实现比什么是你的测试绝对需要更多的,那么你的测试不会完全覆盖你的实现。 例如,如果你的执行情况总体sin()只用两个以上的测试功能,你可能会意外地返回一个三角函数(几乎看起来像一个正弦函数)“破发”,而你的测试将无法检测错误。

你将不必担心对数字函数的另一件事是“平等”和必须处理的精度浮点计算的固有损耗的概念。 这就是我你的问题将是关于阅读只是在标题后。 :)



Answer 7:

需要注意的是(在NUnit的),你也可以做

Assert.That(2.1 + 1.2, Is.EqualTo(3.3).Within(0.0005);

当你处理浮点平等。

建议一件我记得读书是尝试从实现重构出幻数。



Answer 8:

我不知道你用的是什么语言,但是当我处理的数值方法,我通常写一个简单的测试,像你第一次来确保大纲是正确的,然后我养活更多的值来涵盖我怀疑个案事情可能出错。 在.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;   
}


Answer 9:

简短的回答。

  • 写一次一个考验。
  • 一旦失败的话,你回环保先行。 如果这意味着这样做可以工作的最简单的事情,做到这一点。 (选项2)
  • 一旦你在绿色的时候,你可以看看代码,并选择清理(选项1)。 或者你可以说,代码仍然不闻到了很多,写的把目光对准气味随后的测试。

你似乎有另外一个问题,就是你应该有多少测试写。 你需要测试,直到恐惧(该功能可能无法正常工作)变成无聊。 所以,一旦你的所有有趣的输入输出组合测试,你就大功告成了。



文章来源: How to use TDD correctly to implement a numerical method?
标签: tdd numerical