测试:如何在不降低速度的情况下专注于行为而非实施?

krn*_*krn 43 ruby unit-testing rspec ruby-on-rails

似乎有两种完全不同的测试方法,我想引用它们.

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

Brandon Keepers:

理论是测试应该与实现无关.这导致较少的脆弱测试并且实际测试结果(或行为).

使用RSpec,我觉得完全模拟模型来测试控制器的常见方法最终会迫使您过多地研究控制器的实现.

这本身并不算太糟糕,但问题在于它与控制器过多地对齐以决定模型的使用方式.如果我的控制器调用Thing.new,为什么重要?如果我的控制器决定采取Thing.create怎么办?和救援路线?如果我的模型有一个特殊的初始化方法,比如Thing.build_with_foo怎么办?如果我改变实现,我的行为规范不应该失败.

当您拥有嵌套资源并为每个控制器创建多个模型时,此问题会变得更糟.我的一些设置方法最终会长达15行或更长,而且非常脆弱.

RSpec的目的是将您的控制器逻辑与模型完全隔离,理论上听起来不错,但几乎与Rails等集成堆栈相反.特别是如果你练习瘦的控制器/胖模型规则,控制器中的逻辑量变得非常小,并且设置变得很大.

那么BDD想做什么呢?退后一步,我真正想要测试的行为不是我的控制器调用Thing.new,而是给定参数X,它会创建一个新东西并重定向到它.

大卫切林斯基:

这都是权衡利弊.

AR选择继承而非委托的事实使我们处于测试绑定中 - 我们必须与数据库耦合或者我们必须更加密切地与实现相关联.我们接受这种设计选择,因为我们在表现力和干燥方面获益.

在应对困境时,我选择了更快的测试,但代价是更脆弱.你选择较少的脆弱测试,但代价是它们的运行速度稍慢.无论哪种方式,这都是一种权衡.

在实践中,我每天运行数百次(如果不是数千次)测试(我使用自动测试并采取非常精细的步骤)并且我改变我是否使用"新"或"创建"几乎从不.同样由于细化步骤,出现的新模型起初非常不稳定.valid_thing_attrs方法可以最大限度地减少这种痛苦,但它仍然意味着每个新的必填字段意味着我必须更改valid_thing_attrs.

但是如果你的方法在实践中对你有用,那就好了!事实上,我强烈建议您发布一个带有生成器的插件,这些插件可以按您喜欢的方式生成示例.我相信很多人都会从中受益.

瑞恩贝茨:

出于好奇,您在测试/规格中多久使用一次模拟?也许我做错了什么,但我发现它严重限制.自从一个月前切换到rSpec以来,我一直在他们推荐的文档中,控制器和视图层根本没有访问数据库,模型完全被模拟了.这为你提供了一个很好的速度提升并使一些事情变得更容易,但我发现这样做的缺点远远超过了职业选手.自从使用嘲笑以来,我的规格变成了维护噩梦.规范旨在测试行为,而不是实现.我不在乎是否调用了一个方法我只是想确保结果输出是正确的.因为mocking使得规范对于实现很挑剔,所以它使得简单的重构(不改变行为)不可能做到,而不必经常回去并"修复"规范.我非常鄙视规范/测试应涵盖的内容.测试应该只在应用程序中断时中断.这就是为什么我几乎不测试视图层的原因之一,因为我发现它太僵硬了.在视图中更改小东西时,它通常会导致测试中断,而不会破坏应用程序.我发现嘲笑有同样的问题.最重要的是,我今天才意识到,嘲笑/抄袭一个类方法(有时)会在规范之间徘徊.规格应该是自包含的,不受其他规格的影响.这打破了这个规则并导致棘手的错误.我从这一切中学到了什么?在使用模拟的地方要小心.Stubbing并不是那么糟糕,但仍有一些相同的问题.

我花了几个小时从我的规格中删除了几乎所有的模拟.我还使用控制器规范中的"integrate_views"将控制器和视图规范合并为一个.我还为每个控制器规范加载所有灯具,因此有一些测试数据可以填充视图.最终的结果?我的规格更短,更简单,更一致,更不僵硬,并且他们一起测试整个堆栈(模型,视图,控制器),因此没有错误可以穿过裂缝.我不是说这对每个人来说都是"正确"的方式.如果您的项目需要一个非常严格的规范案例,那么它可能不适合您,但在我的情况下,这比使用模拟之前的世界更好.我仍然认为存根是一个很好的解决方案,所以我仍然这样做.

小智 16

我认为这三种观点仍然完全有效.Ryan和我正在努力克服嘲弄的可维护性,而David认为维护权衡对于速度的提高是值得的.

但这些权衡是更深层次问题的症状,David在2007年提到过:ActiveRecord.ActiveRecord的设计鼓励您创建过多的神对象,对系统的其余部分了解太多,并且表面积太大.这导致测试太多而无法测试,对系统的其余部分了解太多,而且要么太慢或太脆.

那么解决方案是什么?尽可能多地将您的应用程序与框架分开.编写许多为您的域建模并且不从任何东西继承的小类.每个对象应该具有有限的表面区域(不超过几个方法)和通过构造函数传递的显式依赖项.

通过这种方法,我只编写了两种类型的测试:隔离单元测试和全栈系统测试.在隔离测试中,我模拟或存根不是被测对象的所有东西.这些测试非常快,通常甚至不需要加载整个Rails环境.完整的堆栈测试可以运行整个系统.它们非常缓慢,并在失败时提供无用的反馈.我尽可能少地写作,但足以让我相信我所有经过良好测试的对象都能很好地融合.

不幸的是,我不能指出你做得很好的示例项目(还).我在关于为什么我们的代码闻到的演讲中谈了一点,观看了Corey Haines关于快速Rails测试的演示,我强烈建议阅读以测试为导向的面向对象的成长软件.

  • 我仍然认为权衡控制器规格的速度是值得的,因为我们正在对不同的层进行存根/模拟.即使`new`和`create`来自ActiveRecord,如果它们是从控制器调用的,它们实际上是_your_模型上的公共API.但是,我确实认为值得在模型规范中进行权衡,因为你必须存在ActiveRecord的低级细节. (3认同)

rya*_*anb 9

感谢汇编2007年的报价.回顾过去很有趣.

我目前的测试方法在这个RailsCasts一集中有所涉及,我很满意.总之,我有两个级别的测试.

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

  • 低级别:这是测试所有复杂逻辑的地方 - 主要是模型和帮助程序.我也避免在这里嘲笑.必要时,测试会打到数据库或周围的对象.

请注意,没有控制器或视图规范.我觉得这些都在请求规范中得到了充分的考虑.

由于没有什么嘲弄,我如何快速保持测试?这里有一些提示.

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

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

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

  • 我每次测试都使用多个断言.为每个断言执行相同的设置代码将大大增加测试时间.RSpec将打印出失败的线路,因此很容易找到它.

我发现嘲弄增加了测试的脆弱性,这就是我避免它的原因.确实,它可以很好地作为OO设计的辅助,但在Rails应用程序的结构中,这并不是那么有效.相反,我非常依赖重构,让代码本身告诉我设计应该如何进行.

这种方法最适用于中小型Rails应用程序,没有广泛,复杂的域逻辑.


Myr*_*ton 8

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

  • 单元测试:隔离测试,通常但不总是针对普通红宝石对象.我的单元测试不涉及DB,第三方API调用或任何其他外部内容.
  • 集成测试:这些仍然专注于测试一个类; 不同之处在于它们将该类与我在单元测试中避免使用的外部内容集成在一起.我的模型通常都有单元测试和集成测试,其中单元测试集中在纯逻辑上,可以在没有DB的情况下进行测试,集成测试将涉及DB.此外,我倾向于使用集成测试来测试第三方API包装器,使用VCR来保持测试的快速性和确定性,但让我的CI构建使HTTP请求变为真实(以捕获任何API更改).
  • 验收测试:针对整个功能的端到端测试.这不仅仅是通过水豚进行UI测试; 我在我的宝石中也是如此,它可能根本没有HTML UI.在这些情况下,无论宝石端到端做什么,都可以进行锻炼.我也倾向于在这些测试中使用VCR(如果他们发出外部HTTP请求),就像我的集成测试一样,我的CI构建设置为使HTTP请求真实.

就模仿而言,我没有"一刀切"的方法.我在过去肯定过度拥挤,但我仍然认为它是一种非常有用的技术,特别是在使用像rspec-fire这样的东西时.一般来说,我嘲笑合作者自由地扮演角色(特别是如果我拥有它们,并且它们是服务对象)并且在大多数其他情况下试图避免它.

在过去一年左右的时间里,我测试的最大变化可能是受到了DAS的启发:虽然我曾经有一个spec_helper.rb加载整个环境的东西,但现在我只显式加载了测试中的类(以及任何依赖项).除了提高测试速度(这确实产生了巨大的差异!)之外,它还有助于我确定我的测试阶段何时会引入太多的依赖关系.