an introduction to RSpec - Part I
May 14th, 2007
Prerequisites
<ul>
<li>Ruby 1.8.4 or later</li>
<li>The latest RSpec gem (0.9.4 as of this writing).</li>
</ul>
<p>To install the RSpec gem, open up a command shell and type …</p>
> gem install rspec
Getting Started
<p>For this example, we’ll describe and develop the beginnings of a User class, which can be assigned any number of roles. Start by creating a directory for the files for this tutorial:</p>
> mkdir rspec_tutorial > cd rspec_tutorial
The first example
<p>RSpec provides a Domain Specific Language for describing the expected behaviour of a system with executable examples. The first methods we’ll encounter are <code>describe</code> and <code>it</code>. These methods used to have other names (which are still supported but generally not recommended), but we use <code>describe</code> and <code>it</code> because they lead you to thinking more about behaviour than structure.</p>
<p>Using your favorite editor, create a file in this directory named user_spec.rb and type the following:</p>
describe User do
end
The describe method creates an instance of Behaviour. So “describe User” is really saying “describe the behaviour of the User class”. I guess we could have named the method describe_the_behaviour_of, but there is a point at which clarity bumps up against verbosity, and we feel that point is before the first underscore in describe_the_behaviour_of.
<p>In the shell, type the following command:</p>
> spec user_spec.rb
The spec command gets installed when you install the rspec gem. It supports a large number of command line options. Most of the options are outside the scope of this tutorial, but you can learn about them by running the command without any arguments:
> spec
Getting back to the task at hand, running spec user_spec.rb should have resulted in output that includes the following error:
./user_spec.rb:1: uninitialized constant User (NameError)
We haven’t even written any examples and already RSpec is telling us what code we need to write. We need to create a User class to resolve this error, so create user.rb with the following:
class User
end
… and require it in user_spec.rb:
require 'user'
describe User do
end
Now run the spec command again.
$ spec user_spec.rbFinished in 6.0e-06 seconds
0 examples, 0 failures
The output shows that we have no examples yet, so lets add one. We’ll start by describing the intent of example without any code.
describe User do
it "should be in any roles assigned to it" do
end
end
The it method returns an instance of Example. This is a metaphor for an example of the behaviour that we are describing.
<p>Read that out loud. It is quite satisfying. We can thank <a href="http://dannorth.net">Dan North</a> for the names <code>describe</code> and <code>it</code>.</p>
<p>Run the <code>spec</code>, but this time add the <code>--format</code> option:</p>
$ spec user_spec.rb --format specdocUser - should be in any roles assigned to it
Finished in 0.022865 seconds
1 example, 0 failures
The specdoc format outputs the name of each Behaviour (the object created by the describe method) and each Example (the object created by the it method). This format comes from TestDox, a tool which produces a similar report from the names of JUnit TestCases and methods within.
<p>Now add a Ruby statement that begins to express the described intent.</p>
describe User do
it "should be in any roles assigned to it" do
user.should be_in_role("assigned role")
end
end
… and run the spec command.
$ spec user_spec.rb --format specdocUser - should be in any roles assigned to it (ERROR - 1)
1) NameError in 'User should be in any roles assigned to it' undefined local variable or method `user' for #<#<Class:0x14ed15c>:0x14ecdd8> ./user_spec.rb:6:
Finished in 0.017956 seconds
1 example, 1 failure
There are a couple of things to note about this output. First, the text “(ERROR – 1)” tells us that there was an error in the “should be in any roles assigned to it” example. The “1” tells us that as we scroll down to the detailed failure report that this particular failure is described under “1)”. This will become more useful as the number of examples increases.
<p>Another thing to note is the absence of any references to RSpec code in the backtrace. RSpec filters that out by default, however you can see the entire backtrace by adding the <code>--backtrace</code> switch to the command.</p>
<p>The output tells us that there is no <code>user</code>, so the next step is to make one:</p>
describe User do
it "should be in any roles assigned to it" do
user = User.new
user.should be_in_role("assigned role")
end
end
$ spec user_spec.rb --format specdocUser - should be in any roles assigned to it (ERROR - 1)
1) NoMethodError in 'User should be in any roles assigned to it' undefined method `in_role?' for #<User:0x14ec8ec> ./user_spec.rb:7:
Finished in 0.020779 seconds
1 example, 1 failure
Now we learn that User does not respond to in_role?, so we add that to User:
class User
def in_role?(role)
end
$ spec user_spec.rb --format specdocUser - should be in any roles assigned to it (FAILED - 1)
1) 'User should be in any roles assigned to it' FAILED expected in_role?("assigned role") to return true, got nil ./user_spec.rb:7:
Finished in 0.0172110000000001 seconds
1 example, 1 failure
We now have a failing example, which is the first goal. We always want to see a meaningful failure before success because that’s the only way we can be sure the success is the result of writing code in the right place in the system.
<p>To get this to pass, we do the simplest thing that could possibly work:</p>
class User
def in_role?(role)
true
end
$ spec user_spec.rb --format specdocUser - should be in any roles assigned to it
Finished in 0.018173 seconds
1 example, 0 failures
That passes, but we’re not done yet. Take a look again at the example:
describe User do
it "should be in any roles assigned to it" do
user = User.new
user.should be_in_role("assigned role")
end
end
Does that express the described intent? Not fully. The description says that the User “should be in any roles assigned to it”, but we haven’t assigned any roles to it. Let’s add that assignment to the example:
describe User do
it "should be in any roles assigned to it" do
user = User.new
user.assign_role("assigned role")
user.should be_in_role("assigned role")
end
end
$ spec user_spec.rb --format specdocUser - should be in any roles assigned to it (ERROR - 1)
1) NoMethodError in 'User should be in any roles assigned to it' undefined method `assign_role' for #<User:0x14ec784> ./user_spec.rb:6:
Finished in 0.018564 seconds
1 example, 1 failure
Following the advice in the output, we now add the assign_role method to User.
class User
def in_role?(role)
true
end
def assign_role(role)
end
end
$ spec user_spec.rb --format specdocUser - should be in any roles assigned to it
Finished in 0.018998 seconds
1 example, 0 failures
The example is passing again, but are we done yet? Scroll up a few lines and take a look at the current implementation of User. I think it’s fair to say that this is NOT the implementation that we know we want to end up with. And this is the point in the process that makes TDD “Test-Driven”. Rather than implementing the code we think that we know that we want, we’re going to proceed under the guidance of the principle that “code does not exist until it is tested.”
<p>Right now, the only requirement of this system that we’ve expressed is that a “User should be in any roles assigned to it”, and the system meets that requirement. In order to push the code to the next step, we need to express more requirements with more executable examples.</p>
<h4>The second example</h4>
<p>As things stand now, a <code>User</code> will answer <code>true</code> when you ask it if it is any role, regardless of whether it has been assigned that role. We want the <code>User</code> to tell is it is not in a role which it has not been assigned, so let’s add that example:</p>
describe User do
it "should be in any roles assigned to it" do
user = User.new
user.assign_role("assigned role")
user.should be_in_role("assigned role")
end
it "should NOT be in any roles not assigned to it" do
end
end
$ spec user_spec.rb --format specdocUser - should be in any roles assigned to it - should NOT be in any roles not assigned to it
Finished in 0.018231 seconds
2 examples, 0 failures
Now add a statement to express intent:
describe User do
it "should be in any roles assigned to it" do
user = User.new
user.assign_role("assigned role")
user.should be_in_role("assigned role")
end
it "should NOT be in any roles not assigned to it" do
user.should_not be_in_role("unassigned role")
end
end
$ spec user_spec.rb --format specdocUser - should be in any roles assigned to it - should NOT be in any roles not assigned to it (ERROR - 1)
1) NameError in 'User should NOT be in any roles not assigned to it' undefined local variable or method `user' for #<#<Class:0x14eca54>:0x14ebce4> ./user_spec.rb:11:
Finished in 0.018465 seconds
2 examples, 1 failure
Now create the User:
describe User do
it "should be in any roles assigned to it" do
user = User.new
user.assign_role("assigned role")
user.should be_in_role("assigned role")
end
it "should NOT be in any roles not assigned to it" do
user = User.new
user.should_not be_in_role("unassigned role")
end
end
$ spec user_spec.rb --format specdocUser - should be in any roles assigned to it - should NOT be in any roles not assigned to it (FAILED - 1)
1) 'User should NOT be in any roles not assigned to it' FAILED expected in_role?("unassigned role") to return false, got true ./user_spec.rb:12:
Finished in 0.019014 seconds
2 examples, 1 failure
Once again we have an example that is failing in the way we want it to fail – the intent is correctly expressed, but the code is not behaving as expected.
<p>Doing the simplest thing that could possibly work, we can get the example to pass like so:</p>
class User
def in_role?(role)
role == "assigned role"
end
def assign_role(role)
end
end
$ spec user_spec.rb --format specdocUser - should be in any roles assigned to it - should NOT be in any roles not assigned to it
Finished in 0.017194 seconds
2 examples, 0 failures
Everything passes, but now we have duplication between the examples and the subject code. Time to refactor to remove the duplication!
class User
def in_role?(role)
role == @role
end
def assign_role(role)
@role = role
end
end
$ spec user_spec.rb --format specdocUser - should be in any roles assigned to it - should NOT be in any roles not assigned to it
Finished in 0.018199 seconds
2 examples, 0 failures
At this point, you probably feel as though the implementation is not quite right yet. That feeling is based on an assumption that we should be able to assign any number of roles to a User. Part of that assumption comes from our initial example: “should be in any roles assigned to it”. Before diving in and changing the implementation, this is a great time to ask the customer whether that assumption is correct! If the answer is that a User can only be in one role at a time, then we’re done with the code but we should probably re-phrase the examples to read “should be in the role assigned to it” and “should NOT be in a role not assigned to it”.
<p>If the answer is that a <code>User</code> can be in more than one role at a time, then we have more work to do. I’ll address this scenario in Part II of this tutorial. Stay tuned …</p>



May 14th, 2007 at 10:25 am
despite this being an intro to rspec, I appreciate your including details on the BDD process and discipline. Thanks.
May 14th, 2007 at 10:25 am
Hey David, thanks for this tutorial. It’s great to see RSpec in action with its shiny new method names. Like caton, I also appreciate you taking the time to emphasise the TDD/BDD iteration process (failing test, passing test, refactor).
May 14th, 2007 at 10:25 am
I’m have been wondering about using RSpec for a while. One thing that really gets me down when it comes to traditional testing in Rails is that there are often times when you want to make a simple change to the app and tests break all over the place and you have to spend a load of time fixing those test. This could be that my tests aren’t as modular as they could be, does RSpec help with this kind of problem from your experience?
May 14th, 2007 at 10:25 am
Jamie – of course I’d love to see you check out RSpec, but it’s not your only option. It sounds like your problem is related to a lack of component isolation. RSpec’s built in mock framework can help you with this, but so can mocha or flexmock plugged into test/unit.
David
May 14th, 2007 at 10:25 am
When I follow the first example I get a different error, it says “undefined method `should_be_in_role’” not undefined method `in_role?’, which makes sense because we are calling should_in_role in the test not in_role?
“Another thing to note is the absence of any references to RSpec code in the backtrace. RSpec filters that out by default, however you can see the entire backtrace by adding the—backtrace switch to the command.”
May 14th, 2007 at 10:25 am
I sometimes don’t have the full discipline to work like this, and there are a couple of reason why I find it hard to accept. 1. Switching contexts hurts productivity. Why write a line in one file, then a line in another … when I can write 10 lines in user.rb and then 20 lines in user_spec.rb – being faster at doing it. 2. Easy refactoring becomes hard – with code like you showed in this example, when I want to change the user methods then not only do I need to think about what needs to be changed in the user class, but also think even more and harder about the spec. Thus refactoring code with BDD becomes harder. Perhaps you can argue that its easier because you can more easily see that you don’t break nything. But when various things depend on each other, its inevitable to break more than just 1-2 things at once before stuff starts working again.
May 14th, 2007 at 10:25 am
Thank you thank you David for somehow getting Rspec onto the RailsConf 2007 talks. Can’t wait.
May 14th, 2007 at 10:25 am
Re: “undefined method `should_be_in_role’� not undefined method
May 14th, 2007 at 10:25 am
I was at the rspec BoF, and I was wanting to ask you a question but I didn’t really have the time as I was busy listening to everything else.
Daniel
May 14th, 2007 at 10:25 am
Daniel – check out http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model. In that article, Jamis Buck talks about moving a lot of what you’re question implies should be in the controller into the model. He doesn’t get into the testing aspects of it, but separating out things in the way he suggests does tend to make things more testable.
cd stuff ruby script/plugin install svn://rubyforge.org/var/svn/rspec/tags/CURRENT/rspec ruby script/plugin install svn://rubyforge.org/var/svn/rspec/tags/CURRENT/rspec_on_rails script/generate rspec script/generate rspec_scaffold Thing name:string
May 14th, 2007 at 10:25 am
That should have been:
May 14th, 2007 at 10:25 am
Hey Dave,
It is also possible to pass a string which can be used to describe the context in which the enclosed examples (?) are run;
Can you comment please on the intent behind using the class (in this case User) rather than the string as I used above. And is there a move away from using the string version of #describe ?
May 14th, 2007 at 10:25 am
Hi Sinclair – #describe will accept one or two arguments. The first can be a type or a String, the second should be a String.
module Auth do describe User do it "should ...." doThe output from that would be:
You can also use an additional string, so something like this:
module Casino do describe PokerTable, ", when empty" do it "should welcome a new player" dooutputting:
So we’ve built in some flexibility. In RSpec’s own examples we use all of these forms. Some seem more right in some places while others seem more right in others.
David
May 14th, 2007 at 10:25 am
David, I just got my hands on rspec and its super dooper cool. Can u suggest me a good way to spec has_many :through association. This has been eating my head around. And thanks for giving us this wonderful tool.
May 14th, 2007 at 10:25 am
Sri – check out the rspec google group. You’ll find a lot of help there.
May 14th, 2007 at 10:25 am
Really Good… Thanks.