最近我正在编写一些对象,其中一个方法的行为有时在特定条件下调用其自己的另一个方法。为了测试这个,我一直在模拟对象调用自己的方法,因为这样可以更容易地覆盖代码中的每个分支,而不会出现组合爆炸。这通常被认为是不好的实践。但是这里有一个例子,几乎完全反映了我现在正在工作的代码(使用CoffeeScript编写,但这个哲学问题在其他语言中也曾经出现过)。
这很简单。fetchUsernameAsync和fetchUserCommentsAsync分别与给定的@dataSource交互以获取数据。然后我们有fetchUsernameAndUserCommentsAsync,它在某种程度上是一个组合方法,调用其他两个方法并以特定方式处理它们; 在这个例子中,它可能希望重新引发由fetchUsernameAsync引发的任何错误,但是忽略由fetchUserCommentsAsync引发的任何错误。
测试前两个方法的行为很简单。我模拟它们对@dataSource的各自调用,并断言它们以正确的格式返回数据或允许引发任何错误。但是,我遇到的困境是如何测试最后一个方法的行为。
它仍然是一个简单的方法,具有最小的分支逻辑,并通过传递消息委派大部分工作。通常,我会通过模拟其“协作者”来测试它的行为,并断言传递了某些消息并以适当的格式返回数据或正确失败。但是,这里的区别在于它只有一个“协作者”,即this。我将模拟被测试对象的一些方法,这被认为是不好的实践。
显然,解决这个问题的方法是将此方法移动到另一个对象中;它将是一个具有恰好一个方法和几乎完全相同机制的对象,并且它将允许我模拟其协作者,而不违反“不要在测试对象上模拟方法”的规则。它可能会有一个荒谬的名称,如
class UserDataFetcher
constructor: (@dataSource) ->
fetchUsernameAsync: ->
# ... snip ... Hits API and raises any errors
fetchUserCommentsAsync: ->
# ... snip ... Hits API and raises any errors
fetchUsernameAndUserCommentsAsync: ->
# ... snip ... Calls each of the above in order and handles errors specially
这很简单。fetchUsernameAsync和fetchUserCommentsAsync分别与给定的@dataSource交互以获取数据。然后我们有fetchUsernameAndUserCommentsAsync,它在某种程度上是一个组合方法,调用其他两个方法并以特定方式处理它们; 在这个例子中,它可能希望重新引发由fetchUsernameAsync引发的任何错误,但是忽略由fetchUserCommentsAsync引发的任何错误。
测试前两个方法的行为很简单。我模拟它们对@dataSource的各自调用,并断言它们以正确的格式返回数据或允许引发任何错误。但是,我遇到的困境是如何测试最后一个方法的行为。
它仍然是一个简单的方法,具有最小的分支逻辑,并通过传递消息委派大部分工作。通常,我会通过模拟其“协作者”来测试它的行为,并断言传递了某些消息并以适当的格式返回数据或正确失败。但是,这里的区别在于它只有一个“协作者”,即this。我将模拟被测试对象的一些方法,这被认为是不好的实践。
显然,解决这个问题的方法是将此方法移动到另一个对象中;它将是一个具有恰好一个方法和几乎完全相同机制的对象,并且它将允许我模拟其协作者,而不违反“不要在测试对象上模拟方法”的规则。它可能会有一个荒谬的名称,如
UsernameAndCommentsFetcher
,我将从一个小类转变为两个微小的、几乎微不足道的类。小类是好的,但这种粒度可能有点过度设计,在这种情况下只是为了满足“不要在测试对象上模拟方法”的规则。
这是一个可靠的规则,还是像这样的情况下弯曲规则是合理的?
(注:我意识到这与这个问题相似,但我认为例子不同,值得再看一遍:作为“mockist”TDD实践者,我应该在测试方法所在的同一类中模拟其他方法吗?)