ruby on rails
在上一篇有关RSpec的文章发表后,一些读者走近我,询问在RSpec中模拟对象可能是什么样的。 我必须承认,我上次故意避开了模拟框架这一主题。
RSpec团队进行了内部讨论,讨论的是继续投资以建立和维护模拟框架还是采用现有框架之一。 在本文中,我将向您介绍存根和模拟的基础知识。 然后,我将向您展示使用三个流行的Ruby框架的示例。
问题
几乎所有用于测试自动化的策略都依赖于一些基本概念。 测试必须是:
- 可重复的。 如果要自动化测试,则测试用例每次应返回相同的结果,以便您可以验证那些结果。
- 快速。 如果您的测试用例太慢,您将不会运行它们,它们也不会给您带来任何好处。
- 简单。 如果测试很难编写,则不会编写。
出于这些原因,优秀的程序员正在寻找方法来替代缓慢,不可预测或复杂的应用程序。 经过多年的经验积累,该行业已经确定了至少两项重要的技术来替换系统的不同部分,就像建筑商可能会在建造过程中搭建临时脚手架来支撑结构一样。
不是存根
四年前,ThoughtWorks的马丁·福勒发表了一篇名为嘲弄一个著名的博客文章是不是存根 (见相关信息中的链接)。 四年后,大多数开发人员仍然对概念感到困惑。 (附带说明,也许我们已经选择了一些不良名称来描述这些概念,但是现在我们仍然坚持使用它们。)让我用一个类比来澄清这些概念。
每个圣诞节,我和我的妻子都离开圣诞树装饰圣诞树。 举起那棵树应该是一种珍贵的回忆,但是对我来说,这是一场噩梦。 我必须处理灯。 每年,少数灯会晃动,将一整串灯熄灭。 实际上,这项工作很简单。 我必须找到坏灯泡。 我只是在外面的灯串上工作,测试每根灯。 我可以至少以两种不同的方式测试每个。 这些技术就像存根和嘲笑。
存根
我了解到,我不应该只拿一个工作的灯泡,而是尝试更换线束中的每个灯泡。 可能有多个灯泡熄灭。 相反,我测试每个灯泡。 两种技术都有效。 第一项测试技术是取出灯泡,然后将其放置在电池供电的小灯下。 如果灯泡点亮,则灯泡工作,然后继续进行下一个。 如果小恶魔没有亮起来,我用锤子将其捣成纸浆,然后用有效的锤子代替。 你可以看到我在做什么。 我用一台简单得多的机器代替了复杂的灯串。 然后,我通过给灯泡通电来锻炼灯泡,观察结果,然后通过或未通过相应的测试。
现在,将灯泡视为类或组件。 使用该对象的客户端代码是轻链。 套接字是接口。 存根保留接口,但替换被测对象的部分或全部客户端代码 。
嘲笑
模拟对象相似,但稍微复杂一些。 如果我没有灯泡测试仪,则可以轻松使用称为电压表的工具。 电压表可测量电流是否流过灯泡。 这个过程是相似的。 我取出每个灯泡,并用电压表进行测试,将两个电极靠在灯泡上。 电压表试图将少量电流通过电路,然后测量电流是否流过。 如果您稍稍张开头脑,您会发现我实际上正在做的是通过电压表测量灯泡如何使用电路。 因此,模拟对象是一种存根。 它替换了使用被测对象的客户端代码。 但是模拟对象的功能更多。 它测量被测对象实际如何使用客户端代码。
数据库存根可能会通过返回假记录来响应查询。 模拟也可以做到这一点,但是模拟数据库对象还可以确保客户端代码调用了某些查询,并最终调用了close
以关闭连接。 当您的测试取决于接口的使用方式时,应使用模拟,而在根本不关心的情况下,应使用存根。
不足之处
存根和模拟是强大的技术,可以提高测试用例的速度,隔离代码,简化测试,并且(有人认为)可以解决世界饥饿问题。 您知道总会有一个很大的“但是”。 使用嘲笑和存根,您必须注意不要替换太多的真实世界。 我曾与许多客户合作,他们最终挑出了任何与现实世界类似的东西。 请记住,在某些时候,您需要与应用程序的其余部分一起测试该讨厌的数据库代码。 考虑一下自己被警告。
存根基础
在为任何框架构建任何存根时,您的方法基本上是相同的。 您必须做两件事:
- 用存根替换测试用例中的一部分真实系统。
- 复制存根中所需的真实行为。
您可能决定替换整个对象,或者只是更改单个方法的结果。 当您要替换整个对象时,这很容易-只需编写新对象并在测试用例中替换它即可。 在某些语言中,方法可能更难一些,但是使用动态语言(如Ruby),存根很容易,因为您只是在重新定义方法。 ew。 这种理论变得有点抽象。 是时候将这些想法付诸实践了。
假设您有一个打印文档的用户界面。 您要测试处理打印失败的代码。 您真的不在乎是否调用了print方法。 您只想验证如果您的代码确实调用了print,那么该代码将有效地处理失败的打印。 想一想伪代码语句是什么样子。
在文档对象的实例上,对print方法进行存根处理,返回false。
您可以将该句子分为两部分。 第一部分标识要存根的内容。 第二部分定义了存根应该做什么。 现在,该使用三个框架来构建该存根了。
摩卡/ Stubba
目前,Mocha是顶级的模拟和存根框架之一。 您可以通过gem轻松安装它:
清单1.安装Mocha
batate$ gem install mocha
Bulk updating Gem source index for: http://gems.rubyforge.org
Successfully installed mocha-0.5.5
Installing ri documentation for mocha-0.5.5...
Installing RDoc documentation for mocha-0.5.5...
为了演示它,我将需要一个应用程序。 清单2中的应用程序将包含一个称为Document
的模型和一个名为View
。 用户界面会将打印委托给Document
。
清单2. document.rb中的应用程序
class Document
def print
# doesn't matter -- we are stubbing it out
end
end
class View
attr :document
def initialize(document)
@document = document
end
def print()
if document.print
puts "Excellent!"
true
else
puts "Bummer."
false
end
end
end
我不会为Document
类构建更多的外壳,因为无论如何我都会将其存根。 实际上,存根框架的一大优点是它们使您能够以较小的增量进行测试驱动的开发。 在这种情况下,我不必具有完整的文档模型来构建用户界面。
清单3显示了测试用例。
清单3.使用Mocha进行测试
require 'test/unit'
require 'rubygems'
require 'mocha'
require 'document'
class ViewTest < Test::Unit::TestCase
def test_should_return_false_for_failed_print
document = stub("my document")
document.stubs(:print).returns(false)
ui = View.new(document)
assert_equal false, ui.print
end
end
您会看到它是如何工作的。 我使用语句stub("my document")
构建一个简单的名为stub的对象。 然后,用代码行document.stubs(:print).returns(false)
定义存根内打印的行为。 如果仔细看,这行代码与之前的伪代码非常相似:
在文档对象的实例上,对print方法进行存根处理,返回false。
我可以将测试方法的前两行简化为:
清单4.简化测试
Document.any_instance.stubs(:print).returns(false)
清单4中的版本将print
存根替换为Document
类的任何实例。 在Ruby框架中进行存根的好处是语法可能会稍有变化,但是底层的概念和结构完全相同。
用Flex Mock存根
与Mocha一起,Flex Mock是一个流行的模拟框架。 Jim Weirich是rake等日常Ruby工具的流行作者,他写了Flex Mock来处理Ruby测试中的基本模拟和存根。 就受欢迎程度和实用性而言,Mocha和Flex Mock非常相似。 使用gem可以轻松安装Flex Mock(另一个Jim Weirich项目):
清单5.安装Flex Mock
gem install flexmock
您可以使用flexmock
方法创建存根。 您将提供一个散列,其中的键是要存根的方法的名称,而值是返回值。 您将使用清单6中所示的内容创建一个存根:
清单6.使用Flex Mock进行存根
require 'rubygems'
require 'test/unit'
require 'flexmock/test_unit'
require 'document'
class ViewTest < Test::Unit::TestCase
def test_should_return_false_for_failed_print
document = flexmock(:print => false)
ui = View.new(document)
assert_equal false, ui.print
end
end
语法略有不同,但是概念完全相同。 您将用简化的存根替换document
,该存根返回false
以print
。
规范
请记住,您可以将Mocha和Flex Mock与RSpec一起使用。 也就是说,使用RSpec创建存根的语法类似于Mocha。 清单7显示了如何。
清单7.使用RSpec存根
document.stub!(:print).and_return(false)
该语法与Mocha的语法非常相似。 使用RSpec的主要决定是是否使用可能很快被弃用的API或向RSpec添加另一个测试框架。 目前,RSpec的优点之一是它是一站式测试商店。 RSpec处理所有主要问题。 但是,当您同时查看Mocha和RSpec的语法时,您会同意在RSpec测试用例中使用Mocha不会造成太多损失。
模拟基础
如您所知,创建模拟对象就像创建存根。 区别在于存根是被动的,它只是模拟您为存根方法调用的实际解决方案。 模拟是活动的,它实际上测试了使用模拟对象的方式。 如果您未按照期望的方式使用它,则测试将失败。 这些是使用模拟的基本步骤:
- 用存根替换测试用例中的一部分真实系统。
- 复制存根中所需的真实行为。
- 确定您的期望。
- 测试后,将发生的事情与您的期望进行比较。
回想一下电压表和圣诞灯。 我拿出一个灯泡,代表被测物体。 然后,我用代表被测试对象的电压表替换了代表真实应用程序的那串灯。 我应该定义两个隐式步骤。 我使用电压表,是因为如果灯泡工作,我希望指针记录电流。 测试后,我看着电压表。 这些隐式步骤分别表示步骤3和步骤4。实际上,测试框架通常会在测试用例完成后处理步骤4。 一行伪代码为我的文档打印应用程序描述了一个模拟对象,如下所示:
对于document
对象,我希望调用
与Mocha,Flex Mock和RSpec进行模拟
通常,当您可以用一个句子来表达一个想法时,就可以用一行或两行Ruby代码来实现该想法。 清单8显示了非常简单的Mocha代码,可以将清单4中的存根更改为一个模拟。
清单8.简化测试
Document.any_instance.expects(:print).once.returns(false)
我将方法stubs
更改为expects
以反映我的期望。 我还添加了once
方法,以显示客户端代码应调用print
。 当且仅当被测应用程序在Document类的任何实例上完全调用一次print方法时,我的测试用例才会成功。
用于实现模拟的Flex Mock代码与Mocha版本几乎相同,尽管方法名称略有不同。 清单9显示了区别。
清单9.使用Flex Mock模拟
document = flexmock("my document")
document.should_receive(:print).times(1).and_return(false)
清单10在RSpec中显示了相同的内容:
清单10.使用RSpec进行模拟
document = mock("my document")
document.should_receive(:print).and_return(false)
您可以在这些模拟框架中添加很多限定符。 您可以为调用参数和调用次数指定期望值。 您可以存根和模拟完整对象,对象上的单个方法或类方法。 我建议您使用Flex Mock或Mocha进行模拟,至少直到有关弃用的讨论逐渐解决为止。
结论
如果您在过去的两年中一直活跃于Ruby,那么您会注意到在高级测试思想上的巨额投资。 一些测试工具已经存在很长时间了,但是现在大众开始注意到它了。 随着Ruby爱好者开发更多的测试用例,诸如模拟之类的技术将变得越来越普遍。
存根和模拟是各种Ruby编码器的宝贵技术。 测试优先的开发人员将喜欢模拟尚未完全开发的接口的功能。 具有慢速测试套件的程序员可以使用存根和模拟来删除昂贵的接口。 所有测试人员都必须使用这些技术来增加测试的可预测性。
我没有向您展示对Ruby中的模拟库的详尽介绍,而是向您展示了模拟背后的概念。 我还为您提供了三个最受欢迎的模拟库的快速回顾。 如果这些概念对您来说是新手,您仍然有很多东西要学习,但是您应该有一个进一步探索的坚实基础。 与往常一样,了解更多的最佳方法是获取编码!
翻译自: https://www.ibm.com/developerworks/web/library/wa-mockrails/index.html
ruby on rails