在ruby世界中有许多好用的测试框架,rspec算是其中比较流行的一个。rspec使用了和直接测试方法的不同测试思路——测试应用的行为。下面我就来解释一下怎样实用rspec来测试应用。
Rspec入门
RSpec使用describe块及其近亲context块来封装测试代码。通常在单元测试时我们使用describe来描述类的行为:
describe Hash do
end
it块被用来编写具体测试代码。下面这个例子就是为Hash类编写的一个spec测试:
describe Hash do
it "should return a blank instance" do
Hash.new.should == {}
end
end
上面就是Rspec的基础示例了。在用命令gem install rsepc安装好rspec,然后将上面的代码放入hash_spec.rb文件,键入下面的命令执行测试:
$ rspec hash_spec.rb
测试的输出结果如下:
.
Finished in 0.11021 seconds
1 example, 0 failures
你也可以实用before和after来设置测试前后需要执行的代码,在before块和after块中的设置在describe块中也有效:
describe Hash do
before do
@hash = Hash.new({:hello => 'world'})
end
it "should return a blank instance" do
Hash.new.should == {}
end
it "hash the correct information in a key" do
@hash[:hello].should == 'world'
end
end
上面的代码会在每个测试块执行前创建一个 @hash变量。before块可以携带两个参数——all(一次性为所有的测试块设置变量)或each(每个测试块单独设置变量)。after块和before块一样也有这两个参数,不同的仅仅是after是在测试块代码之后执行。在前一个测试执行结束需要销毁起状态时after块很有用。
RSpec术语
通常,我们使用describe来描述方法。在方法名前使用点号“.”表示测试的是类方法,实用“#”表示测试的是实例方法,如下所示:
describe MyClass do
describe ".class_method_1" do
end
describe "#instance_method_1" do
end
end
context方法主要用来将一群具有相同上下文的测试集合在一起。当使用复杂的setup和teardown代码来设置对象时特别有用。下面以真实生活中的汉堡类为例。
假设我们要测试汉堡类Burger的#apply_ketchup方法。然而有的人却不想加番茄酱,于是我们为这两种人分别编写测试:
describe Burger do
describe "#apply_ketchup" do
context "with ketchup" do
before do
@burger = Burger.new(:ketchup => true)
@burger.apply_ketchup
end
it "sets the ketchup flag to true" do
@burger.has_ketchup_on_it?.should be_true
end
end
context "without ketchup" do
before do
@burger = Burger.new(:ketchup => false)
@burger.apply_ketchup
end
it "sets the ketchup flag to false" do
@burger.has_ketchup_on_it?.should be_false
end
end
end
end
整理一下
上面的代码可以正常工作,但是却有点重复了。RSpec为我们提供了一些helper来简化代码。我们可以使用let关键字来重写上述代码,使得变量在第一次被访问时能够自动创建:
describe Burger do
describe "#apply_ketchup" do
context "with ketchup" do
let(:burger) { Burger.new(:ketchup => true) }
before { burger.apply_ketchup }
it "sets the ketchup flag to true" do
burger.has_ketchup_on_it?.should be_true
end
end
context "without ketchup" do
let(:burger) { Burger.new(:ketchup => false) }
before { burger.apply_ketchup }
it "sets the ketchup flag to false" do
burger.has_ketchup_on_it?.should be_false
end
end
end
end
代码变得好看了些,但我们可以使用subject方法来进一步简化。subject指定的对象是rspec目前正在进行的测试针对的对象。下面我将结合specify方法使用它。specify方法和it十分相似,唯一区别是specify将测试内容作为测试描述:
describe Burger do
describe "#apply_ketchup" do
subject { burger }
before { burger.apply_ketchup }
context "with ketchup" do
let(:burger) { Burger.new(:ketchup => true) }
specify { subject.has_ketchup_on_it?.should be_true }
end
context "without ketchup" do
let(:burger) { Burger.new(:ketchup => true) }
specify { subject.has_ketchup_on_it?.should be_false }
end
end
end
按照rspec的约定,它会寻找以has开头并且以问号结尾的方法,下面是最终的burger_spec.rb:
class Burger
attr_reader :options
def initialize(options={})
@options = options
end
def apply_ketchup
@ketchup = @options[:ketchup]
end
def has_ketchup_on_it?
@ketchup
end
end
describe Burger do
describe "#apply_ketchup" do
subject { burger }
before { burger.apply_ketchup }
context "with ketchup" do
let(:burger) { Burger.new(:ketchup => true) }
it { should have_ketchup_on_it }
end
context "without ketchup" do
let(:burger) { Burger.new(:ketchup => false) }
it { should_not have_ketchup_on_it }
end
end
end
汉堡需要的不仅仅是番茄酱
在这个简短的教程中我们创建了burger类并用rspec进行了测试,并且我们根据rspec的习惯使测试代码更符合rspec的风格。我们的目的是为了更好的可读性和更好的运行代码。以后我会书写更多的符合rspec的测试,但现在,该吃午饭了。
------------------------------------------原文结束-------------------------------------------------------------
附:RSpec断言规则
RSpec有一些常见的断言规则。Ruby的断言方法是以问号结尾并且返回true或false的方法,常见的如: empty? nil? instance_of? 等。在spec中的断言很简单,就是
should be_去掉问号的断言方法。如:
[].should be_empty => [].empty? #passes
[].should_not be_empty => [].empty? #fails
除了用"be_"来前缀断言方法,也可以用"be_a_"和"be_an_"前缀,使得代码读起来更加自然:
"a string".should be_an_instance_of(String) =>"a string".instance_of?(String) #passes
3.should be_a_kind_of(Fixnum) => 3.kind_of?(Numeric) #passes
3.should be_a_kind_of(Numeric) => 3.kind_of?(Numeric) #passes
3.should be_an_instance_of(Fixnum) => 3.instance_of?(Fixnum) #passes
3.should_not be_instance_of(Numeric) => 3.instance_of?(Numeric) #fails
Rspec也会为诸如“has_key?”之类的断言创建匹配器,要使用该特性,在断言对象上使用 should have_key(:key) 就可以了,rspec会自动在对象上调用has_key?(:key)。如:
{:a => "A"}.should have_key(:a) => {:a => "A"}.has_key?(:a) #passes
{:a => "A"}.should have_key(:b) => {:a => "A"}.has_key?(:b) #fails
还有一些常见的断言方法如: be be_close change eql have be_true be_false be_nil include raise_error respond_to throw_symbol 等等