Rspec Get Started

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

Prerequisites

  • Ruby 1.9.3 or later
  • The latest RSpec gem (2.9.0 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 methoddescribe_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:

 

> rspec user_spec.rb

 

The rspec command gets installed when you install the rspec gem. 

Getting back to the task at hand, running rspec 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, the “LOAD_PATH” line is used to specify where to find the library used in “require”.

$LOAD_PATH << '.'
require 'user'

 

describe User do end

 

Now run the rspec command again.

 

$ rspec user_spec.rb

 

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

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

 

$ rspec user_spec.rb --format documentation

 

User – should be in any roles assigned to it

Finished in 0.00031 seconds

1 example, 0 failures

 

The documentation format outputs the name of each Behaviour (the object created by the describemethod) and each Example (the object created by the it method). This format comes fromTestDox, 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 rspec command.

 

$ rspec user_spec.rb --format documentation

 

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 

 

 

$ rspec user_spec.rb --format documentation

 

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 

 

 

$ rspec user_spec.rb --format documentation

 

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 

 

 

$ rspec user_spec.rb --format documentation

 

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 

 

 

$ rspec user_spec.rb --format documentation

 

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

 

 

$ rspec user_spec.rb --format documentation

 

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

 

 

$ rspec user_spec.rb --format documentation

 

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

 

 

$ rspec user_spec.rb --format documentation

 

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

 

 

$ rspec user_spec.rb --format documentation

 

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

 

 

$ rspec user_spec.rb --format documentation

 

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

 

 

$ rspec user_spec.rb --format documentation

 

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

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: