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
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
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
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
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
No comments:
Post a Comment