如何应用功能复杂的TDD?



Uncle Bob 的测试驱动开发的三条规则如下:

  1. 不允许编写任何生产代码,除非要使失败的单元测试通过。

  2. 不允许编写超过足以失败的单元测试;编译失败就是失败。

  3. 您不能编写超过足以通过一个失败单元测试的生产代码。

例如,在现实世界中,我有一个任务来完成一个需要一些复杂算法(例如一些高级数学知识(的方法,如果我应用上述 3 条规则,它将是这样的:

  • 开始编写测试用例 =>运行并看到它失败

  • 编写足以使测试通过的代码行

  • 重构代码

  • 然后重复此循环。

我的问题是:在这种情况下,这种方法是否非常不现实和分散注意力?

为什么我们不编写第一个测试用例,然后专注于寻找解决方案然后实现它,我的意思是着眼于大局,而不是编写足够的代码来通过第一个测试用例?

我认为另一个答案非常好,但除此之外:如果一种方法需要您实现多种算法 - 您确定这种方法尊重单一责任原则吗?听起来这种方法做的事情太多了。

因此,当您退后一步并看到使用了 3 种算法时 - 这已经告诉我们每种算法可能应该进入自己的方法。而且我们的"初始"方法只会调用这些其他方法来完成一些计算。

并朝着其他方向发展 - 没有法律可以阻止您根据自己的需求调整TDD。这导致了一种我称之为"自下而上的TDD"的做法。

它是这样的:而不是首先为我的一个巨大的方法编写一个测试 - 我实际上考虑了我在这个巨大的方法中需要的不同部分。所以我只写下第一部分的测试;然后实施它。我对所有部分都这样做。随着时间的推移,这些部分得到了增强,也许我会在途中将较小的部分合并成更大的部分(这很可能意味着将多个测试合并为一个更大的测试(。

这种技术可能意味着你最终可能会为你的大型方法得到一个测试用例 - 但你实际上使用TDD来测试小部件,同时构建"大解决方案"。

换句话说:我不是为该方法的公共契约编写一个大的函数测试,而是从为我知道我需要的小辅助方法编写测试开始。最后,这些帮助程序将是私有方法 - 直接测试它们是没有意义的。但是你可以保留早期测试中那些在你的大型公共方法的上下文中有意义的部分。

长话短说:所有这些技巧都是为了指导你找到自己的方式。鉴于您有足够的经验进行即时设计,以这种方式使用TDD是可能的(实际上很有趣(!

根据我对TDD的经验,执行此小步骤具有一些优点:

让开发人员思考应该如何使用代码

一个可以理解的方法名称,输入(我将从文件,数组中读取(,输出(将是json,ArrayList(以及每个特定输入的行为(引发异常,什么都不做(。

测试反馈更精确

执行婴儿步骤可帮助您专注于如何处理算法中的特定情况。例如,假设一个带有快速排序算法的 TDD:

def "Given an empty list, when I use quicksort, should raise an exception"()
def "Given an list of one elements, should return the list itself"()
def "Given an ordered list of elements, should return the list itself"()
def "Given an unordered list of elements, should return a copy of ordered list"()

请注意,测试本身应该非常精确,并且详细地说明了您要测试的行为。 一个不太好的测试可能是这样的

def "Given an list, should give me an ordered list"()

如果上面的测试失败,它失败了,因为没有引发异常?或者列表已排序但更改了原始列表?

您的测试代码成为规范

如果你对算法进行一些广泛的测试代码,你可能会隐藏算法的一些重要细节。所有重要的代码都必须有一个写得很好的测试用例。这样做,您最终将得到一个测试代码,该代码基本上是算法的规范。 如果其他开发人员问你算法应该如何工作,只需向他展示测试用例即可。

我一开始的想法和你一样。感觉我们在浪费时间,做这些婴儿步骤似乎很荒谬。但相信我,你练习得越多,你就越能意识到TDD的好处。

最新更新