7

好的,所以这可能是一个危险的问题。我已经做了一段时间的单元测试,但是由于某种原因,我今天早上醒来,问了自己这个问题。

假设我有一个 UserFactory 接口,它有 CreateUser 方法。

在某些时候我需要创建一个用户权利?因此,我创建了一个测试,检查是否在适当的位置为 UserFactory 调用了 CreateUser。

现在,单元测试与实际代码非常耦合——这很好。但也许有点太多了?如中所示,打破测试的唯一方法是不调用 CreateUser。我们没有检查它的实现等,而只是检查接口是否被调用。但是,无论谁删除了该调用,都会有一个失败的测试,并最终从验证 CreateUser 被调用的步骤中删除验证语句。

我已经看到这种情况一遍又一遍地发生。

有人可以把灯带回给我并解释为什么验证模拟对象的方法已被调用是有益的吗?我可以理解为什么设置它们可能很有用,比如 CreateUser 应该为代码的后面部分返回一个虚拟用户,但是在我们简单且仅验证它们是否被调用的地方是让我着迷的部分。

谢谢!

4

4 回答 4

2

您不仅可以验证接口是否已被调用,还可以针对接口的不同行为进行多次测试。尤其是极端情况——当 CreateUser返回错误引发异常时,您的代码是否会优雅地故障转移?

于 2012-08-12T18:15:59.087 回答
2

打破测试的唯一方法是不调用 CreateUser

因此,在具有一些复杂性、一些条件等的方法中,跳过该调用可能很容易;以后的维护可能会无意中导致错过呼叫。

所以我认为这些副作用测试是有价值的。在您的情况下,是否应该始终调用 CreateUser?抛出异常时怎么办?在某些情况下检查 CreateUser 是否未被调用可能会有一些价值。

我同意你的观点,在简单的情况下,有时会感觉我们的测试和输出代码或多或少地重复自己,维护变成了一种无意识的“更改代码,更改测试”活动。我认为当有更多路径和错误处理时,价值会变得更加清晰。

于 2012-08-12T18:18:22.030 回答
2

验证模拟对象通常是必要的,正如您所提到的,单元测试有时与被测类紧密耦合。

我不知道如何对你的问题给出一个好的答案,但我会尝试的。

以这段代码为例,其中 userRepository 是一个依赖项(示例不是很好)。

public void doSomething(User user) {
    if( user.isValid() ) {
        userRepository.save(user)
    } else {
        user.invalidate();
    }
}

对此进行测试的一种方法是插入连接到数据库的真实存储库,并验证用户是否被持久化。但是由于我是单元测试,所以我的测试中不能有外部依赖。

现在,您还需要哪些其他选项来验证用户有效的场景?由于 userRepository.save() 返回 void,因此验证副作用的唯一方法是验证是否调用了 mock。如果我不验证模拟,那么单元测试就不会很好,因为我可以删除保存对象的行,并且测试仍然会通过。

对于一些返回值的模拟,情况并非如此,然后在方法中使用了值。这通常意味着如果模拟返回 null,则应用程序会抛出 NullPointerException(在 java 的情况下)。

于 2012-08-12T18:25:58.927 回答
1

了解依赖项在测试代码中的用途很重要:

  • 帮助履行班级合同/责任(如您的UserFactory示例)
  • 允许委派超出类范围但必须完成的工作(例如,日志功能可能就是这种情况)

现在,如果依赖(模拟)不能帮助,显然依赖于这个帮助的测试代码也会失败。在这种情况下,进行额外的测试来验证是否调用了依赖项并不是很有用。任何调用都不会导致依赖于它并带有明确消息的测试中的多次失败。

回到你的工厂例子,当它没有被调用时会发生什么?其他一些测试,可能是那些验证该用户是否被保存在某个地方,或者用它制作的东西会失败。

自然有第二组依赖项,它们根本不影响您的代码,而是在后台静默执行。但是,这些很可能会反映在您的代码职责中,例如:

SaveUser 方法应将新用户保存在存储库中并记录操作结果

通常,除了验证是否调用了适当的方法之外,没有其他方法可以进行行为检查。

作为结论,在确定是否应该编写测试时要考虑两个问题:

  • 这个测试会验证我班级的(部分)责任吗?
  • 当此测试通过/失败时,我将获得关于测试代码的哪些知识?

除非有必要,否则不要测试你的代码是否调用了其他代码;测试您的代码是否以您认为的方式工作

于 2012-08-12T18:49:57.610 回答