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:
1 2 |
|
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:
1 2 |
|
… and require it in user_spec.rb:
1 2 3 4 |
|
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.
1 2 3 4 |
|
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.
1 2 3 4 5 |
|
… 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:
1 2 3 4 5 6 |
|
$ 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:
1 2 3 4 |
|
$ 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:
1 2 3 4 5 |
|
$ 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:
1 2 3 4 5 6 |
|
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:
1 2 3 4 5 6 7 |
|
$ 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
.
1 2 3 4 5 6 7 8 |
|
$ 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:
1 2 3 4 5 6 7 8 9 10 |
|
$ 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:
1 2 3 4 5 6 7 8 9 10 11 |
|
$ 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
:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
$ 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:
1 2 3 4 5 6 7 8 |
|
$ 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!
1 2 3 4 5 6 7 8 9 |
|
$ 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 …