At the Forge - RSpec

by Reuven M. Lerner

Last month, I covered Shoulda, a Ruby gem that allows you to test your code using a method called behavior-driven development. BDD, as it is known, is closely related to test-driven development (TDD), which has become increasingly popular during the past few years, particularly within the Ruby community.

In both BDD and TDD, you start to program by writing a test that the program should pass, if it's working correctly. Of course, because the program hasn't been written yet, the test will fail. You then write the smallest amount of code possible to ensure that the test passes. When that happens, you continue coding by writing another test. The fact that your code is tested completely gives you the confidence and flexibility to “refactor”, moving code around and joining it together, without having to worry about introducing new, subtle bugs.

BDD differs from TDD not in its overall method, but rather in its approach and semantics. BDD concentrates on how things look from the outside, rather than from the inside of the code. In the case of a Web application, this often means looking at things from the user's perspective, or if you're a consultant, from the customer's perspective. No longer are you testing the code—instead, you are checking that it meets its specifications. Thus, working with BDD requires that you constantly think of yourself as a consumer of a particular piece of code, and that you consider what it should do at each point, if it is to work correctly. I intentionally use the word should here, because as you will see, that is an especially important word in the RSpec vocabulary, and it appears in nearly every test.

RSpec has become quite popular among Ruby programmers in general and Rails programmers in particular. It also is closely tied to several other high-quality testing technologies, such as Cucumber and Celerity, which I will explore in coming months. And, although RSpec is not everyone's cup of tea, it is popular enough that you should expect to encounter it if you do any Ruby development. Moreover, it is often good to try something different, and RSpec definitely is different, providing a new way of looking at testing.

Installing RSpec

The home page for RSpec is rspec.info, which contains instructions for installing RSpec, either on its own or as part of a Rails application. I'm looking at a simple Rails application this month as an example, so you need to install both parts.

The first requirement is installing two Ruby gems, both of which are stored on the popular repository for open-source projects, GitHub. You can install these gems with:

sudo gem install rspec rspec-rails -V --source
 ↪http://gems.github.com/

(If you already have installed GitHub as a source for gem installations, you don't need to specify it in this command.)

Note that if you have older RSpec-related gems installed, such as rspec_generator or spicycode_rspec_extensions, you probably should remove them from your system. Current versions of RSpec handle these functions for you, and I have encountered problems and conflicts that disappeared when I removed those old gems.

Now that you have RSpec installed, let's create a new, simple Rails project. I often like to use an address book (and appointment calendar) for my examples, so let's create one:

rails --database=postgresql appointments

Remember, Rails assumes you have three databases for your application, one each for the development, test and production environments. The database parameters are defined in config/database.yml. I assume you are able to set these configuration parameters correctly. Although you don't necessarily need a production database for the purposes of this column, you will need both development and test databases.

Now you must tell the Rails application to include RSpec. There are plugins for RSpec, but I generally prefer to use gems when possible. Modern versions of Rails allow you to include gems in config/environment.rb by adding the following two lines:

config.gem "rspec", :lib => false, :version => ">= 1.2.0"
config.gem "rspec-rails", :lib => false, :version => ">= 1.2.0"

With the gems in place, you now can put RSpec in place for your Rails application:

./script/generate rspec

This creates a spec directory (parallel to the test directory, which it effectively replaces). The spec directory contains, by default, three files:

  • rcov.opts: setting options for running the Ruby coverage tool rcov when run from within RSpec.

  • rspec.opts: setting options for RSpec itself.

  • spec_helper.rb: a Ruby file containing global definitions and configurations for the individual specifications, much like test_helper.rb performs in Test::Unit.

With the spec directory in place, you can begin to use the special RSpec generators for models, controllers and scaffolds. For example, you normally would generate a person model with:

./script/generate model person first_name:text last_name:text

This still will work, but any automatically generated tests will use Test::Unit, installing files into the test directory. By contrast, you can use:

./script/generate rspec_model person first_name:text last_name:text

This creates the same model file, but also creates a skeleton set of RSpec tests.

Model Testing with RSpec

Let's create a slightly more sophisticated version of the person model:

./script/generate rspec_model person first_name:text \
    last_name:text email_address:text phone_number:text \
    sex:text

This creates a migration, which you can use to create the first version of your person model:

rake db:migrate

Now, it's true that you should go into the migration file and modify things, such that (for example) the person's name, e-mail address and sex are all mandatory. However, let's ignore that step for now and assume that you want all of your validation logic to be at the application layer. In such a case, you would want to put some validations in the model file.

Well, you could do that, but that wouldn't be very BDD of you, would it? Rather, you should imagine the specification that a consumer, or the manager, might want from a “person” object, and then build the object up to adhere to those standards.

For example, you might want to ensure the presence of the first and last names. So, the first file to modify is spec/models/person_spec.rb, rather than app/models/person.rb. (For reasons I don't quite understand, Test::Unit calls model tests unit tests, and RSpec calls them model tests, and the controller tests are called functional tests.) If you open that file, you'll see a new, bare-bones specification:

require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')

describe Person do
  before(:each) do
    @valid_attributes = {
      :first_name => "value for first_name",
      :last_name => "value for last_name",
      :email_address => "value for email_address",
      :phone_number => "value for phone_number",
      :sex => "value for sex"
    }
  end

  it "should create a new instance given valid attributes" do
    Person.create!(@valid_attributes)
  end
end

You can run your full suite of specs at any time, by typing:

rake spec

The first line imports anything defined in spec_helper, which I mentioned earlier. Next comes a describe line; this will be familiar to those of you who have looked at Shoulda. The basic idea is that someone reading the specification reads the argument to “describe” and then reads each of the individual specifications that start with “it”. In other words, this spec file tries to say “Person should create a new instance given valid attributes.” And, sure enough, it does.

The before(:each) block tells RSpec what it should invoke before each “it” block. This ensures that the @valid_attributes instance variable will be set to a predictable value before running each spec. You then can modify @valid_attributes as necessary within each spec, as you will soon see.

The thing is, you're checking the validity of your specification by creating a new instance of Person. You can do that, but if the spec fails, you will end up with a code backtrace mixed in with your report. For this reason, I'm going to change the existing spec definition to look like this:

it "should create a new instance given valid attributes" do
  p = Person.new(@valid_attributes)
  p.should be_valid
  p.save.should_not == false
end

Instead of Person.create, you now are invoking Person.new, assigning it to the variable p. Let's check p in two different ways, once using should and the other using should_not. These methods are mixed in by RSpec to the Object class and contain a great deal of behind-the-scenes magic to make specifications readable, almost as if they were in plain English. For example, when you say:

p.should be_valid

RSpec's should method looks for a method named valid? for that object and checks that the invocation of this method returns true. This works for any predicate (that is, method that returns true or false). If should or should_not is followed by be_XXX, RSpec turns that into a method call of XXX? on the object instance.

So, you can understand what it means to say:

p.save.should_not == false

which you equivalently could write in a more positive, optimistic way:

p.save.should == true

In both cases, you invoke the save method on the object and check that its returned value is true. You might argue that you don't need to invoke both new and save on your object, but I like to make sure the object is valid in both Ruby and the database. After all, it could be that you told the database to reject null values, but that you allowed it using validations in your ActiveRecord definition.

Now let's move a bit beyond the defaults to set some limits on attributes. Presumably, you want people in your database to have all of these fields (first name, last name, e-mail address, phone number and sex) defined. If you were developing in a non-TDD/BDD way, you first would set up validations for all of those and then add some tests. But, here you're trying to write tests first, thinking from the “outside” how your object might behave. And indeed, each person should have a first name, a last name, an e-mail address and a telephone number. (Strange as it might seem now, there was once a time when having an e-mail address was not expected.)

So you could, for example, include the following:

it "should not be valid without a first name" do
  @valid_attributes.delete[:first_name]
  p = Person.new(@valid_attributes)
  p.should_not be_valid
  p.save.should == false
end

In other words, you take @valid_attributes, remove the :first_name key from it and then create a new person with the rest of the name-value pairs from @valid_attributes. This should not work, because everyone needs a first name. But when I run the specs, I get:

1)
'Person should not be valid without a first name' FAILED
expected valid? to return false, got true
./spec/models/person_spec.rb:23:

Finished in 0.038731 seconds

2 examples, 1 failure

In other words, the specification failed. But that's okay—that's precisely what you want when you're working in BDD fashion. You wrote a test, it failed, and now you can go into the code and modify it, so as to ensure that the test passes. Ensuring that this current test passes is a simple matter of adding a validation to your ActiveRecord model. Instead of being the empty default:


class Person < ActiveRecord::Base
end

you need to make it:


class Person < ActiveRecord::Base
    validates_presence_of :first_name
end

I save this change, run rake spec again, and sure enough, I get:

Finished in 0.070752 seconds
2 examples, 0 failures

What's next? Now I can move on to the other fields, one by one, in order to test them. And indeed, this back and forth is precisely the way you want to work when you're programming in TDD/BDD fashion. You add a spec indicating what the object should do, watch the spec fail and then add the appropriate line or lines for it to work that way.

You can get a bit fancier than merely checking whether attributes exist. RSpec's should method is very powerful, allowing you to check equality (==), numeric comparisons (< and >) and regular expression matches, among other things.

When using RSpec on models, to a large degree, you can rely on the built-in validations that Rails provides. For example, you presumably want the sex field to contain either an M or an F. If someone enters a value other than that, you should not save it to the database. The first step toward such a feature is the introduction of a new spec:

it "should forbid characters other than M and F" do
  @valid_attributes[:sex] = 'Z'
  p = Person.new(@valid_attributes)
  p.should_not be_valid
  p.save.should == false
end

I run rake spec, and find that this test fails. Again, that's to be expected, and now I can modify my Person class such that it is more restrictive:


class Person < ActiveRecord::Base
  validates_presence_of :first_name
  validates_inclusion_of :sex, :in => %w(M F), :message => 
  "Sex must be either M or F"
end

When I run rake spec, I get a failure, but not from this latest spec, which passed just fine, telling me that Z is illegal. Rather, what fails is the first spec, in which @valid_attributes has set the key sex to the value for sex. Once again, that's fine; the fact that I have moved forward in small, incremental steps gives me a chance to identify such issues and fix them, before things get too out of hand. By modifying @valid_attributes such that it uses an M (or an F, if you prefer), the specs work.

Conclusion

RSpec offers a refreshingly different, but still somewhat familiar, approach to issues of testing. By thinking in terms of behavior and specifications, rather than configuration and internals, it becomes easier to create tests. The natural “describe”, “it” and “should” terms used in RSpec were chosen carefully, and they help turn testing into a joint venture among all parties, not just programmers.

Although I covered only built-in RSpec matchers (that is, the test that comes after should), it is possible, and even encouraged, to create your own custom matchers for objects in your project.

Next month, I'll continue exploring RSpec by looking at the ways you can test controllers. This raises a number of questions and issues, including those having to do with model objects that are instantiated while inside a controller. As you will see, RSpec's “mock objects” will make this problem much less painful than it otherwise might be.

Resources

The home page for RSpec is rspec.info, and it contains installation and configuration documentation, as well as pointers to other documents. The Pragmatic Programmers recently released a book called The RSpec Book, written by RSpec maintainer David Chelimsky and many others actively involved in the RSpec community. If you are interested in using RSpec (or its cousin, the BDD tool Cucumber), this book is an excellent starting point. An RSpec mailing list, which is helpful and friendly but fairly high volume, is at groups.google.com/group/rspec.

Reuven M. Lerner, a longtime Web/database developer and consultant, is a PhD candidate in learning sciences at Northwestern University, studying on-line learning communities. He recently returned (with his wife and three children) to their home in Modi'in, Israel, after four years in the Chicago area.

Load Disqus comments

Firstwave Cloud