unit testing处理退化的networking堆栈,文件损坏和其他缺陷

我主要是一个C ++编码器,到目前为止,没有真正为我的所有代码编写testing。 我已经认定这是一个坏主意(tm),在添加了一些巧妙地破坏了旧function的新function之后,或者根据你想看的方式,引入了一些新的function。

但是,unit testing似乎是一个非常脆弱的机制。 你可以在“完美”的条件下testing某些东西,但是当你的代码中断的时候,你不会看到你的代码是如何执行的。 举例来说,抓取工具是抓取一些特定网站的数据X.您是否简单地保存示例页面,对其进行testing,并希望网站永远不会更改? 这对于回归testing可以很好地工作,但是,你会写什么样的testing来不断地检查这些网站,并让你知道应用程序何时不做这个工作,因为网站改变了一些东西,现在会导致你的应用程序崩溃? 你不想让你的testing套件监视代码的意图吗?

上面的例子是有点人为的,还有一些我没有碰到(如果你没有猜到的话)。 不过,让我挑一些我有的东西。 你如何testing一个应用程序将面对退化的networking堆栈时的工作? 也就是说,由于某种原因,你有一个适度的数据包丢失量,并且你有一个函数DoSomethingOverTheNetwork() ,当堆栈没有按照它的要求执行时,它应该会优雅地退化; 但是呢? 开发人员通过专门build立一个网关,在首次写入数据包时模拟一个不好的networking,进行个人testing。 几个月后,有人检查了一些细微修改的代码,所以不能及时检测到退化,或者应用程序甚至不能识别退化,这是从来没有被捕获的,因为你不能运行真实的世界像unit testing这样的testing,可以吗?

此外,如何文件损坏? 假设您将一个服务器列表存储在一个文件中,并且校验和看起来不错,但数据并不是真的。 你想要的代码来处理,你写一些代码,你认为这样做。 你如何testing它确实是应用程序的生命? 你可以吗?

因此,脆性。 unit testing似乎只能在完美的条件下testing代码(这是通过模拟对象等方式来提升的),而不是他们在野外会遇到的情况。 不要误解我的意思,我认为unit testing是很棒的,但是一个仅由它们组成的testing套件似乎是一种巧妙的方法,可以在代码中引入微妙的错误,同时又对自己的可靠性过度自信。

我如何处理上述情况? 如果unit testing不是答案,那是什么?

编辑:我看到很多答案说“只是嘲笑它”。 那么,你不能“嘲笑它”,这是为什么:以我的退化networking堆栈的例子,让我们假设你的函数有一个定义良好的NetworkInterface,我们将模拟。 应用程序通过TCP和UDP发送数据包。 现在,让我们说,嘿,让我们使用模拟对象在接口上模拟10%的损失,看看会发生什么。 你的TCP连接增加了他们的重试次数,以及增加他们的回退,所有的好习惯。 你决定改变你的UDP数据包的X%来实际build立一个TCP连接,有损接口,我们希望能够保证一些数据包的传输,而其他的不应该损失太多。 很好用。 与此同时,在现实世界中,当你增加TCP连接数(或TCP上的数据)时,在一个有损连接的情况下,最终会增加你的UDP数据包丢失,因为你的TCP连接将会结束 – 越来越多地发送他们的数据和/或减less他们的窗口,导致10%的数据包丢失现在更像是90%的UDP数据包丢失。 Whoopsie。

没什么大问题,让我们把它分解成UDPInterface和TCPInterface。 等一下..这些是相互依赖的,testing10%的UDP丢失和10%的TCP丢失是没有什么不同。

所以,问题是现在你不是简单地unit testing你的代码,而是将你的假设引入到操作系统的TCP栈的工作方式中。 而且,这是一个坏主意(tm)。 一个更糟糕的想法,而不是只是避免这整个惨败。

在某些时候,你将不得不创build一个模拟操作系统,其行为完全像你的真实操作系统,除了是可testing的。 这似乎不是一个好的前进方向。

这是我们经历的事情,我相信别人也可以增加他们的经验。

我希望有人会告诉我,我很错,并指出为什么!

谢谢!

通过任何一本体面的unit testing书,你会发现,编写testing确实包含了input不理想或者错误的边缘情况。

具有exception处理的语言中最常见的方法是“应该抛出”规范,其中某个testing预期会引发特定的exceptiontypes。 如果没有抛出exception,则testing失败。

更新

在更新中,您将描述复杂的时序敏感交互。 unit testing根本没有帮助。 不需要引入networking连接:试想写一个简单的线程安全队列类,或许在一个带有一些新的并发原语的平台上。 在8核心系统上testing它是否工作? 你根本无法确定通过testing。 时间可能会导致操作在核心之间重叠的方式太多了。 取决于运气,可能需要几周的连续执行才能发生一些不太可能的巧合。 通过仔细的分析(静态检查工具可以帮助),获得这样的东西的唯一方法是正确的。 大多数并发软件很可能有一些很less发生的错误,包括所有的操作系​​统。

回到实际可以testing的情况,我发现集成testing通常和unit testing一样有用。 这可以像自动化产品安装,向其添加configuration(例如用户可能创build的),然后从外部“戳”它,例如自动化您的UI一样精心制作。 这发现了另外一类与unit testing分开的问题。

你首先谈论unit testing,然后谈论整个应用程序; 看起来你对于unit testing有点困惑。 按定义进行unit testing是关于在testing软件的每个“单元”时,在最细粒度级别进行testing。 在常用的情况下,“单元”是一个单独的function,而不是整个应用程序。 现代的编程风格function很短,每个function都有一个明确的东西,因此易于unit testing。

你会写什么样的testing来经常检查这些网站?

UnitTests将目标的小部分代码写入。 UnitTests不确认世界上的事情是否正常。 您应该为那些不完美的场景定义应用程序行为。 然后你可以在那些不完美的场景中unit testing你的应用程序。

例如一个爬虫

抓取工具是您可能编写的大量代码。 它有一些不同的部分,一部分可能会获取一个网页。 另一部分可能会分析HTML。 即使这些部分可能太大,不能写unit testing。

你如何testing一个应用程序将面对退化的networking堆栈时的工作? 开发人员通过专门build立一个网关,在首次写入数据包时模拟一个不好的networking,从而亲自testing它。

如果一个testing使用networking,它不是一个UnitTest。

UnitTest(必须以你的代码为目标)不能调用networking。 你没有写networking。 UnitTest应该包含一个仿真networking(每次都是一致的)丢包。

unit testing似乎只在完美的条件下testing代码

UnitTests在定义的条件下testing你的代码。 如果你只能定义完美的条件,你的陈述是真实的。 如果你能够定义不完美的条件,你的陈述是错误的。

听起来好像你回答了你自己的问题。

模拟/存根是testing难以testing区域的关键。 对于你所有的例子来说,手动创build一个网站数据不好,或者造成networking故障的手动方法都可以手动完成。 然而,这样做会非常困难和繁琐,而不是任何人都会推荐的。 事实上,做一些意味着你实际上不是unit testing。

相反,你会使用模拟/存根假装这种情况已经发生,让你testing它们。 使用mock的好处是,不像手动方法,您可以保证每次运行testing时都会执行相同的过程。 反过来,testing将会更快更稳定。

编辑 – 关于更新的问题。

作为一个免责声明我的networking经验是非常有限的,因此我不能评论你的问题的技术方面。 不过,我可以评论你的声音,就好像你testing太多了一样。 换句话说,你的testing涵盖了太多的范围。 我不知道你的代码库是什么样子,但是在给定的函数/对象的情况下,你仍然应该能够提供假的input,这样你就可以testing你的对象/函数是否独立地做了正确的事情。

因此,让我们想象你的孤立的地区工作正常的要求。 仅仅因为你的unit testing通过并不意味着你已经testing了你的应用程序。 您仍然需要手动testing您描述的这种情况。 在这种情况下,听起来好像压力testing – 限制networking资源等是必需的。 如果您的应用程序按预期工作 – 很好。 如果不是的话,你已经错过了testing。 unit testing(更多与TDD / BDD配合使用)是确保您的应用程序工作中的小的孤立区域。 你还需要整合/手动/回归等。 所以你应该使用mocks / stubs来testing你的小的孤立区域的function。 在我看来,unit testing更类似于devise过程。

集成testing与unit testing

我应该在这个答案前面说,我偏向于集成testing,unit testing是tdd中使用的主要testingtypes。 在工作中,我们也有一些混合的unit testing,但只是在必要的时候。 我们开始进行集成testing的主要原因是因为我们更关心应用程序的function,而不是特定的function。 根据我的经验,我们也得到了整合覆盖面,在自动化testing方面有巨大的差距。

嘲弄与否,为什么不做两个

我们的集成testing可以运行完全连接(到非托pipe资源)或模拟。 我们发现这有助于弥补现实世界与嘲笑之间的差距。 这也为我们提供了一个select,决定不使用模拟版本,因为实施模拟的投资回报率是不值得的。 你可能会问为什么要使用模拟。

  • testing套件运行得更快
  • 每次保证相同的响应(没有超时,不可预见的退化networking等)
  • 对行为进行细粒度的控制

有时你不应该写一个testing

testing,任何types的testing都有折衷。 你看看实施testing,模拟,变体testing等的成本,并权衡这些好处,有时候写testing,模拟或变体是没有意义的。 这个决定也是在你的软件types的背景下进行的,这是决定你的testing套件的深度和广度的主要因素之一。 换句话说,我会为社交培根聚会function写一些testing,但是我不打算为培根朋友algorithm写出正式的validationtesting。

你是否简单地保存样本页面,testing这些页面,并希望网站永远不会改变?

testing不是万能的

是的,你保存样品(如夹具)。 你不希望页面不改变,但你不知道如何以及何时会改变。 如果你有想法或参数可能会改变,那么你可以创build变种,以确保你的代码将处理这些变种。 如果它确实发生了变化,那么它会中断,您将添加新的样本,修复问题并继续前进。

你会写什么样的testing来不断地检查这些网站的生活,并让你知道什么时候应用程序没有做它的工作,因为该网站改变了一些东西,现在会导致你的应用程序崩溃?

testing!=监测

testing是testing和开发(和质量保证)的一部分,而不是生产。 监控是你在生产中使用的,以确保你的应用程序正常工作。 你可以编写监视器,当有什么东西坏了的时候应该提醒你。 这是另一个话题。

你如何testing一个应用程序将面对退化的networking堆栈时的工作?

培根

如果是我,我会有一个有线和模拟模式的testing(假设模拟已经足够有用)。 如果模拟很难正确,或者如果不值得的话,我只会进行有线testing。 但是,我发现几乎总是有一种方法将variables分解成不同的testing。 然后,每个testing的目标是testing变化的向量,同时尽量减less所有其他变化。 诀窍是写出重要的变体,不是每个可能的变体。

此外,如何文件损坏?

多lesstesting

您提到校验和是正确的,但文件实际上已经损坏。 这里的问题是我在写什么类的软件。 我是否需要超级偏执的统计小误报的可能性或不。 如果我这样做,那么我们的工作就是find多么深刻和广泛的考验。

我认为你不能也不应该对你可能面对的所有可能的错误进行unit testing(如果陨石碰到db服务器怎么办?) – 你应该努力testing错误,合理的概率和/或依赖或另一个服务。 例如; 如果您的应用程序需要networking数据包的正确到达; 你应该使用TCP传输层:它保证接收的数据包的正确性透明,所以你只需要集中,例如。 如果networking连接中断,会发生什么。 校验和旨在检测或纠正合理数量的错误 – 如果您希望每个文件有10个错误,那么您将使用不同的校验和,而不是预计有100个错误。 如果select的校验和表明文件是正确的,比你没有理由认为它是坏的(被打破的可能性可以忽略不计)。 因为你没有无限的资源(例如时间),所以在你写testing的时候你必须妥协。 并select这些妥协这是一个棘手的问题。

虽然对于您面临的巨大困境并不是一个完整的答案,但是您可以通过使用称为“ 等价分区”的技术来减lesstesting的数量。

在我的组织中,我们在自动和手动testing中执行了许多级别的覆盖,回归,正面,负面,基于场景的UI,都是从“清洁的环境”开始的,但即使这样也不完美。

至于你提到的其中一个案例,程序员进来并改变了一些敏感的检测代码,但没有人注意到,我们可能会得到一个“行为不友好”的数据快照,检测程序 – 我们会定期运行所有testing(而不是在最后一刻)。

有时我会创build两个(或更多)testing套件。 一个套件使用mocks / stubs,只testing我正在写的代码。 其他testingtesting数据库,网站,networking设备,其他服务器,以及其他任何我不能控制的内容。

那些其他的testing真的是我的代码与我的代码交互系统的假设testing。 所以如果他们失败了,我知道我的要求已经改变了。 然后,我可以更新我的内部testing,以反映我的代码需要具有的任何新行为。

内部testing包括模拟外部系统各种故障的testing。 每当我观察到一种新的失败,无论是通过我的其他testing还是作为错误报告的结果,我都有一个新的内部testing来写。

编写testing来模拟真实世界中发生的所有奇怪的事情可能是有挑战性的,但结果是,你真的想到所有这些情况,并生成健壮的代码。

unit testing的正确使用从头开始。 也就是说,在编写生产代码之前,您需要编写unit testing。 然后unit testing被迫考虑错误条件,前置条件,后置条件等等。一旦你编写你的生产代码(并且unit testing能够成功编译和运行),如果有人对代码进行了修改改变它的任何条件(甚至是微妙的),unit testing将会失败,你将会很快地了解它(通过编译器错误或者unit testing失败)。

编辑:关于更新的问题

你想要testing的东西不适合unit testing。 networking和数据库连接在模拟集成testing中testing得更好。 在远程连接的初始化过程中,有太多东西会被打破,为它创build一个有用的unit testing(我确定有一些unit testing – 修复所有在那里不同意我的人,但是在我的经验,试图unit testingnetworkingstream量和/或远程数据库的function比通过一个圆孔推动一个方形钉子更糟)。

你正在谈论图书馆或应用程序testing,这与unit testing不一样。 您可以使用unit testing库(如CppUnit / NUnit / JUnit)进行库和回归testing,但正如其他人所说,unit testing是关于testing您的最低级别的函数,这些函数应该是非常明确的,的代码。 当然,您可以通过所有低级unit testing,并且在整个系统中仍然有networking故障。

图书馆testing可能非常困难,因为有时只有一个人可以评估输出的正确性。 考虑一个vectorgraphics或字体渲染库; 没有一个完美的输出,你可能会得到一个完全不同的结果根据您的机器的video卡。

或者由于大量的可能的input,testingPDFparsing器或者C ++编译器是非常困难的。 这是拥有10年的客户样本,缺陷历史比源代码本身更有价值。 几乎任何人都可以坐下来编写代码,但是最初你没有办法validation你的程序的正确性。

模拟对象的美妙之处在于你可以有多个对象。 假设您正在针对networking堆栈定义明确的接口进行编程。 然后你可以有一个模拟对象WellBehavingNetworkStack来testing正常情况和另一个模拟对象OddlyBehavingNetworkStack ,它模拟了你期望的一些networking故障。

使用unit testing我通常也会testing参数validation(比如确保我的代码抛出NullPointerExceptions),这在Java中很容易,但在C ++中很困难,因为在后一种语言中,您可以很容易地触发未定义的行为 ,然后所有的赌注都closures。 所以你不能确定你的unit testing工作,即使他们似乎。 但是,您仍然可以testing不会引发未定义行为的奇怪情况,这在编写良好的代码中应该是相当多的。

你在谈论的是使应用程序更健壮。 也就是说,你希望他们优雅地处理失败。 然而,如果不是不可能的话,testing每一个可能的现实世界故障情况将是困难的 使应用程序健壮的关键是假设失败是正常的,并且应该在未来的某个时间点被预期。 应用程序如何处理失败真的取决于情况。 有许多不同的方式来检测和处理失败(可能是一个很好的问题)。 试图依靠unit testing只会让你的一部分。 预测故障(即使在一些简单的操作上)也会使您更接近更强大的应用程序。 亚马逊build立了整个系统来预测所有types的故障(硬件,软件,内存和文件损坏)。 看看他们的迪纳摩真实世界error handling的例子。