I do not think there is any thrill that can go through the human heart like that felt by the inventor as he sees some creation of the brain unfolding to success. | ||
--Nikola Tesla |
RSpec is a Ruby domain-specific language for specifying the desired behavior of a Ruby application. Its strongest appeal is that RSpec scripts tend to be very readable, letting the authors express their intention with greater readability and fluidity than is achievable using Test::Unit
’s methods and assertions.
RSpec on Rails, a drop-in replacement for the Rails testing subsystem, was released by the RSpec team in late 2006. It supplies verification, mocking, and stubbing features customized for use with Rails models, controllers, and views. I’m happy to say that I’ve been using the RSpec plugin for my Rails projects every day since it was released, and have never needed to touch Test::Unit
for anything significant again.[1] RSpec is simply that good.
Since RSpec scripts (or specs) are so readable, I can’t really think of a better way of introducing you to the framework than to dive into some actual specs. Along the way, I’ll highlight the key concepts that you need to know.
Listing 18.1 is part of a real-world RSpec script defining the behavior of a CreditCard
model class.
Example 18.1. A Description Block from Monkeycharger’s CreditCard
Spec[2]
1 describe "A valid credit card" do 2 before(:each) do 3 @credit_card = generate_credit_card 4 end 5 6 it "should be valid after saving" do 7 @credit_card.save 8 @credit_card.should be_valid 9 end 10 end
RSpec scripts are collections of behaviors, which in turn have collections of examples. Line 1 uses the describe
method to create a Behavior
object under the covers. The behavior sets the context for a set of specification examples, and you should pass it a sentence fragment that accurately describes the context you’re about to specify.
The before
method on line 2 (and its companion after
) are akin to the setup
and teardown
methods of xUnit frameworks like Test::Unit
. They are used to set up the state as it should be prior to running an example, and if necessary, to clean up the state after the example has run. This particular behavior did not require an after
block. (For brevity, the source of the generate_credit_card
method on line 3 is not included in this listing. It is simply a factory method that returns instances of CreditCard
preset with known and overridable attribute values. We’ll learn more about using helper methods to write more readable spec code later in the chapter.)
The it
method on line 6 is used to create an Example
object, and you also give it a description. The idea is to complete the thought that was started in the describe
method, so that it forms a complete sentence. Our example reads “A valid credit card should be valid after saving.” See it?
Moving on, line 7 of Listing 18.1 invokes save
on the credit card object, and since it’s an ActiveRecord
model, we know that will get it validated. So all we have left is to verify that the credit card instance is valid. Rather than xUnit-style assertions, RSpec introduces some funky DSL-ish syntax to do verification, based on a pair of methods called should
and should_not
.
RSpec mixes should
and should_not
into the base Ruby Object
class at runtime so that they are available on all objects. They expect to receive Matcher
objects, which you generate using RSpec expectation syntax.
@credit_card.should be_valid
There are several ways to generate expectation matchers and pass them to should
(and should_not
):
receiver.should(matcher) # the simplest example # Passes if matcher.matches?(receiver) receiver.should == expected #any value # Passes if (receiver == expected) receiver.should === expected #any value # Passes if (receiver === expected) receiver.should =~ regexp # Passes if (receiver =~ regexp)
The process of learning to write expectations is probably one of the meatier parts of the RSpec learning curve. One of the most common idioms is “should equal,” akin to Test::Unit
’s assert_equal
assertion. This is how you would rewrite the “credit card should be valid” assertion using should equal syntax:
@credit_card.valid?.should == true
The valid?
method returns true
or false
and according to our spec, it should be true
. Now, why didn’t we write the expectation that way to begin with? Simply because that question-mark-and-period combo is ugly. It works, but it’s not as elegant or readable as saying: should be_valid
. There is no predefined be_valid
method in RSpec—it is an arbitrary predicate.
Thanks to the magic of method_missing
, RSpec can support arbitrary predicates, that is, it understands that if you invoke something that begins with be_
, then it should use the rest of the method name as a pointer to a Boolean attribute of the target object.
The simplest hard-coded predicate-style matchers are used to assert Boolean and nil target values:
target.should be_true target.should be_false target.should be_nil target.should_not be_nil
The arbitrary predicate matchers can assert against any Boolean target, and even support parameters!
collection.should be_empty #passes if target.empty? target.should_not be_empty # passes unless target.empty? target.should_not be_under_age(13) # passes unless target.under_age?(13)
As an alternative to prefixing arbitrary predicate matchers with be_
, you may choose from the indefinite article versions be_a_
and be_an_
, making your specs read much more naturally:
"a string".should be_an_instance_of(String) 3.should be_a_kind_of(Fixnum) 3.should be_a_kind_of(Numeric) 3.should be_an_instance_of(Fixnum) 3.should_not be_instance_of(Numeric) #fails
The cleverness (madness?) doesn’t stop there. RSpec will even understand have_
prefixes as referring to predicates like has_key?
:
{:foo => "foo"}.should have_key(:foo) {:bar => "bar"}.should_not have_key(:foo)
RSpec has a number of expectation matchers for working with classes that implement module Enumerable
. You can specify whether an array should include a particular element, or if a string contains a substring.
[1, 2, 3].should include(1) [1, 2, 3].should_not include(4) "foobar".should include("bar") "foobar".should_not include("baz")
You get a slick bit of syntactic sugar for testing the length of collections:
[1, 2, 3].should have(3).items
What if you want to specify the length of a has_many
collection? “Schedule.days.should have(3).items” is admittedly quite ugly. RSpec gives us some more sweetness here as well.
schedule.should have(3).days # passes if schedule.days.length == 3
When you find that none of the stock expectation matchers provide a natural-feeling expectation, you can very easily write your own. All you need to do is write a Ruby class that implements the following four methods (only two are required):
matches?(actual) failure_message negative_failure_message #optional description #optional
The example given in the RSpec API documentation is a game in which players can be in various zones on a virtual board. To specify that a player bob
should be in zone 4, you could write a spec like this:
bob.current_zone.should eql(Zone.new("4"))
However, it’s more expressive to say one of the following, using the custom matcher in Listing 18.2:
bob.should be_in_zone("4") and bob.should_not be_in_zone("3")
Example 18.2. BeInZone
Custom Expectation Matcher Class
class BeInZone def initialize(expected) @expected = expected end def matches?(target) @target = target @target.current_zone.eql?(Zone.new(@expected)) end def failure_message "expected #{@target.inspect} to be in Zone #{@expected}" end def negative_failure_message "expected #{@target.inspect} not to be in Zone #{@expected}" end end
In addition to the matcher class you would also need to write the following method so that it’d be in scope for your spec.
def be_in_zone(expected) BeInZone.new(expected) end
This is normally done by including the method and the class in a module, which is then included in your spec.
describe "Player behaviour" do include CustomGameMatchers ... end
Or you can include helpers globally in a spec_helper.rb
file required from your spec file(s):
Spec::Runner.configure do |config| config.include(CustomGameMatchers) end
Note that you don’t have to worry about Behavior
and Example
object instances while you’re writing RSpec scripts. (They are only used internally by the framework.)
The Monkeycharger example presented in Listing 18.1 is pretty simple and only has one example, but only because I wanted a simple introduction to the most basic concepts of RSpec.
A behavior usually has more than one example in it. It’s easiest to just show you some more code from Monkeycharger. Listing 18.3 is the next describe
block of that CreditCard
spec and has five examples.
Example 18.3. Another Description Block from Monkeycharger’s CreditCard
Spec
describe CreditCard do it "should have a valid month" do card = generate_credit_card(:month => 'f') card.errors.on(:month).should == "is not a valid month" end it "should have a valid year" do card = generate_credit_card(:year => 'asdf') card.errors.on(:year).should == "is not a valid year" end it "date should not be in the past" do past_month = (Date.today << 2) card = generate_credit_card(:year => past_month.year, :month => past_month.month) card.should_not be_valid end it "should have two words in the name" do card = generate_credit_card(:name => "Sam") card.errors.on(:name).should == "must be two words long." end it "should have two word last_name if name is three words long" do card = generate_credit_card(:name => "Sam Van Dyk") card.last_name.should == "Van Dyk" end it "should have one word first_name if name is three words long" do card = generate_credit_card(:name => "Sam Van Dyk") card.first_name.should == "Sam" end end
Even if you don’t know much about credit cards (or RSpec for that matter), you should still be able to read through this spec without too much of a problem.
Plain old RSpec scripts usually need a string passed to the describe
method. The spec in Listing 18.3 happens to be an ActiveRecord
model spec based on the RSpec on Rails plugin, so it’s okay to pass it an ActiveRecord
model class instead of a description string. (More on that later when we get into Rails specifics. Right now we’re still just getting through the basics of RSpec.)
Often you’ll want to specify multiple behaviors that share a lot of the same behavior. It would be silly to type out the same code over and over. Most programmers will extract the common code into individual methods. However, the problem is that an RSpec behavior contains many pieces:
before(:all)
before(:each)
after(:each)
after(:all)
all of the expectations
any included modules
Even with good refactoring, you’ll end up with lots of duplication. Fortunately RSpec lets us take advantage of shared behaviors. Shared behaviors aren’t run individually, but rather are included into other behaviors. We do this by passing the :shared => true
option to describe.
Let’s say we want to specify two classes, Teacher
and Student
. In addition to their unique behavior, they have some common behavior that we’re interested in. Instead of specifying the same behavior twice, we can create a shared behavior and include it in each class’s specification.
describe "people in general", :shared => true do it "should have a name" do @person.name.should_not be_nil end it "should have an age" do @person.age.should_not be_nil end end
Where does the @person
instance variable come from? We never assigned it anywhere. It turns out this spec won’t run because there’s nothing to run yet. Shared behaviors are just used to factor out common behavior specifications. We need to write another spec that uses the shared behavior.
describe Teacher do before(:each) do @person = Teacher.new("Ms. Smith", 30, 50000) end it_should_behave_like "people in general" it "should have a salary" do @person.salary.should == 50000 end end
The it_should_behave_like
takes a string argument. RSpec then finds the shared behavior with that name and includes it into the Teacher
specification.
We can do the same thing with Student
.
describe Student do before(:each) do @person = Student.new("Susie", 8, "pink") end it_should_behave_like "people in general" it "should have a favorite color" do @person.favorite_color.should == "pink" end end
Passing —format specdoc
(or -f s
in abbreviated form) to the spec
command shows that the shared behavior is indeed included in the individual class specifications.
Teacher - should have a name - should have an age - should have a salary Student - should have a name - should have an age - should have a favorite color
It’s important to note that, as of this writing, RSpec runs the before
and after
methods in the order they’re defined in the spec. You can see this by adding an output statement to each one.
describe "people in general" before(:each) do puts "shared before()" end after(:each) do puts "shared after()" end ... end describe Teacher do before(:each) do puts "teacher before()" @person = Teacher.new("Ms. Smith", 30, 50000) end after(:each) do puts "teacher after()" end it_should_behave_like "people in general" ... end
This will give us the following output:
teacher before() shared before() teacher after() shared after() .
Move the it_should_behave_like
statement to the beginning of the spec and notice how the shared behavior’s before
method runs first.
In Chapter 17, “Testing,” we introduced the concepts of mocks and stubs in association with the Mocha library. RSpec relies heavily on mocking and stubbing.[3] It’s possible to use Mocha together with RSpec, but in our examples we’ll use RSpec’s own mocking and stubbing facilities, which are equally powerful. Actually, they are almost the same—mostly the method names change a little bit.
To create a mock object, you simply call the mock
method anywhere in a spec, and give it a name as an optional parameter. It’s a good idea to give mock objects a name if you will be using more than one of them in your spec. If you use multiple anonymous mocks, you’ll probably have a hard time telling them apart if one fails.
echo = mock('echo')
Remember that you set expectations about what messages are sent to your mock during the course of your spec—mocks will cause a spec to fail if their expectations are not met. Where you would say expects
in Mocha to set an expectation on that mock that a message will be passed, in RSpec we say should_receive
or should_not_receive
.
echo.should_receive(:sound)
Both frameworks have a chained method called with
used to set expected parameters, and where in Mocha we would say returns
to set the return value, in RSpec we say and_return
. It’s close enough that you should be able to switch between frameworks pretty easily, if you need to do so.
echo.should_receive(:sound).with("hey").and_return("hey")
Occasionally you just want an object for testing purposes that accepts any message passed to it—a pattern known as null object. It’s possible to make one using the mock
method and the :null_object
option.
null_object = mock('null', :null_object => true)
You can easily create a stub object in RSpec via the stub
factory method. You pass stub
a name (just like a mock) and default attributes as a hash.
yodeler = stub('yodeler', :yodels? => true)
By the way, there’s no rule that the name parameter of a mock or stub needs to be a string. It’s pretty typical to pass mock
or stub
a class reference corresponding to the real type of object.
yodeler = stub(Yodeler, :yodels? => true)
The stub
factory method is actually just a convenience—what you get back is a Mock
object, with predefined method stubs, as you can see from its implementation shown in Listing 18.4:
See the
stub!
method in Listing 18.4? You can use it to install or replace a method on any object, not just mocks—a technique called partial mocking and stubbing.A partial is RSpec’s way of describing an instance of an existing class that has some mocked or stubbed behavior set on it. Even though RSpec’s authors warn us about the practice in their docs, the ability to do partial mocking and stubbing is actually really crucial to RSpec working well with Rails, particularly when it comes to interactions involving
ActiveRecord
’screate
andfind
methods.
To see RSpec mocking and stubbing in action, let’s go back and take a look at another Monkeycharger model spec, this time for the Authorizer
class. It talks to a payment gateway and specifies how credit card transactions are handled.
You might recall that back in Chapter 17, in the “Rails Mocks?” section, we touched on how external services need to be mocked, so that we don’t end up sending test data to a real service. Listing 18.5 shows you this technique in action, using RSpec mocks and stubs.
Example 18.5. Monkeycharger’s Authorizer
Model Spec
describe Authorizer, "processing a non-saved card" do before(:each) do @card = CreditCard.new(:name => 'Joe Van Dyk', :number => '4111111111111111', :year => 2009, :month => 9, :cvv => '123') end it "should send authorization request to the gateway" do $gateway.should_receive(:authorize) .with(599, @card).and_return(successful_authorization) Authorizer::authorize!(:credit_card => @card, :amount => '5.99') end it "should return the transaction id it receives from the gateway" do $gateway.should_receive(:authorize) .with(599, @card).and_return(successful_authorization) Authorizer::authorize!(:credit_card => @card, :amount => '5.99') .should == successful_authorization.authorization end it "authorize! should raise AuthorizationError on failed authorize" do $gateway.should_receive(:authorize) .with(599, @card).and_return(unsuccessful_authorization) lambda { Authorizer::authorize!(:credit_card => @card, :amount => '5.99') }.should raise_error(AuthorizationError, unsuccessful_authorization.message) end private def successful_authorization stub(Object, :success? => true, :authorization => '1234') end def unsuccessful_authorization stub(Object, :success? => false, :message => 'reason why it failed') end end
Specs are executable documents. Each example block is executed inside its own object instance, to make sure that the integrity of each is preserved (with regard to instance variables, etc.).
If I run the credit card specs from Listings 18.1 and 18.2 using the spec
command that should have been installed on my system by RSpec, I’ll get output similar to that of Test::Unit
—familiar, comfortable, and passing... just not too informative.
$ spec spec/models/credit_card_spec.rb ......... Finished in 0.330223 seconds 9 examples, 0 failures
In case it wasn’t obvious by now, when we’re using RSpec, we don’t have to use
Test::Unit
any more. They serve similar, mutually exclusive functions: to specify and verify the operation of our application. Both can be used to drive the design of an application in an evolutionary manner according to the precepts of test-driven development (TDD).There’s a project called test/spec that implements Behavior-Driven Development (BDD) principles on top of
Test::Unit
, but at the time that I’m writing this, it’s far behind RSpec and doesn’t seem to have much momentum.
RSpec is capable of outputting results of a spec run in many formats. The traditional dots output that looks just like Test::Unit
is called progress and, as we saw a moment ago, is the default.
If we add the -fs
command-line parameter to spec
, we can cause it to output the results of its run in a very different and much more interesting format, the specdoc format. It surpasses anything that Test::Unit
is capable of doing on its own “out of the box.”
$ spec -fs spec/models/credit_card_spec.rb A valid credit card - should be valid CreditCard - should have a valid month - should have a valid year - date should not be in the past - should have two words in the name - should have two words in the last name if the name is three words long - should have one word in the first name if the name is three words long We only take Visa and MasterCard - should not accept amex - should not accept discover Finished in 0.301157 seconds 9 examples, 0 failures
Nice, huh? If this is the first time you’re seeing this kind of output, I wouldn’t be surprised if you drifted off in speculation about whether RSpec could help you deal with sadistic PHB-imposed documentation requirements.
We can also do Ruby RDoc-style output:
$ spec -fr spec/models/authorization_spec.rb # Authorizer a non-saved card # * the gateway should receive the authorization # * authorize! should return the transaction id # * authorize! should throw an exception on a unsuccessful authorization Finished in 0.268268 seconds 3 examples, 0 failures
And perhaps the most beautiful output of all, color-coded HTML output, which is what TextMate pops up in a window whenever I run a spec in the editor.
Figure 18.1 shows a successful spec run. If we had failing examples, some of those bars would have been red. Having these sorts of self-documenting abilities is one of the biggest wins you get in choosing RSpec. It actually compels most people to work toward better coverage of their project. I also know from experience that development managers tend to really appreciate RSpec’s output, even incorporating it into their project deliverables.
Besides the different formatting, there are all sorts of other command-line options available. Just type spec --help
to see them all.
To get started with RSpec on Rails, you need to install the main RSpec library gem. Then install the RSpec on Rails plugin[4] into your project:
sudo gem install rspec script/plugin install svn://rubyforge.org/var/svn/rspec/tags/CURRENT/rspec_on_rails
The project leads actually advise you to install RSpec as a Rails plugin also, so that you can have different versions on a per-project basis.
That does it for our introduction to RSpec. Now we’ll take a look at using RSpec with Ruby on Rails.
The RSpec on Rails plugin provides four different contexts for specs, corresponding to the four major kinds of objects you write in Rails. Along with the API support you need to write Rails specs, it also provides code generators and a bundle of Rake tasks.
Assuming you have the plugin installed already, you should run the rspec
generator provided to set up your project for use with RSpec.
$ script/generate rspec create spec create spec/controllers create spec/fixtures create spec/helpers create spec/models create spec/views create spec/spec_helper.rb create spec/spec.opts create previous_failures.txt create script/spec_server create script/spec
A spec
directory is created, containing subdirectories for each of the four types of specs. A bunch of additional support files are also created, which we’ll look at in detail later on.
Model specs help you design and verify the domain model of your Rails application, both ActiveRecord
and your own classes. RSpec on Rails doesn’t provide too much special functionality for model specs, because there’s not really much needed beyond what’s provided by the base library.
An rspec_model
generator is provided, which can be used in place of the default model
generator that’s included in Rails. It functions almost the same as its default counterpart, except that it creates a stubbed-out spec in the models
directory instead of a stubbed-out test in the test
directory. Pass it a class name (capitalized) and pairs of attribute_name
:type
values. The datetime
columns (updated_at
/created_at
) are automatically added to the migration; no need to specify them.
$ script/generate rspec_model Schedule name:string exists app/models/ exists spec/models/ exists spec/fixtures/ create app/models/schedule.rb create spec/fixtures/schedules.yml create spec/models/schedule_spec.rb exists db/migrate create db/migrate/001_create_schedules.rb
The generated Schedule
class is empty and not very interesting. The skeleton spec/models/schedule.rb
looks like this:
require File.dirname(__FILE__) + '/../spec_helper' describe Schedule do before(:each) do @schedule = Schedule.new end it "should be valid" do @schedule.should be_valid end end
Assume for a moment that the Schedule
class has a collection of day objects.
class Schedule < ActiveRecord::Base has_many :days end
Let’s specify that we should be able to get a roll-up total of hours from schedule objects. Instead of fixtures, we’ll mock out the days
dependency.
require File.dirname(__FILE__) + '/../spec_helper' describe Schedule do before(:each) do @schedule = Schedule.new end it "should calculate total hours" do days_proxy = mock('days') days_proxy.should_receive(:sum).with(:hours).and_return(40) @schedule.stub!(:days).and_return(days_proxy) @schedule.total_hours.should == 40 end end
Here we’ve taken advantage of the fact that association proxies in Rails are rich objects. ActiveRecord
gives us several methods for running database aggregate functions. We set up an expectation that days_proxy
should receive the sum
method with one argument—:hours
—and return 40
.
We can satisfy this specification with a very simple implementation:
class Schedule has_many :days def total_hours days.sum :hours end end
One valid criticism of this approach is that it makes our code harder to refactor. Our spec would fail if we changed the implementation of total_hours
to use Enumerable#inject
, even though the external behavior doesn’t change. Specifications are not only about describing the visible behavior of objects, but the interactions between an object and its associated objects as well. Mocking the association proxy in this case lets us clearly specify how a Schedule
should interact with its Days
.
A huge benefit of mocking the days
proxy is that we no longer rely on the database[5] in order to write our specifications and implement the total_hours
method. Our specs will run very quickly, and we don’t have any messy fixtures to deal with!
Leading mock objects advocates see mock objects as a temporary design tool. You may have noticed that we haven’t defined the Day
class yet. So another benefit of using mock objects is that they allow us to specify behavior in true isolation, and during design-time. There’s no need to break our design rhythm by stopping to create the Day
class and database table. This may not seem like a big deal for such a simple example, but for more involved specifications it is really helpful to just focus on the design task at hand. After the database and real object models exist, you can go back and replace the mock days_proxy
with calls to the real deal. This is a subtle, yet very powerful message about mocks that is usually missed.
Quick Mock ActiveRecord
Models
mock_model(model_class, stubs = {})The
mock_model
method creates mocks with autogenerated numeric ids and a number of certain common methods stubbed out:
id
. Returns the autogeneratedid
value
to_param
. Returns theid
value as a string
new_record?
Returnsfalse
errors
. Returns a stub errors collection that will report a 0 error count
is_a?
Returnstrue
if the parameter matchesmodel_class
class. Returns
model_class
You should pass in any additional stubbed method values via the
stubs
hash argument or set them in a block using the yielded mock instance.
RSpec gives you the ability to specify your controllers either in isolation from their associated views or together with them, as in regular Rails tests. According to the API docs:
Controller Specs use
Spec::Rails::DSL::ControllerBehaviour
, which supports running specs for Controllers in two modes, which represent the tension between the more granular testing common in TDD and the more high-level testing built into rails. BDD sits somewhere in between: we want to achieve a balance between specs that are close enough to the code to enable quick fault isolation and far enough away from the code to enable refactoring with minimal changes to the existing specs.The
Controller
class is passed to thedescribe
method like this:describe MessagesController do
An optional second parameter can provide additional information, or you can explicitly use the controller_name
method inside a describe
block to tell RSpec which controller to use.
describe "Requesting /messages using GET" do controller_name :messages fixtures :people
I typically group my controller examples by action and HTTP method. Fixtures are available if needed, like any other Rails test or spec. This example requires a logged-in user, so I stub my application controller’s current_person
accessor to return a fixture.
before(:each) do controller.stub!(:current_person, people(:quentin))
Next, I create a mock Message
object using the mock_model
method. I want this mock message to be returned whenever Message.find
is called during the spec.
@message = mock_model(Message) Message.stub!(:find).and_return([@message]) end
Now I can start specifying the behavior of actions (in this case, the index
action). The most basic expectation is that the response should be successful, HTTP’s 200 OK response code.
it "should be successful" do get :index response.should be_success end
I also want to specify that the find
method of Message
is called with the proper arguments.
it "should find all the messages" do Message.should_receive(:find).with(:all).and_return [@message] get :index end
Additional expectations that should be done for most controller actions include the template to be rendered and variable assignment.
it "should render index.rhtml" do get :index response.should render_template(:index) end it "should assign the found messages for the view" do get :index assigns[:messages].should include(@message) end
Previously we saw how to stub out a model’s association proxy. It would be nice not to have to use fixtures in the controller specs. Instead of stubbing the controller’s current_person
method to return a fixture, we can have it return a mock person.
@mock_person = mock_model(Person, :name => "Quentin") controller.stub!(:current_person).and_return @mock_person
By default, RSpec on Rails controller specs run in isolation mode, meaning that view templates are not involved. The benefit of this mode is that you can spec the controller in complete isolation of the view, hence the name. Maybe you can sucker someone else into maintaining the view specs? (The next heading in the chapter is all about spec’ing views.)
Actually, that “sucker” comment is facetious. Having separate view specs is not as difficult as it’s made out to be sometimes. It also provides much better fault isolation, which is a fancy way of saying that you’ll have an easier time figuring out what’s wrong when something fails.
If you prefer to exercise your views in conjunction with your controller logic inside the same controller specs, just as traditional Rails functional tests do, then you can tell RSpec on Rails to run in integration mode using the integrate_views
macro. It’s not an all-or-nothing decision—you can specify modes on a per-behavior basis.
describe "Requesting /messages using GET" do integrate_views
When you run integrated, the controller specs will be executed once with view rendering turned on.
Ordinarily, Rails rescues exceptions that occur during action processing, so that it can respond with a 501 error code and give you that great error page with the stack trace and request variables, and so on. In order to directly specify that an action should raise an error, you have to override the controller’s rescue_action
method, by doing something like this:
controller.class.send(:define_method, :rescue_action) { |e| raise e }
If you don’t mind just checking that the response code was an error, you can just use the be_an_error
predicate or response_code
accessor of the response
object:
it "should return an error in the header" do response.should be_an_error end it "should return a 501" do response.response_code.should == 501 end
One of Rails’ central components is routing. The routing mechanism is the way Rails takes an incoming request URL and maps it to the correct controller and action. Given its importance, it is a good idea to specify the routes in your application. You can do this with the route_for
method in a controller spec.
describe MessagesController, "routing" do it "should map { :controller => 'messages', :action => 'index' } to /messages" do route_for(:controller => "messages", :action => "index").should == "/messages" end it "should map { :controller => 'messages', :action => 'edit', :id => 1 } to /messages/1;edit" do route_for(:controller => "messages", :action => "edit", :id => 1).should == "/messages/1;edit" end end
Controller specs let us integrate the view to make sure there are no errors with the view, but we can do one better by specifying the views themselves. RSpec will let us write a specification for a view, completely isolated from the underlying controller. We can specify that certain tags exist and that the right data is outputted.
Let’s say we want to write a page that displays a private message sent between members of an internet forum. RSpec creates the spec/views/messages
directory when we use the rspec_controller
generator. The first thing we would do is create a file in that directory for the show view, naming it show_rhtml_spec.rb
. Next we would set up the information to be displayed on the page.
describe "messages/show.rhtml" do before(:each) do @message = mock_model(Message, :subject => "RSpec rocks!") sender = mock_model(Person, :name => "Obie Fernandez") @message.stub!(:sender).and_return(sender) recipient = mock_model(Person, :name => "Pat Maddox") @message.stub!(:recipient).and_return(recipient)
If you want to be a little more concise at the cost of one really long line of code that you’ll have to break up into multiple lines, you can inline the creation of the mocks like this:
describe "messages/show.rhtml" do before(:each) do @message = mock_model(Message, :subject => "RSpec rocks!", :sender => mock_model(Person, :name => "Obie Fernandez"), :recipient => mock_model(Person, :name => "Pat Maddox"))
Either way, this is standard mock usage similar to what we’ve seen before. Again, mocking the objects used in the view allows us to completely isolate the specification.
We now need to assign the message to the view. The rspec_on_rails
plugin gives us a familiar-looking assigns
method, which you can treat as a hash.
assigns[:message] = @message end
Fantastic! Now we are ready to begin specifying the view page. We’d like to specify that the message subject is displayed, wrapped in an <h1>
tag. The have_tag
expectation takes two arguments—the tag selector and the content within the tag. It wraps the assert_select
functionality included with Rails testing, which we covered extensively in Chapter 17.
it "should display the message subject" do render "messages/show" response.should have_tag('h1', 'RSpec rocks!') end
HTML tags often have an ID associated with them. We would like our page to create a <div
> with the ID message_info
for displaying the sender and recipient’s names. We can pass the ID to have_tag
as well.
it "should display a div with id message_info" do render "messages/show" response.should have_tag('div#message_info') end
What if we want to specify that the sender and recipient’s names should appear in <h3>
tags within the div
?
it "should display sender and recipient names in div#message_info" do render "messages/show" response.should have_tag('div#message_info') do with_tag('h3#sender', 'Sender: Obie Fernandez') with_tag('h3#recipient', 'Recipient: Pat Maddox') end end
Note that the view specs do not mix in helper methods automatically, in order to preserve isolation. If your view template code relies on helper methods, you need to mock or stub them out on the provided template
object.
The decision to mock versus stub those helper methods should depend on whether they’re an active player in the behavior you want to specify, as in the following example:
it "should truncate subject lines" do
template.should_receive(:truncate).exactly(2).times
render "messages/index"
end
If you forget to mock or stub helper method calls, your spec will fail with a NoMethodError
.
Speaking of helpers, it’s really easy to write specs for your custom helper modules. Just pass describe
to your helper module and it will be mixed into the spec class so that its methods are available to your example code.
describe ProfileHelper do it "profile_photo should return nil if user's photos is empty" do user = mock_model(User, :photos => []) profile_photo(user).should == nil end end
It’s worth noting that in contrast to view specs, all of the framework-provided ActionView::Helper
modules are mixed into helper specs, so that they’re available to your helper code. All dynamically generated routes helper methods are added too.
Rails comes with the scaffold_resource
generator to easily create RESTful controllers and the underlying models. The rspec_on_rails
plugin provides the rspec_scaffold
generator, which does the same thing using RSpec instead of Test::Unit
.
Play around with rspec_scaffold
when you have some free time—the generated specs are another source of very good example spec code for all three MVC layers. It generates specs that cover the Rails-generated code 100%, making it a very good learning tool.
There are several open-source projects that enhance RSpec’s functionality and your productivity. (None of these tools are unique to RSpec. In fact they all were originally written for Test::Unit
.)
The Autotest project is part of the ZenTest suite[6] created by Ryan Davis and Eric Hodel. As the name implies, it automatically runs your test suite for you. Each time you save a file in your project, Autotest will run any spec files that may be affected by the change. This is an excellent tool for getting in a solid red-green-refactor rhythm because you won’t have to switch windows to manually run the tests. In fact you don’t even need to run any command! Just cd
to your project’s directory and type autospec
to kick things off.
RCov is a code coverage tool for Ruby.[7] You can run it on a spec file to see how much of your production code is covered. It provides HTML output to easily tell what code is covered by specs and what isn’t. You can RCov individually on a spec file, or the rspec_on_rails
plugin provides the spec:rcov
task for running all of your specs under RCov. The results are outputted into a directory named coverage
and contain a set of HTML files that you can browse by opening index.html
(as shown in Figure 18.2):
Heckle is part of the Seattle Ruby Brigade’s awesome collection of projects,[8] and is another code coverage tool. Instead of simply checking the scope of your tests, Heckle helps you measure the effectiveness of your specs. It actually goes into your code and scrambles things like variable values and if
statements. If none of your specs break, you’re missing a spec somewhere.
The current versions of RSpec have Heckle support built-in. Just experiment with the --heckle
option and see what happens.
You’ve gotten a taste of the different testing experience that RSpec delivers. At first it may seem like the same thing as Test::Unit
with some words substituted and shifted around. One of the key points of TDD is that it’s about design rather than testing. This is a lesson that every good TDDer learns through lots of experience. RSpec uses a different vocabulary and style to emphasize that point. It comes with the lesson baked in so that you can attain the greatest benefits of TDD right away.
1. | Well, other than to write Chapter 17, which has full coverage of |
2. | You can grab a copy of the Monkeycharger project at http://monkeycharger.googlecode.com/. |
3. | Confused about the difference between mocks and stubs? Read Martin Fowler’s explanation at http://www.martinfowler.com/articles/mocksArentStubs.html. |
4. | If you have firewall trouble with that plugin because of the |
5. | Actually that’s not quite true. |
6. | |
7. | |
8. |
18.117.230.81