如何测试难以模拟对象的系统?


34

我正在使用以下系统:

Network Data Feed -> Third Party Nio Library -> My Objects via adapter pattern

最近,我们遇到了一个问题,即我更新了所使用的库的版本,这尤其导致时间戳(第三方库返回该时间戳long)从时期后的毫秒数更改为时期后的毫微秒。

问题:

如果编写模拟第三方库对象的测试,则如果我对第三方库的对象犯了错误,则我的测试将是错误的。例如,我没有意识到时间戳会改变精度,这导致需要更改单元测试,因为我的模拟返回了错误的数据。这不是库中的错误,它的发生是因为我错过了文档中的某些内容。

问题是,我无法确定这些数据结构中包含的数据,因为如果没有真实的数据馈送,将无法生成真实的数据。这些对象又大又复杂,并且其中包含许多不同的数据。第三方库的文档很差。

问题:

如何设置测试以测试此行为?我不确定我可以在单元测试中解决此问题,因为测试本身很容易出错。另外,集成系统又大又复杂,容易遗漏一些东西。例如,在上述情况下,我已经在几个地方正确地调整了时间戳记处理,但是我错过了其中之一。在我的集成测试中,该系统似乎在做正确的事情,但是当我将其部署到生产环境(具有大量数据)时,问题变得很明显。

我目前没有集成测试过程。测试本质上是:尝试保持单元测试良好,在出现问题时添加更多测试,然后部署到我的测试服务器并确保一切正常,然后部署到生产中。这个时间戳问题通过了单元测试,因为模拟创建错误,然后通过了集成测试,因为它没有引起任何直接的,明显的问题。我没有质量检查部门。


3
您能否“记录”真实的数据供稿,然后稍后将其“播放”到第三方库中?
伊丹·阿里

2
有人可以就这样的问题写一本书。实际上,迈克尔·费瑟斯(Michael Feathers)确实写过那本书:c2.com/cgi/wiki
cbojar 2015年

2
第三方库周围的适配器?是的,这正是我的建议。这些单元测试不会改善您的代码。他们不会使其更可靠或更可维护。那时,您只是部分复制了别人的代码。在这种情况下,您是从声音中复制一些写得不好的代码。那是净亏损。一些答案建议进行一些集成测试。如果您只想说“这有用吗?”,这是一个好主意。完整性检查。好的测试很困难,它需要与好的代码一样多的技巧和直觉。
jpmc26 2015年

4
内置邪恶的完美例证。为什么不返回库一Timestamp类(含任何他们想要的表现),并提供名称的方法(.seconds().milliseconds().microseconds().nanoseconds()),当然还有命名构造函数。这样就不会有问题。
Matthieu M.

2
俗话说:“编码中的所有问题都可以通过一层间接层来解决(当然,过多的间接层问题除外)。”
Dan Pantry

Answers:


27

听起来您已经在尽职调查了。但是...

在最实际的水平上,始终在套件中为自己的代码包含少量的“全循环”集成测试,并编写比您认为所需更多的断言。特别是,您应该有少数几个测试可以执行完整的create-read- [do_stuff] -validate周期。

[TestMethod]
public void MyFormatter_FormatsTimesCorrectly() {

  // this test isn't necessarily about the stream or the external interpreter.
  // but ... we depend on them working how we think they work:
  var stream = new StreamThingy();
  var interpreter = new InterpreterThingy(stream);
  stream.Write("id-123, some description, 12345");

  // this is what you're actually testing. but, it'll also hiccup
  // if your 3rd party dependencies introduce a breaking change.
  var formatter = new MyFormatter(interpreter);
  var line = formatter.getLine();
  Assert.equal(
    "some description took 123.45 seconds to complete (id-123)", line
  );
}

听起来您已经在做这种事情。您只是在处理一个片状和/或复杂的库。在这种情况下,最好进行一些“这就是库的工作方式”类型的测试,这些测试既可以验证您对库的理解,又可以作为使用库的示例。

假设您需要了解并取决于 JSON解析器如何解释JSON字符串中的每个“类型”。在您的套件中包含以下内容是有用且微不足道的:

[TestMethod]
public void JSONParser_InterpretsTypesAsExpected() {
  String datastream = "{nbr:11,str:"22",nll:null,udf:undefined}";
  var o = (new JSONParser()).parse(datastream);

  Assert.equal(11, o.nbr);
  Assert.equal(Int32.getType(), o.nbr.getType());
  Assert.equal("22", o.str);
  Assert.equal(null, o.nll);
  Assert.equal(Object.getType(), o.nll.getType());
  Assert.isFalse(o.KeyExists(udf));
}

但是其次,请记住,任何形式的自动化测试,无论以何种严格程度进行,都仍然无法保护您免受所有错误的侵害。发现问题时添加测试很常见的没有质量检查部门,这意味着最终用户会发现很多这样的问题。

在很大程度上,这很正常。

第三,当库在不重命名字段或方法或“破坏”相关代码(可能通过更改其类型)而更改返回值或字段的含义的情况下,我会对该发布者感到非常不满意。而且我认为,即使您可能已经阅读了变更日志(如果有的话),也应该将一些压力传递给发布者。我认为他们需要充满希望的建设性批评...


gh,我希望它就像将json字符串输入库一样简单。不是。我不能做等效的(new JSONParser()).parse(datastream),因为它们直接从a抓取数据,NetworkInterface并且所有进行实际解析的类都是私有的且受保护的包。
durron597

此外,变更日志不包括将时间戳从ms更改为ns的事实,以及其他未记录的令人头疼的问题。是的,我对他们感到非常不满,我已经向他们表达了这一点。
durron597

@ durron597哦,几乎从来没有。但是,您经常可以伪造基础数据源-就像第一个代码示例一样。...点是:做全循环集成测试时,可能的话,测试你可能在图书馆的理解,要知道,你依然让虫子到野外。您的第三方供应商必须对做出无形的,重大的变化负责。
svidgen

@ durron597我不熟悉NetworkInterface……您可以通过将接口连接到本地主机上的端口来向其中馈送数据吗?
svidgen

NetworkInterface。它是用一块网卡直接合作并在其上打开插座等低级别的对象
durron597

11

简短的回答:很难。您可能会觉得没有好的答案,这是因为没有简单的答案。

长答案:就像@ptyx所说,您需要系统测试和集成测试以及单元测试:

  • 单元测试快速且易于运行。它们在代码的各个部分中捕获错误,并使用模拟来使其运行成为可能。根据需要,它们不能捕获代码段之间的不匹配(例如毫秒与纳秒)。
  • 集成测试和系统测试运行起来较慢且较难,但会捕获更多错误。

一些具体建议:

  • 简单地使系统测试在尽可能多的系统上运行有一些好处。即使它不能验证很多行为,也不能很好地找出问题所在。(Micheal Feathers在有效地使用旧版代码中对此进行了更多讨论。)
  • 投资可测试性会有所帮助。有一个巨大的技术,您可以在这里使用号码:持续集成,脚本,虚拟机,工具,回放,代理或重定向网络流量。
  • 投资可测试性的优势之一(至少对我而言)可能不是显而易见的:如果测试乏味,烦人或编写或运行繁琐,那么如果我感到压力很大,我就很容易跳过这些测试还是累 将测试保持在“很容易做到没有借口这样做的门槛”以下很重要。
  • 完善的软件是不可行的。像其他所有东西一样,花在测试上的努力是一种折衷,有时这是不值得的。存在约束(例如您缺少质量检查部门)。接受错误会发生,恢复和学习的错误。

我已经看到编程被描述为学习问题和解决方案空间的活动。提前使一切都完美可能并不可行,但是您可以在事后学习。(“我在多个地方固定了时间戳处理,但是错了一个地方。我可以更改数据类型或类以使时间戳处理更明确,更难错过,还是使其更加集中,所以我只能更改一个地方?我可以修改吗?我的测试来验证时间戳处理的更多方面?我可以简化测试环境以使其在将来变得更容易吗?我可以想象某个工具可以使此操作变得更容易吗?如果可以,我可以在Google上找到这样的工具吗?等等)


7

我更新了该库的版本…导致了时间戳(第三方库将其返回为long),从时间戳记的毫秒数更改为时间戳记的十亿分之一秒。

不是库中的错误

我在这里非常不同意你。这是库中的错误,实际上是一个非常隐蔽的错误。他们更改了返回值的语义类型,但没有更改返回值的程序类型。这可能造成各种破坏,特别是如果这是次要版本的颠簸,即使是次要的颠簸也是如此。

假设相反,该库返回了一种类型,该类型包含MillisecondsSinceEpoch一个简单的包装long。当他们将其更改为NanosecondsSinceEpoch值时,您的代码将无法编译,并且显然会将您指向需要进行更改的地方。所做的更改无法默默地破坏您的程序。

更好的TimeSinceEpoch是,当添加了更高的精度时,该对象可以适应其界面,例如在该#toLongNanoseconds方法旁边添加一个#toLongMilliseconds方法,而无需更改代码。

下一个问题是您没有对该库进行可靠的一组集成测试。你应该写那些。最好是在该库周围创建一个接口,以将其封装在应用程序的其余部分之外。其他几个答案解决了这个问题(我输入时还会弹出更多答案)。集成测试的运行频率应低于单元测试的运行频率。这就是为什么有缓冲层会有所帮助的原因。将集成测试隔离到一个单独的区域(或使用不同的名称),以便您可以根据需要运行它们,但并非每次运行单元测试时都可以运行。


2
@ durron597我仍然认为这是一个错误。除了缺乏文档之外,为什么还要完全改变预期的行为?为什么不采用提供新精度的新方法,而又让旧方法仍然提供毫秒?为什么不提供一种让编译器通过返回类型更改来提醒您的方法呢?不仅在文档中,而且在代码本身中,都不需要花很多时间就可以使它变得更加清晰。
cbojar

1
@gbjbaanb,“他们的发布行为不当”对我来说似乎是个错误
Arturo

2
@gbjbaanb第三方库[应该]与用户建立“契约”。可以(应该)将违反合同(无论是否有文件证明)视为错误。正如其他人所说,如果必须进行某些更改,请使用新的函数/方法(请参阅...Ex()Win32API中的所有方法)添加到合同中。如果这不可行,则通过重命名功能(或其返回类型)来“破坏”合同要比改变行为更好。
TripeHound 2015年

1
这是库中的错误。长时间使用纳秒级就可以推动它。
2015年

1
@gbjbaanb您说这不是错误,因为它是预期的行为,即使是意外的行为也是如此。从这个意义上讲,它不是实现错误,但它是相同的错误。它可能被称为设计缺陷或接口错误。错误在于以下事实:它暴露了对long而不是显式单位的原始痴迷,由于导出其内部实现的详细信息(数据存储为某个单位的long),其抽象是泄漏的。最小惊讶原则,并带有细微的单位变更。
cbojar

5

您需要集成和系统测试。

单元测试非常适合验证您的代码是否按预期运行。如您所知,它不会挑战您的假设或确保您的期望是理智的。

除非您的产品与外部系统的交互很少,或者与众所周知,稳定且有据可查的系统交互(可以在真实世界中很少发生),否则单元测试是不够的。

测试级别越高,它们越能防止意外发生。这要付出一定的代价(便利,速度,脆弱性...),因此单元测试应该仍然是测试的基础,但是您需要其他层,包括-最终-一点点人工测试,这在赶上赶上很长的路要走没人想到的愚蠢事情。


2

最好的办法是创建一个最小的原型,并了解该库的工作原理。这样,您将获得有关文档较差的库的一些知识。原型可以是使用该库并执行功​​能的简约程序。

否则,编写具有一半定义的要求并且对系统了解不足的单元测试是没有意义的。

至于您的特定问题-有关使用错误指标的信息:我将其视为需求变更。一旦发现问题,请更改单元测试和代码。


1

如果您使用的是流行的,稳定的库,那么您可能会认为它不会对您造成不良影响。但是,如果像您描述的那样的事情发生在该库中,那么显然,这不是一个。在经历了这种糟糕的经历之后,每次与该库的交互出现问题时,您不仅需要检查犯错的可能性,还需要检查该库犯错的可能性。因此,可以说这是您“不确定”的一个库。

我们“不确定”的与库一起使用的一种技术是在我们的系统和所述库之间建立一个中间层,该中间层抽象了库提供的功能,断言我们对库的期望是正确的,并且还大大简化了我们未来的生活,如果我们决定启动该库,并用性能更好的另一个库替换它。


这并不能真正回答问题。我已经有一个将库与系统分开的层,但是问题是,当库在没有警告的情况下更改时,我的抽象层可能会出现“错误”。
durron597

1
@ durron597然后,该层可能无法将库与应用程序的其余部分充分隔离。如果发现难以测试该层,则可能需要简化行为并更加隔离底层数据。
cbojar 2015年

@cbojar说什么。另外,让我重复一下上面的文本中可能没有注意到的内容:assert关键字(或函数或功能,取决于所使用的语言)是您的朋友。我不是在谈论单元/集成测试中的断言,而是要说隔离层应该包含很多断言,断言所有有关库行为的断言。
Mike Nakis 2015年

这些断言不一定在生产运行中执行,但它们确实在测试时执行,具有隔离层的白盒视图,因此能够(尽可能)确保您的层从库中接收到的信息是声音。
Mike Nakis 2015年
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.