测试:如何在不损失速度的情况下专注于行为而不是实现



似乎有两种完全不同的测试方法,我想引用这两种方法。

问题是,这些意见是在5年前(2007年)提出的,我很感兴趣的是,从那时起发生了什么变化,我应该走哪条路。

Brandon Keepers:

理论是,测试应该是实施这会减少脆性测试和实际测试结果(或行为)。

对于RSpec,我觉得完全嘲笑你测试控制器的模型最终会迫使你看起来太多进入控制器的实现中。

这本身还不错,但问题是它也有同行对控制器进行了大量的修改,以规定如何使用模型。为什么如果我的控制器调用Thing.new有关系吗?如果我的管理员决定参加Thing.create!以及救援路线?如果我的模特有特殊的初始值设定项方法,如Thing.build_with_foo?我的规范如果我更改实现,行为就不应该失败。

当您有嵌套的资源并且为每个控制器创建多个模型。我的一些设置方法结束最多有15条或更多的线长并且非常脆弱。

RSpec的意图是将您的控制器逻辑与你的模型,理论上听起来不错,但几乎与像Rails这样的集成堆栈的粒度。尤其是如果你练习瘦控制器/胖模型规程控制器变得非常小,并且设置变得巨大。

那么想要BDD的人该怎么办呢?退一步,我真正想测试的不是我的控制器调用Thing.new,而是给定参数X,它会创建一个新事物并重定向到它。

David Chelimsky:

这一切都是关于权衡的。

AR选择继承而不是授权这一事实让我们陷入了困境测试绑定-我们必须耦合到数据库,或者我们必须更密切地执行。我们接受这种设计选择因为我们在表现力和干涩方面获得了好处。

为了应对这种困境,我选择了更快的测试,但代价是稍微更脆。你选择的是不那么脆的测试他们跑得稍微慢一点。无论哪种方式,这都是一种权衡。

在实践中,我每年运行数百次测试,如果不是数千次的话天(我使用自动测试并采取非常精细的步骤),然后我改变我几乎从不使用"new"或"create"。同样由于精细的步骤出现的模型一开始很不稳定。valid_thing_attrs这种方法可以将痛苦降到最低,但这仍然意味着每一个新的必填字段都意味着我必须更改valid_thing_attrs。

但如果你的方法在实践中对你有效,那就很好!在里面事实上,我强烈建议您发布一个带有生成器的插件以您喜欢的方式生成示例。我相信很多成千上万的人将从中受益。

Ryan Bates:

出于好奇,你在测试/规格中多久使用一次模拟?也许我做错了什么,但我发现这很严重限制。自从一个多月前切换到rSpec以来,我一直在做他们在文档中建议控制器和视图层根本不要访问数据库,模型完全被模拟了出来这会给你一个很好的速度提升,让一些事情变得更容易,但我发现这样做弊大于利。自从使用mock,我的眼镜变成了一场维护噩梦。规格旨在测试行为,而不是实现。我不在乎如果调用了一个方法,我只想确保结果输出是正确的。因为嘲笑会让规格对实现时,它会进行简单的重构(不会改变行为)不可能不经常回去"修复"规格。我对规范/测试应该是什么很有意见掩蔽只有当应用程序中断时,测试才会中断。这是一个为什么我几乎不测试视图层,因为我觉得它太硬了。当改变视野中的小事。我发现了同样的问题模拟。最重要的是,我今天才意识到嘲笑类方法(有时)在规范之间徘徊。规格应独立,不受其他规格的影响。这打破了规则,并导致棘手的错误。我从这一切中学到了什么?是小心你使用嘲讽的地方。Stubbing没有那么糟糕,但仍然有一些相同的问题。

我花了过去的几个小时,从我的眼镜上删除了几乎所有的仿制品。我还使用控制器规范中的"integrate_views"。我也正在加载所有每个控制器规格的夹具,因此有一些测试数据需要填写视图。最终结果是什么?我的眼镜更短、更简单、更多一致,刚性较小,它们一起测试整个堆栈(模型、视图、控制器),这样就不会有漏洞从裂缝中溜走。我是不要说这对每个人来说都是"正确的"方式。如果您的项目需要一个非常严格的规范案例,那么它可能不适合你,但在我的如果这比我以前使用mock的情况要好得多。我仍然我认为在某些地方使用存根是一个很好的解决方案,所以我仍在这样做那个

我认为这三种观点仍然完全有效。Ryan和我一直在为嘲讽的可维护性而挣扎,而David觉得为了提高速度,维护方面的权衡是值得的。

但这些权衡是一个更深层次问题的症状,大卫在2007年提到了这个问题:ActiveRecord。ActiveRecord的设计鼓励你创建那些做得太多、对系统其他部分了解太多、表面积太大的神对象。这导致测试要测试的内容太多,对系统的其他部分了解太多,而且速度太慢或太脆。

那么解决方案是什么呢?将尽可能多的应用程序与框架分离。编写许多小类,为您的域建模,并且不继承任何内容。每个对象都应该有有限的表面积(不超过几个方法)和通过构造函数传递的显式依赖关系。

使用这种方法,我只编写了两种类型的测试:独立单元测试和全栈系统测试。在隔离测试中,我模拟或存根所有不是被测试对象的东西。这些测试速度极快,通常甚至不需要加载整个Rails环境。全栈测试锻炼了整个系统。他们行动迟缓,失败时会给出无用的反馈。我写的尽可能少,但足以让我相信我所有经过良好测试的对象都能很好地集成。

不幸的是,我还不能为您指出一个做得很好的示例项目。我在关于"为什么我们的代码有味道"的演讲中谈到了这一点,观看了Corey Haines关于Fast Rails测试的演讲,我强烈建议阅读《由测试引导的成长面向对象软件》。

感谢您编译2007年的报价。回首往事很有趣。

我目前的测试方法在RailsCasts这一集中有介绍,我对此非常满意。总之,我有两个级别的测试。

  • 高级:我在RSpec、Capybara和VCR中使用请求规范。可以根据需要标记测试以执行JavaScript。这里避免了模拟,因为目标是测试整个堆栈。每个控制器动作至少测试一次,可能测试几次。

  • 低级:这是测试所有复杂逻辑的地方,主要是模型和助手。我在这里也避免嘲讽。必要时,测试会命中数据库或周围的对象。

请注意,没有控制器或视图规格。我觉得这些已经在请求规范中充分涵盖了。

既然没有什么嘲讽,我该如何保持测试的快速?以下是一些提示。

  • 在高级测试中避免过多的分支逻辑。任何复杂的逻辑都应该移到较低的级别。

  • 生成记录时(例如使用Factory Girl),请先使用build,必要时才切换到create

  • 使用带有Spork的Guard可以跳过Rails启动时间。相关测试通常在保存文件后几秒钟内完成。在RSpec中使用:focus标记来限制在特定区域工作时运行的测试。如果是大型测试套件,请在Guardfile中设置all_after_pass: false, all_on_start: false,使其仅在需要时运行所有测试套件。

  • 我在每次测试中使用多个断言。为每个断言执行相同的设置代码将大大增加测试时间。RSpec将打印出失败的行,以便很容易找到它。

我发现mocking会增加测试的脆性,这就是我避免它的原因。诚然,它可以很好地帮助OO设计,但在Rails应用程序的结构中,这感觉没有那么有效。相反,我非常依赖重构,让代码本身告诉我设计应该如何进行。

这种方法在没有广泛、复杂的域逻辑的中小型Rails应用程序上效果最好。

精彩的问题和精彩的讨论@ryanb和@bkeepers提到他们只编写两种类型的测试。我采取了类似的方法,但有第三种类型的测试:

  • 单元测试:针对普通ruby对象的独立测试,通常但并不总是如此。我的单元测试不涉及DB、第三方API调用或任何其他外部内容
  • 集成测试:这些测试仍然集中在测试一个类上;不同之处在于,它们将该类与我在单元测试中避免的外部内容集成在一起。我的模型通常同时有单元测试和集成测试,其中单元测试集中在可以在没有涉及DB的情况下进行测试的纯逻辑中,而集成测试将涉及DB。此外,我倾向于使用集成测试来测试第三方API包装器,使用VCR来保持测试的快速性和确定性,但让我的CI构建实现真实的HTTP请求(以捕捉任何API更改)
  • 验收测试:针对整个功能的端到端测试。这不仅仅是通过水豚进行UI测试;我在我的gems中也这样做,它可能根本没有HTML UI。在这些情况下,这将锻炼gem端到端所做的一切。我也倾向于在这些测试中使用VCR(如果他们发出外部HTTP请求),并且像在集成测试中一样,我的CI构建是为了使HTTP请求成为真实的

就嘲讽而言,我没有"一刀切"的方法。我过去确实做过过度锁定,但我仍然发现这是一种非常有用的技术,尤其是在使用rspec fire这样的技术时。一般来说,我会嘲笑合作者自由扮演角色(尤其是如果我拥有他们,而且他们是服务对象),并在大多数其他情况下尽量避免这种情况。

在过去一年左右的时间里,我的测试最大的变化可能是受到DAS的启发:虽然我过去有一个spec_helper.rb来加载整个环境,但现在我只显式地加载被测试的类(以及任何依赖项)。除了提高了测试速度(这确实有很大的不同!)之外,它还帮助我识别测试中的类何时引入了太多依赖项。

最新更新