从TDD的角度来看,如果我针对实时端点而不是模拟进行测试,那我是一个坏人吗?


16

我虔诚地遵循TDD。我的项目通常具有有意义的测试用例,测试覆盖率可达85%或更高。

我使用HBase进行了大量工作,而主要的客户端接口HTable实在令人难以接受。与编写使用实时端点的测试相比,编写我的单元测试要花3到4倍的时间。

从哲学上讲,我知道使用模拟的测试应优先于使用实时端点的测试。但是模拟HTable是一个严重的难题,我不确定它相对于针对实时HBase实例进行测试是否具有很多优势。

我们团队中的每个人都在其工作站上运行一个单节点HBase实例,而我们的Jenkins机器上运行着一个单节点HBase实例,因此这不是可用性问题。实时端点测试显然比使用模拟的测试花费更长的时间,但是我们并不在乎。

现在,我为我的所有类编写了实时端点测试和基于模拟的测试。我很想抛弃模拟游戏,但我不希望结果因此而下降。

你们怎么想


8
实时端点不是真正的单元测试,不是吗?这是一个集成测试。但这最终可能是实用主义的问题。您可以花时间编写模拟游戏,也可以花时间编写功能或修复错误。
罗伯特·哈维

4
我听说过有人通过对自己的代码运行单元测试来关闭第三方服务的故事……这已与实时端点挂钩。速率限制不是单元测试通常要做或关心的事情。

14
你不是一个坏人。你是做坏事的好人。
Kyralessa 2014年

15
我虔诚地遵循TDD也许是问题所在?我不认为任何这种方法的,就是要采取的是认真。;)
FrustratedWithFormsDesigner 2014年

9
认真遵循TDD意味着您将丢弃15%的未发现代码。
mouviciel 2014年

Answers:


23
  • 我的第一个建议是不要模拟您不拥有的类型。您提到了HTable的模拟是一个真正的难题-也许您应该将其包装在公开了所需HTable功能的20%的适配器中,然后在需要的地方模拟包装器。

  • 话虽这么说,让我们假设我们正在谈论大家都拥有的类型。如果基于模拟的测试专注于一切顺利的快乐路径场景,那么您将不会失去任何放弃它们的机会,因为集成测试可能已经在测试完全相同的路径。

    但是,当您开始考虑被测系统如何应对其协作者的合同中定义的所有可能发生的小事时,不管它正在与之交谈的是什么实际的具体对象,隔离测试都会变得很有趣。这就是所谓的基本正确性的一部分。这些小案例可能有很多,还有更多的组合。这是集成测试开始变得糟糕的地方,而孤立的测试将保持快速且易于管理。

    更具体地说,如果您的HTable适配器的方法之一返回一个空列表怎么办?如果返回null怎么办?如果抛出连接异常怎么办?如果发生任何上述情况,则应在适配器的合同中进行定义,并且它的任何使用者都应准备好应对这些情况,因此需要对其进行测试。

综上所述:如果基于模拟的测试与集成测试完全相同,那么删除基于模拟的测试不会对质量造成任何影响。但是,尝试想象其他隔离测试(和合同测试)可以帮助您解决界面/合同的广泛问题,并通过解决一些缺陷(这些缺陷很难用集成测试来考虑和/或测试)来提高质量。


+1我发现用模拟测试构造边缘案例比用这些案例填充数据库要容易得多。
罗布

我同意你的大部分回答。但是,我不确定我是否同意适配器部分。HTable很难模拟,因为它是裸机。例如,如果要执行批处理get操作,则必须创建一堆Get对象,将它们放在列表中,然后调用HTable.batch()。从嘲弄的角度来看,这是一个很大的痛苦,因为您必须创建一个自定义Matcher,以检查传递给HTable.batch()的get对象列表,然后为该get()对象列表返回正确的结果。严重的疼痛。
sangfroid 2014年

我想我可以为HTable创建一个不错的,友好的包装器类,该类负责所有的内务处理,但是到那时……我觉得我有点像在围绕HTable建立框架,这真的应该成为我的工作吗?通常“让我们建立一个框架!” 是我走错方向的迹象。我可能要花几天的时间编写课程,以使HBase更加友好,而且我不知道那是否可以充分利用我的时间。另外,然后我在接口或包装器周围停顿,而不仅仅是普通的旧HTable对象,这肯定会使我的代码更复杂。
sangfroid 2014年

但是我同意您的主要观点,即不应为他们不拥有的类编写模拟。绝对同意编写模拟测试来测试与集成测试相同的东西是没有意义的。似乎模拟最适合测试接口/合同。感谢您的建议-这对我有很大帮助!
sangfroid 2014年

我几乎不了解HTable的真正功能以及如何使用它,所以不要以我的包装器示例为例。我提到包装器/适配器是因为我认为包装的东西相对较小。您不必向HTable引入一对一副本,这当然很痛苦,更不用说整个框架了-但是您需要一个seam,即应用程序领域和HTable领域之间的接口。它应该将HTable的某些功能改写为应用程序自己的术语。当涉及到数据访问时,存储库模式是这种接缝的完美体现。
guillaume31 2014年

11

从哲学上讲,使用模拟的测试应优先于使用实时端点的测试

我认为至少,这是TDD支持者当前 持续争论的焦点

我个人的观点超出了说基于模拟的测试的范围,它主要是一种表示接口契约形式的方法。理想情况下,当且仅当您更改接口时,它才会中断(即失败)。因此,在诸如Java之类的合理强类型化的语言中,并且当使用显式定义的接口时,它几乎是完全多余的:编译器已经告诉您是否更改了接口。

主要的例外是,当您使用非常通用的界面时(可能基于注释或反射),编译器无法有效地自动进行监管。即使那样,您也应该检查是否有一种以编程方式(例如SQL语法检查库)进行验证的方法,而不是手动使用模拟方法。

在使用“实时”本地数据库进行测试时,您会遇到后一种情况。htable实现开始了,并且对interfacve合同进行了更为全面的验证,这比您手工编写的想像要多。

不幸的是,基于模拟的测试更普遍的用途是测试:

  • 通过测试时编写的任何代码
  • 除了代码的存在和运行类型外,不对代码的任何属性提供任何保证
  • 每次更改该代码都会失败

当然应该立即删除此类测试。


1
我对此不够支持。我宁愿覆盖1个具有出色测试的覆盖率,也不愿覆盖100个填充剂。
2014年

3
基于模拟的测试确实描述了2个对象用来一起交谈的契约,但是它们远远超出了Java这样的语言的类型系统所能做的。这不仅与方法签名有关,它们还可以为参数或返回的结果指定有效的值范围,可接受的异常,可以调用方法的顺序和次数等。如果存在,则仅编译器不会警告您是那些变化。从这个意义上说,我认为它们根本不是多余的。有关基于模拟的测试的更多信息,请参见infoq.com/presentations/integration-tests-scam
guillaume31 2014年

1
...同意,即测试接口调用周围的逻辑
Rob

1
当然可以将未检查的异常,未声明的前提条件和隐式状态添加到使接口的静态类型较少的事物列表中,从而证明基于模拟的测试而不是简单的编译。但是,问题在于,当这些方面确实发生变化时,它们的规范是隐式的,并且分布在所有客户端的测试中。它们可能不会更新,因此请坐在那里静静地在绿色勾号后面隐藏一个错误。
soru 2014年

“它们的规范是隐式的”:如果您为接口编写合同测试(blog.thecodewhisperer.com/2011/07/07/contract-tests-an-example)并在设置模拟时坚持使用合同测试,则不是这样。
guillaume14年

5

与基于模拟的测试相比,基于端点的测试运行需要多长时间?如果要花费更长的时间,那么是的,值得花大量的时间编写测试,以使单元测试更快-因为您必须多次运行它们。即使不是更长的时间,即使基于端点的测试也不是“纯粹的”单元测试,只要它们在测试单元方面做得很好,就没有必要对此进行信奉。


4

我完全同意guillaume31的响应,永远不要嘲笑您不拥有的类型!

通常,测试中的痛苦(模拟复杂的界面)反映了设计中的问题。也许您需要模型和数据访问代码之间的某种抽象,例如使用六边形体系结构和存储库模式的示例,这是解决此类问题的最常用方法。

如果要执行集成测试以检查事物,请执行集成测试,如果要执行单元测试,因为要测试逻辑,请执行单元测试并隔离持久性。但是进行集成测试是因为您不知道如何将逻辑与外部系统隔离(或者将其痛苦隔离),这是一个很大的麻烦,因此您选择集成而不是单元是为了限制设计而不是真正需要测试集成。

看看Ian cooper的这份谈话表:http : //vimeo.com/68375232,他谈论六角形体系结构和测试,谈论什么时候模拟什么,这是一个真正启发性的谈话,它解决了许多关于您真正的TDD的问题。


1

TL; DR我的看法,这取决于您最终在测试上花费了多少精力,以及将更多的精力花在实际系统上是否会更好。

长版:

这里有一些很好的答案,但是我的看法是不同的:测试是一种经济活动,需要自己付出回报,并且如果您花费的时间没有在开发和系统可靠性上得到回报(或者您希望摆脱困境)测试),则您可能进行了不明智的投资;您从事的是构建系统业务,而不是编写测试。因此,减少编写和维护测试的工作至关重要。

例如,我从测试中获得的一些主要价值是:

  • 可靠性(并因此提高了开发速度):重构代码/集成新框架/将组件/端口交换到其他平台,确信这些东西仍然有效
  • 设计反馈:低/中级接口上的经典TDD / BDD“使用您的代码”反馈

针对实时端点进行测试仍应提供这些功能。

针对实时端点进行测试的一些缺点:

  • 环境设置-配置和标准化测试运行环境需要进行更多工作,并且不同的环境设置可能会导致不同的行为
  • 无状态-针对活动的终结点工作可能最终会促进依赖可变的终结点状态的编写测试,这是脆弱且难以推理的(例如,当某件事失败时,是否会因为怪异的状态而失败?)
  • 测试运行环境很脆弱-如果测试失败,它是测试,代码还是实时端点?
  • 运行速度-活动端点通常较慢,有时更难并行化
  • 创建边缘案例进行测试-通常对于模拟来说是微不足道的,有时对于实时端点来说是痛苦的(例如,设置棘手的是传输/ HTTP错误)

如果我处于这种情况下,并且缺点似乎不是问题,而嘲笑端点会大大减慢我的测试编写速度,那么只要确保确保过一会儿再检查一下,看看实际上这些缺点并不是问题。


1

从测试的角度来看,有一些绝对必要的要求:

  • 测试(单元或其他方式)绝不能接触生产数据
  • 一个测试的结果一定不能影响另一个测试的结果
  • 您必须始终从已知位置开始

当连接到任何在测试之外保持状态的源时,这是一个巨大的挑战。这不是“纯粹的” TDD,但是Ruby on Rails小组以一种可能适合您目的的方式解决了这个问题。Rails测试框架以这种方式工作:

  • 运行单元测试时自动选择了测试配置
  • 数据库是在运行单元测试开始时创建并初始化的
  • 运行单元测试后,数据库被删除
  • 如果使用SqlLite,则测试配置使用RAM数据库

所有这些工作都内置在测试工具中,并且效果很好。还有很多其他内容,但是基础知识足以满足您的对话需求。

在与我长期合作的不同团队中,我们将做出选择,以促进代码被测试即使不是最正确的。理想情况下,我们将使用我们控制的代码将所有调用包装到数据存储中。从理论上讲,如果这些旧项目中的任何一个获得了新的资金,我们可以将注意力集中在少数几个类上,从而将它们从数据库绑定到Hadoop迁移。

重要的方面是不要弄乱生产数据,并确保您正在真正测试自己认为要测试的内容。能够按需将外部服务重置为已知基准非常重要,即使您的代码也是如此。

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.