ruby on rails中使用 RSpec 进行行为驱动测试
来源:互联网 发布:查看windows更新日志 编辑:程序博客网 时间:2024/04/29 22:38
简介: 测试热潮现在传播到了 Ruby 编程社区,并且愈演愈热。在过去一年里,测试领域中最为瞩目的创新应属 RSpec 的引入和快速发展,这是一种行为驱动测试工具。通过本文了解 RSpec 如何改变人们思考测试的方式。
在过去十年中,软件开发人员对测试的热情日渐低迷。同一时期出现的动态语言并没有提供编译程序来捕捉最基本的错误,这使得测试变得更加重要。随着测试社区的成长,开发人员开始注意到,除了捕获bug等最基本的优点外,测试还具有以下优势:
测试能够改进您的设计。进行测试的每个目标对象必须具备至少两个客户机:生产代码和测试用例。这些客户机强制您对代码进行解耦。测试还鼓励开发人员使用更小、更简单的方法。
测试减少了不必要的代码。在编写测试用例时,您养成了很好的测试习惯,即只编写运行测试用例所需的最少代码。您抵制住了对功能进行编码的诱惑,因为您目前还不需要它。
推动了测试优先开发。您编写的每个测试用例会确定一个小问题。使用代码解决这个问题非常有用并且可以推动开发。当我进行测试驱动开发时,时间过得飞快。
测试提供了更多的自主权。在使用测试用例捕获可能的错误时,您会发现自己非常愿意对代码进行改进。
测试驱动的开发和RSpec
有关测试的优点无需赘述,我将向您介绍一个简单的使用RSpec的测试驱动开发示例。RSpec工具是一个Ruby软件包,可以用它构建有关您的软件的规范。该规范实际上是一个描述系统行为的测试。使用RSpec的开发流程如下:
编写一个测试。该测试描述系统中某个较小元素的行为。
运行测试。由于尚没有为系统中的相应部分构建代码,测试失败。这一重要步骤将测试您的测试用例,检验测试用例是否在应当失败的时候失败。
编写足够的代码,使测试通过。
运行测试,检验测试是否成功。
实质上,RSpec开发人员所做的工作就是将失败的测试用例调试为成功的测试用例。这是一个主动的过程。本文中,我将介绍RSpec的基本用法。
首先,假设您已安装了Ruby和gems。您还需要安装RSpec。输入下面的内容:
gem install rspec
使用示例
接下来,我将逐步构建一个状态机。我将遵循TDD规则。首先编写自己的测试用例,并且直到测试用例需要时才编写代码。Rake的创建者JimWeirich认为这有助于角色扮演。在编写实际的生产代码时,您希望充当一回jerk开发人员的角色,只完成最少量的工作来使测试通过。在编写测试时,您则扮演测试人员的角色,试图为开发人员提供一些有益的帮助。
以下的示例展示了如何构建一个状态机。状态机具有多种状态。每种状态支持可以转换状态机状态的事件。测试驱动开发入门的关键就是从零入手,尽量少地使用假设条件。针对测试进行程序设计。
使用清单1的内容创建名为machine_spec.rb的文件。该文件就是您的规范。您还不了解machine.rb文件的作用,目前先创建一个空文件。
清单1.最初的machine_spec.rb文件
require 'machine'
接下来,需要运行测试。始终通过输入 spec machine_spec.rb
运行测试。清单2展示了预料之中的测试失败:
清单2.运行空的规范
~/rspec batate$ spec machine_spec.rb /opt/local/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require': no such file to load -- machine (LoadError) from /opt/local/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require' from ./state_machine_spec.rb:1 from ...
在测试驱动开发中,您需要进行增量开发,因此在进行下一次开发前,需要先解决此次测试出现的问题。现在,我将扮演jerk开发人员的角色,即只完成满足应用程序运行所需的最少工作量。我将创建一个名为machine.rb的空文件,使测试通过。我现在可以以逸待劳,测试通过而我几乎没做任何事情。
继续角色扮演。我现在扮演一个烦躁的测试人员,促使jerk开发人员做些实际的工作。我将编码以下规范,需要使用 Machine
类,如清单3所示:
清单3.初始规范
require 'machine'describe Machine do before :each do @machine = Machine endend
该规范描述了目前尚不存在的 Machine
类。describe
方法提供了RSpec描述,您将传入测试类的名称和包含实际规范的代码块。通常,测试用例需要执行一定数量的设置工作。在RSpec中,将由 before
方法完成这些设置工作。您向 before
方法传递一个可选的标志和一个代码块。代码块中包含设置工作。标志确定RSpec执行代码块的频率。默认的标志为 :each
,表示RSpec将在每次测试之前调用setup代码块。您也可以指定 :all
,表示RSpec在执行所有测试之前只调用一次 before
代码块。您应该始终使用:each
,使各个测试彼此独立。
输入 spec
运行测试,如清单4所示:
清单4.存在性测试失败
~/rspec batate$ spec machine_spec.rb ./machine_spec.rb:3: uninitialized constant Machine (NameError)
现在,烦躁的测试人员要促使jerk开发人员做点什么了—jerk开发人员现在需要创建某个类。对我来说,就是修复测试出现的错误。在 machine.rb
中,我输入最少量的代码,如清单5所示:
清单5.创建初始Machine类
class Machineend
保存文件,然后运行测试。毫无疑问,清单6显示的测试报告没有出现错误:
清单6.测试Machine是否存在
~/rspec batate$ spec machine_spec.rb Finished in 5.0e-06 seconds0 examples, 0 failures
编写行为
现在,我可以开始实现更多的行为。我知道,所有状态机必须在某些初始状态下启动。目前我还不是很清楚如何设计这个行为,因此我先编写一个非常简单的测试,首先假设 state
方法会返回 :initial
标志。我对 machine_spec.rb
进行修改并运行测试,如清单7所示:
清单7.实现初始状态并运行测试
require 'machine'describe Machine do before :each do @machine = Machine.new end it "should initially have a state of :initial" do @machine.state.should == :initial end end~/rspec batate$ spec machine_spec.rb F1)NoMethodError in 'Machine should initially have a state of :initial'undefined method `state' for #<Machine:0x10c7f8c>./machine_spec.rb:9:Finished in 0.005577 seconds1 example, 1 failure
注意这条规则:
it"should initially have a state of :initial" do @machine.state.should == :initialend
。首先注意到这条规则读起来像是一个英文句子。删除标点,将得到 itshould initially have a state ofinitial
。然后会注意到这条规则并不像是典型的面向对象代码。它确实不是。您现在有一个方法,称为 it
。该方法具有一个使用引号括起来的字符串参数和一个代码块。字符串应该描述测试需求。最后,do
和 end
之间的代码块包含测试用例的代码。
可以看到,测试进度划分得很细。这些微小的步骤产生的收益却很大。它们使我能够改进测试密度,提供时间供我思考期望的行为以及实现行为所需的API。这些步骤还能使我在开发期间跟踪代码覆盖情况,从而构建更加丰富的规范。
这种风格的测试具有双重作用:测试实现并在测试的同时构建需求设计文档。稍后,我将通过测试用例构建一个需求列表。
我使用最简单的方式修复了测试,返回 :initial
,如清单8所示:
清单8.指定初始状态
class Machine def state :initial endend
当查看实现时,您可能会放声大笑或感觉受到了愚弄。对于测试驱动开发,您必须稍微改变一下思考方式。您的目标并不是编写最终的生产代码,至少现在不是。您的目标是使测试通过。当掌握以这种方式工作时,您可能会发现新的实现,并且编写的代码要远远少于采用TDD时编写的代码。
下一步是运行代码,查看它是否通过测试:
清单9.运行初始状态测试
~/rspec batate$ spec machine_spec.rb .Finished in 0.005364 seconds1 example, 0 failures
花些时间思考一下这个通过测试的迭代。如果查看代码的话,您可能会觉得气馁。因为并没有取得什么进展。如果查看整个迭代,将看到更多内容:您捕获了一个重要需求并编写测试用例实现需求。作为一名程序员,我的第一个行为测试帮助我明确了开发过程。因为实现细节随着测试的进行越来越清晰。
现在,我可以实现一个更健壮的状态实现。具体来讲,我需要处理状态机的多个状态。我需要创建一个新的规则获取有效状态列表。像以前一样,我将运行测试并查看是否通过。
清单10.实现有效状态规范
it "should remember a list of valid states" do @machine.states = [:shopping, :checking_out] @machine.states.should = [:shopping, :checking_out] end run test(note: failing first verifies test)~/rspec batate$ spec machine_spec.rb .F1)NoMethodError in 'Machine should remember a list of valid states'undefined method `states=' for #<Machine:0x10c7154>./machine_spec.rb:13:Finished in 0.005923 seconds2 examples, 1 failure
在清单10中,出现了一个RSpec形式的断言。该断言从 should
方法开始,然后添加了一些比较关系。should
方法对应用程序进行某种观察。工作中的应用程序应该以某种方式运行。should
方法很好地捕获了这种需求。在本例中,我的状态机应该记忆两种不同的状态。
现在,应该添加一个实例变量来实际记忆状态。像以往一样,我在修改代码后运行测试用例,并观察测试是否成功。
清单11.创建一个属性以记忆状态
class Machine attr_accessor :states def state :initial endend~/rspec batate$ spec machine_spec.rb ..Finished in 0.00606 seconds2 examples, 0 failures
驱动重构
此时,我并不想决定将 :initial
状态称为状态机的第一个状态。相反,我更希望第一个状态是状态数组中的第一个元素。我对状态机的理解在不断演变。这种现象并不少见。测试驱动开发经常迫使我重新考虑之前的假设。由于我已经通过测试用例捕获了早期需求,我可以轻松地对代码进行重构。在本例中,重构就是对代码进行调整,使其更好地工作。
修改第一个测试,使其如清单12所示,并运行测试:
清单12.初始状态应该为指定的第一个状态
it "should initially have a state of the first state" do @machine.states = [:shopping, :checking_out] @machine.state.should == :shoppingend~/rspec batate$ spec machine_spec.rb F.1)'Machine should initially have a state of the first state' FAILEDexpected :shopping, got :initial (using ==)./machine_spec.rb:10:Finished in 0.005846 seconds2 examples, 1 failure
可以这样说,测试用例起到作用了,因为它运行失败,因此我现在需要修改代码以使其工作。显而易见,我的任务就是使测试通过。我喜欢这种测试目的,因为我的测试用例正在驱动我进行设计。我将把初始状态传递给 new
方法。我将对实现稍作修改,以符合修改后的规范,如清单13所示。
清单13.指定初始状态
start to fix itclass Machine attr_accessor :states attr_reader :state def initialize(states) @states = states @state = @states[0] endend~/rspec batate$ spec machine_spec.rb 1)ArgumentError in 'Machine should initially have a state of the first state'wrong number of arguments (0 for 1)./machine_spec.rb:5:in `initialize'./machine_spec.rb:5:in `new'./machine_spec.rb:5:2)ArgumentError in 'Machine should remember a list of valid states'wrong number of arguments (0 for 1)./machine_spec.rb:5:in `initialize'./machine_spec.rb:5:in `new'./machine_spec.rb:5:Finished in 0.006391 seconds2 examples, 2 failures
现在,测试出现了一些错误。我找到了实现中的一些bug。测试用例不再使用正确的接口,因为我没有把初始状态传递给状态机。可以看到,测试用例已经起到了保护作用。我进行了较大的更改,测试就发现了bug。我们需要对测试进行重构以匹配新的接口,将初始状态列表传递给 new
方法。在这里我并没有重复初始化代码,而是将其放置在 before
方法中,如清单14所示:
清单14.在“before
”中初始化状态机
require 'machine'describe Machine do before :each do @machine = Machine.new([:shopping, :checking_out]) end it "should initially have a state of the first state" do @machine.state.should == :shopping end it "should remember a list of valid states" do @machine.states.should == [:shopping, :checking_out] end end~/rspec batate$ spec machine_spec.rb ..Finished in 0.005542 seconds2 examples, 0 failures
状态机开始逐渐成型。代码仍然有一些问题,但是正在向良好的方向演化。我将开始对状态机进行一些转换。这些转换将促使代码实际记忆当前状态。
测试用例促使我全面地思考API的设计。我需要知道如何表示事件和转换。首先,我将使用一个散列表表示转换,而没有使用成熟的面向对象实现。随后,测试需求可能会要求我修改假设条件,但是目前,我仍然保持这种简单性。清单15显示了修改后的代码:
清单15.添加事件和转换
remember events... change before conditionsrequire 'machine'describe Machine do before :each do @machine = Machine.new([:shopping, :checking_out]) @machine.events = {:checkout => {:from => :shopping, :to => :checking_out}} end it "should initially have a state of the first state" do @machine.state.should == :shopping end it "should remember a list of valid states" do @machine.states.should == [:shopping, :checking_out] end it "should remember a list of events with transitions" do @machine.events.should == {:checkout => {:from => :shopping, :to => :checking_out}} end end~/rspec batate$ spec machine_spec.rb FFF1)NoMethodError in 'Machine should initially have a state of the first state'undefined method `events=' for #<Machine:0x10c6f38>./machine_spec.rb:6:2)NoMethodError in 'Machine should remember a list of valid states'undefined method `events=' for #z7lt;Machine:0x10c5afc>./machine_spec.rb:6:3)NoMethodError in 'Machine should remember a list of events with transitions'undefined method `events=' for #<Machine:0x10c4a58>./machine_spec.rb:6:Finished in 0.006597 seconds3 examples, 3 failures
由于新的测试代码位于 before
中,将我的三个测试分解开来。尽管如此,清单16中展示的测试非常容易修复。我将添加另一个访问程序:
清单16.记忆事件
class Machine attr_accessor :states, :events attr_reader :state def initialize(states) @states = states @state = @states[0] endend~/rspec batate$ spec machine_spec.rb ...Finished in 0.00652 seconds3 examples, 0 failurestest
测试全部通过。我得到了一个能正常运行的状态机。接下来的几个测试将使它更加完善。
接近真实的应用程序
目前为止,我所做的不过是触发了一次状态转换,但是我已经做好了所有基础工作。我得到了一组需求。我还构建了一组测试。我的代码可以为状态机提供使用的数据。此时,管理单个状态机转换仅表示一次简单的转换,因此我将添加如清单17所示的测试:
清单17.构建状态机的状态转换
it "should transition to :checking_out upon #trigger(:checkout) event " do @machine.trigger(:checkout) @machine.state.should == :checking_outend~/rspec batate$ spec machine_spec.rb ...F1)NoMethodError in 'Machine should transition to :checking_out upon#trigger(:checkout) event 'undefined method `trigger' for #<Machine:0x10c4d00>./machine_spec.rb:24:Finished in 0.006153 seconds4 examples, 1 failure
我需要抵制快速构建大量功能的诱惑。我应该只编写少量代码,只要使测试通过即可。清单18展示的迭代将表示API和需求。这就足够了:
清单18.定义 trigger
方法
def trigger(event) @state = :checking_outend~/rspec batate$ spec machine_spec.rb ....Finished in 0.005959 seconds4 examples, 0 failures
这里出现了一个有趣的边注。在编写代码时,我两次都弄错了这个简单的方法。第一次我返回了 :checkout
;第二次我将状态设置为 :checkout
而不是 :checking_out
。在测试中使用较小的步骤可以为我节省大量时间,因为测试用例为我捕获的这些错误在将来的开发中很难捕获到。本文的最后一个步骤是实际执行一次状态机转换。在第一个示例中,我并不关心实际的机器状态是什么样子的。我仅仅是根据事件进行盲目转换,而不考虑状态。
两节点的状态机无法执行这个操作,我需要在第三个节点中构建。我没有使用已有的 before
方法,只是在新状态中添加另外的状态。我将在测试用例中进行两次转换,以确保状态机能够正确地执行转换,如清单19所示:
清单19.实现第一次转换
it "should transition to :success upon #trigger(:accept_card)" do @machine.events = { :checkout => {:from => :shopping, :to => :checking_out}, :accept_card => {:from => :checking_out, :to => :success} } @machine.trigger(:checkout) @machine.state.should == :checking_out @machine.trigger(:accept_card) @machine.state.should == :success end~/rspec batate$ spec machine_spec.rb ....F1)'Machine should transition to :success upon #trigger(:accept_card)' FAILEDexpected :success, got :checking_out (using ==)./machine_spec.rb:37:Finished in 0.007564 seconds5 examples, 1 failure
这个测试将使用 :checkout
和 :accept_card
事件建立新的状态机。在处理签出时,我选择使用两个事件而不是一个,这样可以防止发生双命令。签出代码可以确保状态机在签出之前处于 shopping
状态。第一次签出首先将状态机从 shopping
转换为checking_out
。测试用例通过触发 checkout
和 accept_card
事件实现两个转换,并在调用事件之后检验事件状态是否正确。与预期一样,测试用例失败— 我并没有编写处理多个转换的触发器方法。代码修正包含一行非常重要的代码。清单20展示了状态机的核心:
清单20.状态机的核心
def trigger(event) @state = events[event][:to] end~/rspec batate$ spec machine_spec.rb .....Finished in 0.006511 seconds5 examples, 0 failures
测试可以运行。这些粗糙的代码第一次演变为真正可以称之为状态机的东西。但是这还远远不够。目前,状态机缺乏严密性。不管状态如何,状态机都会触发事件。例如,当处于 shopping
状态时,触发 :accept_card
并不会转换为 :success
状态。您只能够从:checking_out
状态触发 :accept_card
。在编程术语中,trigger
方法的范围应针对事件。我将编写一个测试来解决问题,然后修复bug。我将编写一个负测试(negativetest),即断言一个不应该出现的行为,如清单21所示:
清单21:负测试
it "should not transition from :shopping to :success upon :accept_card" do @machine.events = { :checkout => {:from => :shopping, :to => :checking_out}, :accept_card => {:from => :checking_out, :to => :success} } @machine.trigger(:accept_card) @machine.state.should_not == :success end rspec batate$ spec machine_spec.rb .....F1)'Machine should not transition from :shopping to :success upon :accept_card' FAILEDexpected not == :success, got :success./machine_spec.rb:47:Finished in 0.006582 seconds6 examples, 1 failure
现在可以再次运行测试,其中一个测试如预期一样运行失败。修复代码同样只有一行,如清单22所示:
清单22.修复 trigger
中的范围问题
def trigger(event) @state = events[event][:to] if state == events[event][:from] endrspec batate$ spec machine_spec.rb ......Finished in 0.006873 seconds6 examples, 0 failures
组合代码
现在,我具有一个可简单运行的状态机。无论从哪方面来说,它都不是一个完美的程序。它还具有下面这些问题:
状态散列实际上不具备任何功能。我应该根据状态对事件及其转换进行验证,或者将所有状态集中起来。后续需求很可能会要求这样做。
某个既定事件只能存在于一个状态中。这种限制并不合理。例如,
submit
和cancel
事件可能需要处于多个状态。代码并不具备明显的面向对象特征。为使配置保持简单,我将大量数据置入散列中。后续的迭代会进一步驱动设计,使其朝面向对象设计方向发展。
但是,您还可以看到,这个状态机已经能够满足一些需求了。我还具备一个描述系统行为的文档,这是进行一系列测试的好起点。每个测试用例都支持系统的一个基本需求。事实上,通过运行 specmachine_spec.rb --format specdoc
,您可以查看由系统规范组成的基本报告,如清单23所示:
清单23.查看规范
spec machine_spec.rb --format specdocMachine- should initially have a state of the first state- should remember a list of valid states- should remember a list of events with transitions- should transition to :checking_out upon #trigger(:checkout) event - should transition to :success upon #trigger(:accept_card)- should not transition from :shopping to :success upon :accept_cardFinished in 0.006868 seconds
测试驱动方法并不适合所有人,但是越来越多的人开始使用这种技术,使用它构建具有灵活性和适应性的高质量代码,并且根据测试从头构建代码。当然,您也可以通过其他框架(如test_unit)获得相同的优点。RSpec还提供了优秀的实现方法。这种新测试框架的一大亮点就是代码的表示。新手尤其可以从这种行为驱动的测试方法中受益。请尝试使用该框架并告诉我您的感受。
- ruby on rails中使用 RSpec 进行行为驱动测试
- 使用 RSpec 进行行为驱动测试
- 使用 RSpec 进行行为驱动测试
- 使用 RSpec 进行行为驱动测试
- ruby on rails rspec
- ruby on rails 测试驱动
- 使用rbenv进行Ruby on Rails 环境搭建@CentOS 7-2-Ruby on Rails测试
- 使用Rspec进行rails测试详解1-搭建测试环境
- Ruby on rails /rspec undefine 'visit'
- 使用Rspec进行rails测试详解1-Transactions
- ruby on rails测试
- how to test observe with rspec in ruby on rails.
- Ubuntu11.10+ruby on rails 3+rspec+autotest+spork配置
- Ruby on Rails,使用Rails Console进行ActiveRecord调试
- rails中写rspec测试代码
- Rspec-ruby中的BDD(行为驱动开发)
- [基础]Ruby中使用Rspec 和Rake(非Rails环境)
- 在Ruby on Rails中使用FCKeditor
- JS实现简单悬浮框
- 强力删除顽固文件夹
- 《给你的答复:关于朋友或怀疑朋友或怀疑》
- 求素数
- 西行漫记—逐浪软件篇
- ruby on rails中使用 RSpec 进行行为驱动测试
- Proguard混淆器
- c/c++/delphi中存储格式转换为java中格式
- [Openwrt] WR703N 启动信息, 4M和8M的UBoot
- UML中各种符号
- 一个超酷的横向多列响应式布局效果
- TelephonyManager有什么信息
- Windows和Linux下socket的区别
- Flash Builder4.5的破解