监视受过测试的班级是不好的做法吗?


14

我正在一个项目中进行类内部调用,但是结果是简单值的很多倍。示例(不是真实的代码):

public boolean findError(Set<Thing1> set1, Set<Thing2> set2) {
  if (!checkFirstCondition(set1, set2)) {
    return false;
  }
  if (!checkSecondCondition(set1, set2)) {
    return false;
  }
  return true;
}

为这种类型的代码编写单元测试真的很困难,因为我只想测试条件系统而不是实际条件的实现。(我在单独的测试中这样做。)实际上,如果我传递实现条件的函数会更好,而在测试中我只提供一些模拟即可。这种方法的问题在于嘈杂:我们大量使用泛型

一个可行的解决方案;但是,这是使被测对象成为间谍并模拟对内部函数的调用。

systemUnderTest = Mockito.spy(systemUnderTest);
doReturn(true).when(systemUnderTest).checkFirstCondition(....);

这里的关注点是有效更改了SUT的实现,并且使测试与实现保持同步可能会出现问题。这是真的?是否有最佳实践来避免这种内部方法调用的麻烦?

请注意,我们正在谈论的是算法的各个部分,因此将其分解为多个类可能不是一个理想的决定。

Answers:


15

单元测试应将其测试的类视为黑盒。唯一重要的是它的公共方法以预期的方式运行。类如何通过内部状态和私有方法实现此目标无关紧要。

当您认为无法以这种方式创建有意义的测试时,则表明您的类过于强大且执行了太多操作。您应该考虑将其某些功能移到可以单独测试的单独的类上。


1
我很早就掌握了单元测试的想法,并成功地编写了很多。只是在欺骗,看起来在纸上看起来很简单,在代码上看起来更糟,最后我遇到的是一个界面非常简单的东西,但是我需要模拟输入的一半。
allprog 2013年

@allprog当您需要进行大量模拟时,您的类之间似乎有太多的依赖关系。您是否尝试减少它们之间的耦合?
菲利普

@allprog如果您遇到这种情况,则应归咎于类设计。
itsbruce 2013年

导致头痛的是数据模型。它必须遵守ORM规则和许多其他要求。使用纯业务逻辑和无状态代码,正确进行单元测试要容易得多。
allprog 2013年

3
单元测试不一定需要将SUT作为backbox处理。这就是为什么它们被称为单元测试的原因。通过模拟依赖项,我可以影响环境并知道我必须模拟的内容,我还必须了解一些内部信息。但这当然并不意味着应该以任何方式更改SUT。但是,间谍活动可以进行一些更改。
allprog 2013年

4

如果findError()checkFirstCondition()等都是类的公共方法,则findError()实际上是同一API已有功能的外观。这样做没有错,但是这意味着您必须为此编写与现有测试非常相似的测试。此重复仅反映您公共接口中的重复。没有理由将此方法与其他方法区别对待。


内部方法之所以公开,是因为它们需要可测试的,并且我不想将SUT子类化或将单元测试作为静态内部类包含在SUT类中。但我明白你的意思。但是,我找不到避免此类情况的良好指南。教程始终停留在与真实软件无关的基本级别上。否则,进行间谍活动的原因恰恰是避免重复测试代码并使测试单元成为作用域。
allprog 2013年

3
我不同意辅助方法必须公开以进行正确的单元测试。如果一个方法的合同规定它检查各种条件,那么针对同一个公共方法编写多个测试就没有错,每个“分包合同”都要进行一个测试。单元测试的重点是要覆盖所有代码,而不是通过1:1方法与测试的对应关系来实现对公共方法的肤浅覆盖。
Kilian Foth,2013年

仅使用公共API进行测试要比逐个测试内部组件复杂得多。我并不争辩,我认为这种方法不是最好的,我的问题表明它具有其后见之明。最大的问题是函数在Java中不可组合,并且解决方法非常简洁。但是似乎没有其他解决方案可以用于实际的单元测试。
allprog

4

单元测试应测试合同;对他们来说,这是唯一重要的事情。测试不属于合同的任何内容不仅浪费时间,而且是潜在的错误来源。每当您看到开发人员在更改实施细节时更改测试时,都会响起警钟。开发人员可能(无论有意还是无意)隐藏了自己的错误。 故意测试实现细节会加剧这种不良习惯,从而更容易掩盖错误。

内部调用是一个实现细节,仅应在衡量性能时感兴趣。通常这不是单元测试的工作。


听起来不错。但是实际上,我必须输入并称其为代码的“字符串”是使用一种对函数了解很少的语言。从理论上讲,我可以轻松地描述问题并在此处和那里进行替换以简化该问题。在代码中,我必须添加很多语法噪声以实现这种灵活性,从而使我无法使用它。如果method 在同一类中a包含对方法的调用b,则的测试a必须包括的测试b。只要b不将其传递给a参数,就没有办法更改它,但是我没有其他解决方案。
allprog 2013年

1
如果它b是公共接口的一部分,则无论如何都应该对其进行测试。如果不是,则无需测试。如果只是因为要测试而将其公开,则您做错了。
itsbruce

请参阅我对@Philip的回答的评论。我还没有提到,但是数据模型是邪恶的源头。纯粹的无状态代码是小菜一碟。
2013年

2

首先,我想知道您编写的示例函数有哪些难以测试的地方?据我所知,您可以简单地传递各种输入并检查以确保返回正确的布尔值。我想念什么?

对于间谍而言,使用间谍和模拟的所谓“白盒”测试的编写工作量增加了几个数量级,不仅因为要编写的测试代码太多,而且任何时候实现更改后,还必须更改测试(即使接口保持不变)。而且这种测试还不如黑盒测试可靠,因为您需要确保所有额外的测试代码都是正确的,并且您可以放心,如果黑盒单元测试与接口不匹配,它将失败。 ,您不能相信代码会过度使用模拟,因为有时测试甚至不会测试很多真实的代码-仅模拟。如果模拟不正确,则很有可能是您的测试将成功,但是您的代码仍然被破坏。

任何有白盒测试经验的人都可以告诉您,编写和维护它们很麻烦。加上它们可靠性较低的事实,白盒测试在大多数情况下简直逊色。


感谢您的来信。该示例函数比您在复杂算法中编写的任何内容都要简单几个数量级。实际上,问题实际上更像是:用多个部分的间谍测试算法是否有问题。这不是有状态代码,所有状态都分为输入参数。问题是我想在示例中测试复杂功能,而不必为子功能提供合理的参数。
allprog 2014年

随着Java 8中的函数式编程的兴起,这已经变得稍微优雅一些​​,但是在算法的情况下,将功能保持在单个类中可能是更好的选择,而不是将不同的(单独不有用的)部分提取为“使用一次”类只是出于可测试性。在这方面,间谍的行为与模拟相同,但不必在视觉上炸毁连贯的代码。实际上,与模拟程序使用相同的设置代码。我喜欢远离极端,每种类型的测试可能在特定的地方都适用。以某种方式进行测试比没有方式要好得多。:)
allprog 2014年

“我想测试复杂的功能..而不必为子功能提供理智的参数”-我不明白您的意思。哪些子功能?您是否在谈论“复杂功能”正在使用的内部功能?
BT

在我看来,这就是间谍活动的用处。内部功能很难控制。不是因为代码,而是因为它们实现了逻辑上复杂的东西。将材料移到另一个类中是很自然的选择,但仅这些功能根本没有用。因此,将类保持在一起并通过间谍功能对其进行控制是一个更好的选择。完美地工作了将近一年,并且可以轻松承受模型更改。从那时起,我就再也没有使用过这种模式,只是想好说它在某些情况下是可行的。
allprog 2014年

@allprog“逻辑上复杂”-如果复杂,则需要复杂测试。没有办法解决。间谍只会使您变得更加困难和复杂。您应该创建可以单独测试的可理解子功能,而不是使用间谍来测试其在另一个功能中的特殊行为。
英国电信
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.