Previously I showed some unit tests written in
RSpec. Those test were fairly ugly, partially because I didn't really understand RSpec. In an effort to have better unit tests, I have learned a little more about RSpec.
Read the Latest Documentation
You'd think this would be obvious, but given the lack of links to the documentation on the various "here's advice on using RSpec" blogs, it must not be done a lot. Worse, somehow I had been only looking at the
old documentation for version 1 of RSpec. So, if you are going to use RSpec, I suggest you read latest documentation, which as of when I am writing this is
RSpec 2.6. This will definitely be helpful.
Make Tests Self Documenting
Each test (it or specify block) can take a description. Sometimes this is necessary, but any time you have documentation (and that's what this description is), you risk having the documentation be out of sync with the code. While
not everyone agrees, I am a fan of letting the tests document themselves, whenever possible.
Make Use of Context
Use the "
context" keyword to describe what you are doing in a "
before" block to set up the state necessary for testing.
describe Thing do
let(:thing) {Thing.new}
subject {thing}
context "empty object" do
#insert tests
end
context "with inherited object" do
let(:base) {Thing.new}
before(:each) do
base["foo"] = 5
thing.prototype = base
thing["baz"] = 15
end
#insert tests
end
end
Make Use of Describe
Use the "
describe" keyword to describe either the noun that is being tested, or the actions that are under test. i.e. if you have actions in a "
before" block that
are the actions being tested, you should you use "
describe" rather than "
context"
describe Thing do
let(:thing) {Thing.new}
subject {thing}
context "empty object" do
describe "when assigning via properties" do
before(:each) do
thing["foo"] = 5
thing["bar"] = "hello"
end
# insert checks
end
end
end
Make Use of Subject
If you have multiple tests on a single object, make it the RSpec Subject and put it in a
describes block. This way, all of your "
should" comparisons will be implicitly on this object.
describe Thing do
let(:thing) {Thing.new}
subject {thing}
context "empty object" do
describe "keys" do
subject {thing.keys}
it {should_not be_nil}
it {should be_empty}
end
end
end
Use Its
Often times you want to test the properties of an object. You can use the "
its" method to have the implicit subject of "
should" comparisons be the result of the method or array dereference specified.
describe Thing do
let(:thing) {Thing.new}
subject {thing}
context "empty object" do
its(['newProperty']) {should be_nil}
its('newMethod') {should be_nil}
end
end
Let and Subject are Lazy Loaded
The "variables" defined in "
let" calls and the "
subject" aren't actually evaluated until they are used. So if you never reference a variable specified in a "
let", then that code is never executed. This also means that order isn't important. i.e. the following will work:
let (:a) {b + 1}
let (:b) {5}
specify("show using let") {a.should == (b+1)}
Shared_examples and shared_contexts are Global
I actually haven't found this documented, so I may be doing something wrong. But I have found that if I have two different rspec files that each have a "
shared_examples_for 'test this object'", this causes problems. I can test each rspec file in isolation and it is fine. But if I try to test both at the same time, I get an error saying that a shared example already exists with the name "
test this object".
There are two different solutions to this problem that I have found. If the shared examples are the same, pull them into a common
Module that is included. Note, this is better than having repeated code anyway. If the examples are different, then you have to be more unique with the names of the shared examples.
Final Thoughts
It seems that the approach of RSpec is to write tests such that they are self descriptive and so that each test tests exactly one thing. While in theory, this sounds good, I am finding that this results in very verbose test files. That's even with the cleaning up that I have done after learning RSpec better. i.e. I like a lot of what RSpec does, but I am not convinced that it is the best way to write unit tests.
Rewritten Tests
Using what I have learned, I have rewritten the unit tests
from before. Here is what they look like now:
require 'spec_helper'
describe Thing do
let(:thing) {Thing.new}
subject {thing}
shared_examples_for "simple object" do |map, self_keys|
describe "keys" do
subject {thing.keys}
it {should have(map.size).items}
it {should include(*(map.keys))}
it {should_not include("noSuchProperty")}
end
describe "self_keys" do
subject {thing.self_keys}
before(:each) { self_keys ||= map.keys}
it {should have(self_keys.size).items}
it {should include(*self_keys)}
it {should_not include("noSuchProperty")}
end
describe "fields" do
map.each do |k, v|
its([k]) {should == v}
end
its(['noSuchProperty']) {should be_nil}
end
describe "methods" do
map.each do |k, v|
its(k) {should == v}
end
its('noSuchMethod') {should be_nil}
end
describe "to_hash" do
subject {thing.to_hash}
it {should_not be_nil}
it {should have(map.size).items}
it {should include(*map.keys)}
it {should include(map)}
end
end
context "empty object" do
describe "keys" do
subject {thing.keys}
it {should_not be_nil}
it {should be_empty}
end
its(['newProperty']) {should be_nil}
its('newMethod') {should be_nil}
describe "to_hash" do
subject {thing.to_hash}
it {should_not be_nil}
it {should have(0).items}
end
describe "when assigning via properties" do
before(:each) do
thing["foo"] = 5
thing["bar"] = "hello"
end
it_should_behave_like "simple object", 'foo'=>5, 'bar'=>"hello"
end
describe "when assigning via methods" do
before(:each) do
thing.foo = 5
thing.bar = "hello"
end
it_should_behave_like "simple object", 'foo'=>5, 'bar'=>"hello"
end
end
context "with inherited object" do
let(:base) {Thing.new}
before(:each) do
base["foo"] = 5
base["bar"] = "hello"
thing.prototype = base
thing["baz"] = 15
thing["bye"] = "bye"
end
it_should_behave_like "simple object",
{'foo'=>5, 'bar'=>"hello", 'baz'=>15, 'bye'=>"bye"}, ['baz', 'bye']
describe "when overriding values" do
before(:each) do
thing["foo"] = 25
thing["bar"] = "hola"
end
it_should_behave_like "simple object", 'foo'=>25, 'bar'=>"hola", 'baz'=>15, 'bye'=>"bye"
end
end
end
Below is what the output looks like. As you can see, if you read it, it describes the tests that are being run more clearly than the old version of the tests.
$ rspec spec/models/thing_spec.rb
Thing
empty object
keys
should not be nil
should be empty
["newProperty"]
should be nil
newMethod
should be nil
to_hash
should not be nil
should have 0 items
when assigning via properties
it should behave like simple object
keys
should have 2 items
should include "foo" and "bar"
should not include "noSuchProperty"
self_keys
should have 2 items
should include "foo" and "bar"
should not include "noSuchProperty"
fields
["foo"]
should == 5
["bar"]
should == "hello"
["noSuchProperty"]
should be nil
methods
foo
should == 5
bar
should == "hello"
noSuchMethod
should be nil
to_hash
should not be nil
should have 2 items
should include "foo" and "bar"
should include {"foo"=>5, "bar"=>"hello"}
when assigning via methods
it should behave like simple object
keys
should have 2 items
should include "foo" and "bar"
should not include "noSuchProperty"
self_keys
should have 2 items
should include "foo" and "bar"
should not include "noSuchProperty"
fields
["foo"]
should == 5
["bar"]
should == "hello"
["noSuchProperty"]
should be nil
methods
foo
should == 5
bar
should == "hello"
noSuchMethod
should be nil
to_hash
should not be nil
should have 2 items
should include "foo" and "bar"
should include {"foo"=>5, "bar"=>"hello"}
with inherited object
it should behave like simple object
keys
should have 4 items
should include "foo", "bar", "baz", and "bye"
should not include "noSuchProperty"
self_keys
should have 2 items
should include "baz" and "bye"
should not include "noSuchProperty"
fields
["foo"]
should == 5
["bar"]
should == "hello"
["baz"]
should == 15
["bye"]
should == "bye"
["noSuchProperty"]
should be nil
methods
foo
should == 5
bar
should == "hello"
baz
should == 15
bye
should == "bye"
noSuchMethod
should be nil
to_hash
should not be nil
should have 4 items
should include "foo", "bar", "baz", and "bye"
should include {"foo"=>5, "bar"=>"hello", "baz"=>15, "bye"=>"bye"}
when overriding values
it should behave like simple object
keys
should have 4 items
should include "foo", "bar", "baz", and "bye"
should not include "noSuchProperty"
self_keys
should have 4 items
should include "foo", "bar", "baz", and "bye"
should not include "noSuchProperty"
fields
["foo"]
should == 25
["bar"]
should == "hola"
["baz"]
should == 15
["bye"]
should == "bye"
["noSuchProperty"]
should be nil
methods
foo
should == 25
bar
should == "hola"
baz
should == 15
bye
should == "bye"
noSuchMethod
should be nil
to_hash
should not be nil
should have 4 items
should include "foo", "bar", "baz", and "bye"
should include {"foo"=>25, "bar"=>"hola", "baz"=>15, "bye"=>"bye"}
Finished in 0.0831 seconds
78 examples, 0 failures