At the Forge - RSpec for Controllers

RSpec is a popular testing framework for Ruby programmers that works on
the principle of behavior-driven development (BDD). BDD
distinguishes itself from test-driven development (TDD) in that it
looks at programs from the outside, rather than from the inside,
considering code as a user or observer, as opposed to an implementer.
In the BDD world, you don't implement tests, but rather
specifications; if the specification passes, the code is doing
what it is supposed to do.

As with many things in the Ruby arena, RSpec has become particularly
popular among users of the Rails framework for Web development. Last
month, I discussed RSpec in the context of testing Rails
models (that is, classes that connect to the relational database). This
month, I look at the slightly more complicated case of
controller testing. Controller testing is more complicated
because it requires that you consider a few more cases, or at least
different cases. Now you have to consider inputs from the outside
world, in the form of HTTP requests. It also introduces the need for
mocks and stubs, objects you can use to test your controllers
without having to create real objects (and the database that sits
behind them).

This month, I examine some of the ways the RSpec testing
framework allows you to test controllers in your Ruby on Rails
applications. Along the way, I consider what it means to test
controllers and how much you might want to test them. Finally, I
take a quick look at the world of mocks and stubs, and show how they
can help improve your testing.

A Simple Application

Last month, I started building a simple appointment calendar as an example. As it
happens, I implemented only a small part of that appointment
calendar, creating a single person model, which you can use to
represent the people with whom you will meet. Now, let's
create appointments as well:

As you might expect, you will enhance your model files by linking them
together, indicating that each person has_many appointments, but that
each appointment belongs_to one person. That'll allow you to use
Ruby's object-oriented syntax to retrieve person.appointments, or
appointment.person.

Now that you have two models in place, you should do something with
them. One obvious thing to do is list today's
appointments. In BDD fashion, let's write a spec that describes
what the system should do; you actually will implement the code afterward.

The spec will describe how you want to be able to see a list of
appointments. Let's assume that the specs for the models (people
and appointments) are in place, and that you now can concentrate on your
controllers. Basically, you want an appointment controller whose
index action shows all current appointments. You can do that by
generating such a controller:

./script/generate rspec_controller appointment index new create show

Create a controller named appointment, along with a few actions
named similarly to a purely RESTful controller (which this is not).
Now, open up spec/controllers/appointment_controller_spec.rb,
which is the location of the spec file for this controller, and you
will see a number of simple specs, one for each of the methods you've
defined. As I explained last month, RSpec's power is its readability,
with “describe” blocks that indicate an overall context,
“it”
blocks that describe specifications, and then individual assertions,
which are written as “something SHOULD be-something”. The initial,
automatically generated spec for the index action, thus, looks like
this:

describe "GET 'index'" do
it "should be successful" do
get 'index'
response.should be_success
end
end

Mocking

The response object is given automatically in controller specs,
and it allows you to do such things as check for success. The thing is,
you also want this index action to retrieve (and display) all the
current appointments in your database. How can you test for that?

One way is to load your database with a bunch of fake
data, or “fixtures”, and actually retrieve the data from the
database. But hey, you're trying to test the controller here, not the
database—so going to the database is going to be massive overkill.

What you can do instead is tell Ruby you expect the controller to
request a bunch of appointment objects. Indeed, it should request all
the appointments in the database, as per your specification. So
long as it does that, you can rest assured that the action's
specification has been met.

You can do this by switching your normal Appointment object with a mock,
sometimes called a test double object. This mock object allows you
to check that the right things are happening, while staying within your
program. For example, if you want to make sure that
Appointment.find(:all) is being invoked, modify your spec to read as
follows:

describe "GET 'index'" do
it "should be successful" do
appointments = [mock(:appointment), mock(:appointment)]
Appointment.should_receive(:find).and_return(appointments)
get 'index'
response.should be_success
end
end

Here, two lines are added before the invocation of “get
'index'”. In
the first line, you create an array of two mock objects, each of which
will claim (if asked) that it is an instance of Appointment. It isn't
a real appointment object, of course, but rather a thin layer meant
just for testing. You will create two such objects, so you can
pretend that there are multiple appointments in your database.

The next line is even more interesting. It says that Appointment (the
class) should expect to receive the find method at some point.
Notice that the placement here is important; if you were to put this
mocking line after the invocation of GET, it would be too late.
Instead, set up the mock such that the GET method can do things
appropriately. If the mock doesn't receive an invocation of
“index”,
RSpec exits with a fatal error. Indeed, using BDD methods,
that's exactly what I can expect to see after I run RSpec:

In other words, the example above says you want Appointment to have its
“find”
method called, but that never happened. Thus, add that
invocation of find to the index action:

def index
Appointment.find(:all)
end

Now the spec passes (thanks to the mock object), and you have
functionality. What could be better? Well, perhaps you want to
test the output you see in the view that displays that object.
I'm not going to go into it here, but RSpec allows you to test views
as well, using a similar mechanism that looks at the resulting HTML
output.

Indeed, I have begun to scratch only the surface of what is possible
with RSpec's mocking mechanism. You can stub out specific object
methods, allowing you to use models without their overhead or
dependencies. For example, you could replace calls to “find” with a
mock object that you return, and ignore any calls to
“save”—thus,
allowing you to work with real models, but faster and more reliably.

You also can imagine how you could test your ability to retrieve models
that are associated with one another using mocks. For example, the
“index” method probably would be useless if it displayed
only appointments. You probably would want to show the person with whom the
appointment was scheduled. That requires traversing a foreign key
association, which you easily can take care of with stub objects that
you then reference from within your mock.

Now, you might be wondering if all this would be possible with
either fixtures or factories. The answer is yes, and different
developers have used fixtures and factories successfully over the
years. I generally find fixtures to be the most natural of the bunch
to understand and to use, but the fact that they go through the
database and require that I set up and coordinate each of the
individual objects begins to take its toll as a project gets larger.
I also enjoy using factories and have been experimenting (as I mentioned
a few months back) with different factory classes.

But, the more I'm exposed to mocking, the more I wonder if the
entire factory class is necessary, or if I simply can use mocks and
stubs to pinpoint and use the functionality that interests me. I'm
sure other developers are thinking about these considerations as
well, and I hope the plethora of options available to Ruby
developers will improve and encourage the culture of testing that is
already so strong in the Ruby community.