Here’s an introductory tutorial for those of you interested in getting started with RSpec.

Background

Behaviour Driven Development is an Agile development process that comprises aspects of Acceptance Test Driven Planning, Domain Driven Design and Test Driven Development. RSpec is a BDD tool aimed at TDD in the context of BDD.

You could say that RSpec is what is traditionally known as a unit testing framework, but we prefer to describe it as “a Domain Specific Language for describing the expected behaviour of a system with executable examples.”

Also, the process that this tutorial guides you through is Test Driven Development at its core, but we use words like “behaviour” and “example” instead of “test case” and “test method”.

To understand why we choose this nomenclature, and more detailed history of BDD and RSpec’s evolution, check out the writings of “Dan North”:http://dannorth.net/tags/agile/bdd/, “Dave Astels”:http://daveastels.com/articles/2005/07/05/a-new-look-at-test-driven-development, and “Brian Marick”:http://exampler.com/.

Throughout the tutorial, I’ll address some of the philosophy behind various choices that are made, but you’ll have a much better understanding of them (or at least a context to put them in) if you peruse these recommended readings.

Prerequisites

  • Ruby 1.8.4 or later
  • The latest RSpec gem (0.9.4 as of this writing).

To install the RSpec gem, open up a command shell and type …

> gem install rspec

Getting Started

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:

> mkdir rspec_tutorial
> cd rspec_tutorial

The first example

RSpec provides a Domain Specific Language for describing the expected behaviour of a system with executable examples. The first methods we’ll encounter are describe and it. These methods used to have other names (which are still supported but generally not recommended), but we use describe and it because they lead you to thinking more about behaviour than structure.

Using your favorite editor, create a file in this directory named user_spec.rb and type the following:

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.

In the shell, type the following command:

> 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.rb

Finished 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.

Read that out loud. It is quite satisfying. We can thank Dan North for the names describe and it.

Run the spec, but this time add the --format option:

$ spec user_spec.rb --format specdoc

User - 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.

Now add a Ruby statement that begins to express the described intent.

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 specdoc

User - 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.

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.

The output tells us that there is no user, so the next step is to make one:

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 specdoc

User - 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 specdoc

User - 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.

To get this to pass, we do the simplest thing that could possibly work:

class User
  def in_role?(role)
    true
  end

$ spec user_spec.rb --format specdoc

User - 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 specdoc

User - 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 specdoc

User - 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.”

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.

The second example

As things stand now, a User will answer true when you ask it if it is any role, regardless of whether it has been assigned that role. We want the User to tell is it is not in a role which it has not been assigned, so let’s add that 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

it "should NOT be in any roles not assigned to it" do end end

$ spec user_spec.rb --format specdoc

User - 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 specdoc

User - 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 specdoc

User - 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.

Doing the simplest thing that could possibly work, we can get the example to pass like so:

class User
  def in_role?(role)
    role == "assigned role"
  end

def assign_role(role) end end

$ spec user_spec.rb --format specdoc

User - 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 specdoc

User - 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”.

If the answer is that a User 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 …

16 Responses to “an introduction to RSpec - Part I”

  1. caton Says:

    despite this being an intro to rspec, I appreciate your including details on the BDD process and discipline. Thanks.

  2. Sam Aaron Says:

    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).

    <p>However, it would be nice if you could spend just a little bit of time explaining the (seemingly) tricksy meta-programming business going on with the should, should_not, and particularly be_some_thing methods used in the example. Well, I don&#8217;t mean that you explain the implementation (although that would be very interesting) just the semantics of how they are used.</p>
    
    
    <p>Looking forward to future articles&#8230;</p>
    
  3. Jamie Says:

    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?

  4. David Chelimsky Says:

    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.

    <p>Cheers,
    

    David

  5. Kris Says:

    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?

    <p>It could be me but&#8230;</p>
    
    
    <p>I talking about the step just after this bit of text:
    

    “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.”

  6. Evgeny Says:

    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.

    <p>Just my 2cc on the problems I&#8217;ve been having using rSpec, if the next couple of tutorials will cover this problem&#8212;I would really appreciate it.</p>
    
  7. lee Says:

    Thank you thank you David for somehow getting Rspec onto the RailsConf 2007 talks. Can’t wait.

  8. Jim Says:

    Re: “undefined method `should_be_in_role’� not undefined method

    <p>I just experienced the same thing (trying things out here at railsconf).  There is no &#8221;_&#8221; between &#8220;should&#8221; and &#8220;be&#8221;.</p>
    
    
    <p>Cool stuff!</p>
    
  9. Daniel Fischer Says:

    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.

    <p>How would the rspec idealogy handle a test that involves searching a controller, like a query, and validate / return a list of objects as a result of that query.</p>
    
    
    <p>I get rather confused when I think of it.</p>
    
    
    <p>I&#8217;d appreciate any help for this, maybe a post would be great!</p>
    
    
    <p>Thanks,
    

    Daniel

  10. David Chelimsky Says:

    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.

    <p>Accepting that idea as good advice, which I think it is, start a new rails project w/ the rspec_on_rails plugin and generate an rspec_scaffold:</p>
    
    
    <p>rails stuff
    

    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

    <p>And then look at the generated examples in spec/controllers/thing_controller_spec.rb. That should give you some ideas.</p>
    
  11. David Chelimsky Says:

    That should have been:

    rails stuff
    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
    

  12. Sinclair Says:

    Hey Dave,

    <p>In the examples I noticed that the #describe is passed a &#8216;type&#8217; which is the User class.</p>
    

    It is also possible to pass a string which can be used to describe the context in which the enclosed examples (?) are run;

    
        describe 'when the user has no role(s) assigned' do
          it 'should behave like this' do
          end
          it 'should NOT behave like that' do
          end
        end

    describe 'when the user has an assigned role' do
      it 'should behave like that' do
      end
      it 'should NOT behave like this' do
      end
     end
    

    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 ?

    <p><em>Cheers!</em> and thanks to you guys for a great tool</p>
    
    
    <p>sinclair</p>
    
  13. David Chelimsky Says:

    Hi Sinclair – #describe will accept one or two arguments. The first can be a type or a String, the second should be a String.

    <p>You are welcome to use just one String. This will continue to be supported.</p>
    
    
    <p>The benefit of using a type comes when you wrap the descriptions in a module:</p>
    

    module Auth do
      describe User do
        it "should ...." do
    

    The output from that would be:

    Auth::User
    - should ....
    

    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" do
    

    outputting:

    Casino::PokerTable, when empty
    - should welcome a new player
    

    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.

    <p>Hope that helps.</p>
    
    
    <p>Cheers,
    

    David

  14. Sri Says:

    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.

    <p>Cheers, Sri.</p>
    
  15. David Chelimsky Says:

    Sri – check out the rspec google group. You’ll find a lot of help there.

  16. Vijay Says:

    Really Good… Thanks. :)