rspec 测试页面元素
在过去的十年中,软件开发人员已开始接受越来越低水平的测试。 动态语言的同时爆炸(没有提供编译器来捕获最基本的错误)使测试变得更加重要。 随着测试社区的发展,开发人员开始注意到除捕获错误的最基本好处之外的好处:
- 测试可以改善您的设计。 您测试的每个目标对象必须至少具有两个客户端:生产代码和测试用例。 这些客户端迫使您解耦代码。 测试还鼓励和奖励较小,更简单的方法。
- 测试减少了不必要的代码。 当您首先编写测试用例时,您养成了只编写足以使测试用例通过的代码的习惯。 减少了对代码功能的诱惑,因为以后可能需要它们。
- 测试优先的发展是有动力的。 您编写的每个测试用例都会产生一个小问题。 用代码解决该问题是有益和激励的。 当我进行测试驱动的开发时,时间会流逝。
- 测试允许更多的自由。 如果您的测试用例会捕获可能的错误,则会发现您更愿意对代码进行改进。
测试驱动的开发和RSpec
除了讲讲测试的优点之外,我将带您了解一个使用RSpec进行测试驱动的开发(TDD)的简单示例。 RSpec工具是一个Ruby软件包,可让您与软件一起建立规范。 该规范实际上是描述系统行为的测试。 这是使用RSpec进行开发的流程:
- 您编写一个测试。 此测试描述了系统中一小部分元件的行为。
- 您运行测试。 测试失败,因为您尚未为系统的那部分构建代码。 这个重要步骤测试您的测试用例,验证您的测试用例何时应该失败。
- 您编写了足够的代码以使测试通过。
- 您运行测试并验证它们是否通过。
本质上,RSpec开发人员整天将测试用例从红色(失败)变为绿色(通过)。 这是一个激励过程。 在本文中,我将引导您逐步了解RSpec的基础知识。
首先,假设您已安装Ruby和gems。 您还需要安装RSpec。 类型:
gem install rspec
开始一个例子
接下来,我逐步构建一个状态机。 我遵循TDD的规则。 我首先编写我的测试用例,直到我的测试用例使我这样做之前,我不会编写任何代码。 Rake的作者Jim Weirich说这有助于角色扮演。 在编写实际的生产代码时,您需要扮演一个混蛋开发人员的角色,该开发人员只会做最少的工作以使测试通过。 在编写测试时,您正在扮演可怜的测试人员的角色,该人员试图使开发人员做一些有用的事情。
以下示例显示了如何构建状态机。 如果您之前没有看过状态机,请查看resources 。 状态机具有多个状态。 这些状态中的每一个都支持将状态机从一个状态移动到另一状态的事件。 从测试驱动的开发开始的关键是从顶部开始,并做出尽可能少的假设。 让测试驱动您的设计。
使用清单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 ...
在测试驱动的开发中,您希望以较小的增量移动,因此在继续进行下一个之前,应先解决此问题。 现在,我将扮演混蛋角色扮演者的角色,该角色将足以使应用程序正常工作。 我将创建一个名为machine.rb的空文件,以使测试通过。 我现在可以高枕无忧了。 测试通过了,我几乎什么也没做。
角色扮演继续。 我把帽子改回生气的测试仪上,让这个混蛋实际上做点什么。 我将编写以下规范,该规范需要Machine
类的存在,如清单3所示:
清单3.初始规格
require 'machine'
describe Machine do
before :each do
@machine = Machine
end
end
该规范描述了一个不存在的类,称为Machine
。 RSpec的描述使用describe
方法,然后传递被测类的名称和具有实际规范的代码块。 通常,测试用例有一定数量的设置工作。 在RSpec中,您将设置工作放在before
一节中。 您为before
方法传递了一个可选的符号和一个代码块。 代码块包含设置工作。 该符号确定RSpec应多久执行一次代码块。 默认符号是:each
,这意味着RSpec将在每次测试之前调用设置块。 您还可以指定:all
,这意味着RSpec的将调用before
块一旦之前的所有测试。 您几乎应该始终使用:each
,以使每个测试独立且彼此独立。
通过输入spec
运行测试,如清单4所示:
清单4.整理存在测试
~/rspec batate$ spec machine_spec.rb
./machine_spec.rb:3: uninitialized constant Machine (NameError)
现在,恼怒的测试人员已经举起了混蛋的手,他必须醒来并创建课程。 我这个混蛋回应。 现在是解决错误的时候了。 在machine.rb
,输入最低要求,如清单5所示:
清单5.创建初始Machine类
class Machine
end
我保存文件,然后运行测试。 果然,清单6显示了测试报告没有错误:
清单6.测试机器的存在
~/rspec batate$ spec machine_spec.rb
Finished in 5.0e-06 seconds
0 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
F
1)
NoMethodError in 'Machine should initially have a state of :initial'
undefined method `state' for #<Machine:0x10c7f8c>
./machine_spec.rb:9:
Finished in 0.005577 seconds
1 example, 1 failure
注意规则的形式:@ it "should initially have a state of :initial" do @machine.state.should == :initial end
。 您会注意到的第一件事是,规则的读法像是英语句子。 删除标点符号,您会得到, it should initially have a state of initial.
接下来您会注意到,该规则看起来不像典型的面向对象的代码。 不是。 你有一个方法,叫做it
。 该方法采用用引号引起来的字符串参数和代码块。 该字符串应描述您正在测试的需求。 最后, do
和end
之间的代码块包括测试用例的代码。
您可以看到我的工作量很小。 这些小步骤有很多好处。 他们让我提高了测试的密度,让我有时间思考预期的行为以及随附的API。 这些小步骤还使我在开发过程中可以紧跟代码覆盖范围,并建立更丰富的规范。
这种测试风格具有双重目的:测试实现,并在测试时构建需求设计文档。 稍后您将看到我从测试用例创建需求列表。
我以最简单的方式修复测试,返回:initial
,如清单8所示:
清单8.分配初始状态
class Machine
def state
:initial
end
end
在阅读实现时,您可能会大声笑出来或感到有些受骗。 对于测试驱动的开发,您必须稍微改变一下思路。 您的目标不是至少立即写出最终的生产代码。 您的目标是使测试用例通过。 当您学习以这种方式工作时,您可能会发现出现了新的实现,并且您编写的代码将比采用TDD之前编写的代码少得多。
下一步是运行代码,并观察代码是否通过:
清单9.运行初始状态测试。
~/rspec batate$ spec machine_spec.rb
.
Finished in 0.005364 seconds
1 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
.F
1)
NoMethodError in 'Machine should remember a list of valid states'
undefined method `states=' for #<Machine:0x10c7154>
./machine_spec.rb:13:
Finished in 0.005923 seconds
2 examples, 1 failure
在清单10中,您可以看到一个断言的RSpec形式。 断言从方法should
开始,然后添加某种比较。 should
方法会对应用程序进行某种观察。 工作中的应用程序应以某种方式运行。 should
方法很好地满足了这一要求。 在这种情况下,我的状态机应该记住两个不同的状态。
这次,您需要添加一个实例变量以实际记住状态。 和往常一样,我将在更改编码后运行测试用例,并观察测试用例是否成功。
清单11.创建一个属性来记住状态。
class Machine
attr_accessor :states
def state
:initial
end
end
~/rspec batate$ spec machine_spec.rb
..
Finished in 0.00606 seconds
2 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 == :shopping
end
~/rspec batate$ spec machine_spec.rb
F.
1)
'Machine should initially have a state of the first state' FAILED
expected :shopping, got :initial (using ==)
./machine_spec.rb:10:
Finished in 0.005846 seconds
2 examples, 1 failure
我可以说测试用例之所以成功是因为失败了,所以我可以自由地使代码起作用。 我的任务很清楚。 我需要使测试用例通过。 我喜欢事情的发展方向,因为我的测试用例正在推动我的设计。 我将把初始状态传递给new
方法。 我将稍稍更改实现以使其符合修订后的规范,如清单13所示。
清单13.分配初始状态。
start to fix it
class Machine
attr_accessor :states
attr_reader :state
def initialize(states)
@states = states
@state = @states[0]
end
end
~/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 seconds
2 examples, 2 failures
现在,这是出乎意料的。 我在实现过程中发现了一些错误。 测试用例不再使用正确的接口,因为我没有将初始状态传递给状态机。 我可以看到测试用例已经在保护我。 我进行了彻底的更改,测试用例捕获了该错误。 我们需要重构测试以匹配新接口,将状态的初始列表传递给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 seconds
2 examples, 0 failures
状态机开始成形。 该代码仍然存在一些问题,但是已经开始发展。 我将开始一些向状态机的转换。 这些转换将迫使代码实际记住当前状态。
我的测试用例迫使我仔细考虑API的设计。 我需要了解如何表示事件和过渡。 而不是从全面的面向对象的实现开始,我将在哈希表中表示过渡。 稍后,需求可能使我改变自己的假设,但是现在,我将保持简单。 清单15显示了修改后的测试代码:
清单15.添加事件和过渡
remember events... change before conditions
require '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
FFF
1)
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 seconds
3 examples, 3 failures
由于新的测试代码在before
,因此新代码破坏了我的所有三个测试。 清单16显示了这些测试易于修复。 我将添加另一个访问器:
清单16.记住事件
class Machine
attr_accessor :states, :events
attr_reader :state
def initialize(states)
@states = states
@state = @states[0]
end
end
~/rspec batate$ spec machine_spec.rb
...
Finished in 0.00652 seconds
3 examples, 0 failures
test
测试全部通过。 我要关闭正在工作的状态机。 接下来的几个测试将它们组合在一起。
变得真实
到目前为止,我还没有做任何事情来触发状态转换,但是所有基础工作都已完成。 我已经积累了一系列要求。 我已经建立了一组测试。 我有工作代码来设置状态机将使用的许多数据。 此时,管理一个状态机转换代表一个微小的转换,因此我将在清单17中简单地添加测试:
清单17.将状态转换构建到状态机
it "should transition to :checking_out upon #trigger(:checkout) event " do
@machine.trigger(:checkout)
@machine.state.should == :checking_out
end
~/rspec batate$ spec machine_spec.rb
...F
1)
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 seconds
4 examples, 1 failure
我需要抵制太早做太多的诱惑。 我应该只编写足够的代码以使测试用例通过。 清单18中的迭代将让我表达API和需求。 现在就足够了:
清单18.定义trigger
方法
def trigger(event)
@state = :checking_out
end
~/rspec batate$ spec machine_spec.rb
....
Finished in 0.005959 seconds
4 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
....F
1)
'Machine should transition to :success upon #trigger(:accept_card)' FAILED
expected :success, got :checking_out (using ==)
./machine_spec.rb:37:
Finished in 0.007564 seconds
5 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 seconds
5 examples, 0 failures
测试用例运行。 这段原始代码第一次演变为可以实际称为状态机的东西。 距离完成还很遥远。 现在,状态机太宽松了。 无论机器的状态如何,状态机都将触发事件。 例如,在shopping
状态下触发:accept_card
不应使您进入:success
状态。 您应该只能从:checking_out
状态触发:accept_card
。 用编程术语来说, trigger
方法的作用域应为事件。 我将通过编写测试然后修复错误来解决该问题。 我将编写一个否定测试,这意味着一个断言不应发生的行为,如清单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
.....F
1)
'Machine should not transition from :shopping to :success upon :accept_card' FAILED
expected not == :success, got :success
./machine_spec.rb:47:
Finished in 0.006582 seconds
6 examples, 1 failure
我现在可以再次运行测试,并且其中一个测试按预期中断。 该修补程序还是单线的,如清单22所示:
清单22.解决trigger
的范围问题
def trigger(event)
@state = events[event][:to] if state == events[event][:from]
end
rspec batate$ spec machine_spec.rb
......
Finished in 0.006873 seconds
6 examples, 0 failures
放在一起
至此,我有了一个简单的工作状态机。 凭空想像还不是完美的。 您可能会看到一些问题:
- 国家的哈希实际上并没有做任何事情。 我应该针对事件验证事件及其过渡,或者完全擦除状态数组。 未来的需求可能会决定结果。
- 给定事件只能在一种状态下存在。 这可能不是可接受的限制。 例如,
submit
和cancel
状态可能需要处于多个状态。 - 该代码不是特别面向对象的。 为了简化配置,我将大多数数据放在了散列中。 未来的迭代很可能将设计推向更加面向对象的设计。
但是您还可以看到该状态机已经满足了许多要求。 我也有描述系统行为的文档,并且是测试套件的一个良好的开端。 每个测试用例都备份了系统中的基本要求。 实际上,通过运行spec machine_spec.rb --format specdoc
可以看到由系统spec machine_spec.rb --format specdoc
组成的基本报告,如清单23所示:
清单23.查看规范
spec machine_spec.rb --format specdoc
Machine
- 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_card
Finished in 0.006868 seconds
测试驱动的方法并不适合每个人,但是越来越多的人使用该技术来构建灵活,适应性强的质量代码,并牢记测试的基础。 您肯定可以从其他框架(例如test_unit)中获得许多相同的好处。 RSpec还提供了一种出色的方法来带您到达那里。 新的测试框架体现了您使用它构建的代码的表现力。 初学者尤其可以从行为驱动的测试方法中受益。 尝试一下,让我知道您的想法。
翻译自: https://www.ibm.com/developerworks/web/library/wa-rspec/index.html
rspec 测试页面元素