At the Forge - RSpec

 in
A new way of looking at testing.
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

______________________

Webinar
One Click, Universal Protection: Implementing Centralized Security Policies on Linux Systems

As Linux continues to play an ever increasing role in corporate data centers and institutions, ensuring the integrity and protection of these systems must be a priority. With 60% of the world's websites and an increasing share of organization's mission-critical workloads running on Linux, failing to stop malware and other advanced threats on Linux can increasingly impact an organization's reputation and bottom line.

Learn More

Sponsored by Bit9

Webinar
Linux Backup and Recovery Webinar

Most companies incorporate backup procedures for critical data, which can be restored quickly if a loss occurs. However, fewer companies are prepared for catastrophic system failures, in which they lose all data, the entire operating system, applications, settings, patches and more, reducing their system(s) to “bare metal.” After all, before data can be restored to a system, there must be a system to restore it to.

In this one hour webinar, learn how to enhance your existing backup strategies for better disaster recovery preparedness using Storix System Backup Administrator (SBAdmin), a highly flexible bare-metal recovery solution for UNIX and Linux systems.

Learn More

Sponsored by Storix