Perspectives

Controls

In my previous article we took a Behavior Driven Development approach to testing our data layer, in which our models were tested using RSpec. In this article I will showcase how RSpec can be used for controller testing. If you are new with RSpec, I will not go into detail with basic RSpec syntax such as should and it, please read my previous article, TDD, BDD and Using RSpec which goes over the basics to get you started.

Before we dive into controller testing, let's quickly create our app that will help birdkeepers find information on birds. Run the following commands:

rails birdkeeper -d mysql
cd birdkeeper
script/generate scaffold Bird title:string species_id:integer notes:text

Create birdkeeper_development and birdkeeper_test databases, add your database credentials to config/database.yml and migrate:

rake db:migrate
rake db:migrate RAILS_ENV=test

Then add the RSpec plugins (through git), gem (if not installed) and generate the spec directories:

sudo gem install rspec
script/plugin install git://github.com/dchelimsky/rspec.git
script/plugin install git://github.com/dchelimsky/rspec-rails.git
script/generate rspec

Open up app/controllers/birds_controller.rb, it should contain the 7 CRUD actions created from the Rails scaffold generator. If there are no actions, your version of Rails may need to be updated. Create birds_controller_spec.rb inside of spec/controllers. Add the following code:

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

describe BirdsController do

end

The first line loads in the spec_helper file which will contain common code that can be shared between specs. Next we have a describe block which helps keep our tests organized. This one will contain tests relating to the BirdsController, which will hold all our tests. It will also contain inner describe blocks for further organization. Let's test the update method, since in addition to performing a find call like most CRUD actions, it also updates the object. The update method we will be using is as follows:

# PUT /birds/1
# PUT /birds/1.xml
def update
  @bird = Bird.find(params[:id])

  respond_to do |format|
    if @bird.update_attributes(params[:bird])
      flash[:notice] = 'Bird was successfully updated.'
      format.html { redirect_to(@bird) }
      format.xml  { head :ok }
    else
      format.html { render :action => "edit" }
      format.xml  { render :xml => @bird.errors, :status => :unprocessable_entity }
    end
  end
end

Go back to the BirdsController spec and add the following inside of the BirdsController describe block:

# UPDATE
describe "PUT birds/:id" do
    describe "with valid params" do
    
    end
    
    describe "with invalid params" do
    
    end
end

Above we have added 3 describe blocks. One wrapper describe block will contain all tests relating to the update method. Inside there are two describe blocks, one with tests if valid params are given and the other if invalid params are given. Let's start with valid parameters. Before writing the actual tests, we need to set expectations. This is done through mocking and stubbing within a before block. Add the following inside the "with valid params" describe block:

before(:each) do
    @bird = mock_model(Bird)
    Bird.stub!(:find).with("1").and_return(@bird)
end

The before block will run before each test. This will DRY up our tests since we won't have to rewrite the same mocks and stubs for each test. Mocks and stubs allow us to test the controller functionality without relying on ActiveRecord. With mock_model we are imitating a Bird object. Stubs are used to fake method calls, we don't need to know the details of the actual method. We just know that the Bird class will receive a find call with a argument of "1" and it should successfully return a @bird object, which will be our mock. Then later in the same method, the @bird mock will receive an update_attributes call, so we also need to stub this call out. We can stub it out as follows:

@bird.stub!(:update_attributes).and_return(true)

But there is another way to accomplish this much more DRYly. Our mock model accepts a optional hash of method calls and their return value. We can modify our @bird mock object into:

@bird = mock_model(Bird, :update_attributes => true)

With the before block set up we can start writing tests. Tests are contained in it blocks and takes a string argument explaining its contents. Let's first test the find call, which is the first action to happen after update is called.

it "should find bird and return object" do
    Bird.should_receive(:find).with("1").and_return(@bird)
    put :update, :id => "1", :bird => {}
end

Here we are testing if the Bird class received a find call, with the should_receive syntax. The rest is very similar to the stub method since we are checking if it received "1" as an argument and returned a @bird object.

After a Bird object is found, its attributes are updated. Testing this call is very similar to the find call:

it "should update the bird object's attributes" do
    @bird.should_receive(:update_attributes).and_return(true)
    put :update, :id => "1", :bird => {}
end

Next we make sure a flash notice is set:

it "should have a flash notice" do
    put :update, :id => "1", :bird => {}
    flash[:notice].should_not be_blank
end

If the controller can have one of many flash notices, we can also be more specific:

it "should have a successful flash notice" do
    put :update, :id => "1", :bird => {}
    flash[:notice].should eql 'Bird was successfully updated.'
end

After the flash notice is set, the user should get redirected to the bird's show page. We can test the redirect by accessing the response object as so:

it "should redirect to the bird's show page" do
    put :update, :id => "1", :bird => {}
    response.should redirect_to(bird_url(@bird))
end

As for testing if there was invalid data, we would do this:

before(:each) do
    @bird = mock_model(Bird, :update_attributes => false)
    Bird.stub!(:find).with("1").and_return(@bird)
end

it "should find bird and return object" do
    Bird.should_receive(:find).with("1").and_return(@bird)
    put :update, :id => "1", :bird => {}
end

it "should update the bird object's attributes" do
    @bird.should_receive(:update_attributes).and_return(false)
    put :update, :id => "1", :bird => {}
end

it "should render the edit form" do
    put :update, :id => "1", :bird => {}
    response.should render_template('edit')
end

Most is similar to the valid data version, but there are a few differences. We are stubbing the object's update_attributes call to return false, this will cause the conditional to take the else route. Since there are errors, it needs to render the edit page. We test this by doing a response.should render_template('edit').

This is how the complete Bird Controller spec looks:

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

describe BirdsController do

  # UPDATE
  describe "PUT birds/:id" do

    describe "with valid params" do
      before(:each) do
        @bird = mock_model(Bird, :update_attributes => true)
        Bird.stub!(:find).with("1").and_return(@bird)
      end
      
      it "should find bird and return object" do
        Bird.should_receive(:find).with("1").return(@bird)
      end
      
      it "should update the bird object's attributes" do
        @bird.should_receive(:update_attributes).and_return(true)
      end
      
      it "should redirect to the bird's show page" do
        response.should redirect_to(bird_url(@bird))
      end
    end

    describe "with invalid params" do
      before(:each) do
        @bird = mock_model(Bird, :update_attributes => false)
        Bird.stub!(:find).with("1").and_return(@bird)
      end

      it "should find bird and return object" do
        Bird.should_receive(:find).with("1").return(@bird)
      end

      it "should update the bird object's attributes" do
        @bird.should_receive(:update_attributes).and_return(false)
      end

      it "should render the edit form" do
        response.should render_template('edit')
      end
      
      it "should have a flash notice" do
        flash[:notice].should_not be_blank
      end
    end
    
  end
end
Run this spec from the root of your application with the following command, all specs will pass:
ruby spec/controllers/birds_controller_spec.rb

Using the information learned here can be applied to writing tests for the other 6 CRUD actions. Since these actions were generated through the Rails scaffold generator, they should work and tests may not be needed. On the other hand, changes in functionality may cause new bugs to pop up. It is recommended to write tests and cover as much possible. It may become tedious to write tests for basic CRUD actions for each generated controller, luckily RSpec has a scaffold generator that will generate the same files complete with RSpec tests. The RSpec version of the scaffold can be used as follows:

script/generate rspec_scaffold Bird title:string species_id:integer notes:text

As a bonus, let's say we created a Species scaffold. A bird will belong to Species and we have the Birds controller nested within the Species controller. On the Species show page, we would run the following find methods for a list of birds that belong to that Species. Just how should the following be mock and stubbed?

@species = Species.find("1")
@birds_in_species = @species.birds.find(:all)

Take a few minutes to think about it. It is a bit more complicated, but like any complicated matter, can be simplified by breaking it down. I start off by mocking all objects involved, @species and @birds_in_species are a given. But we also can't forget the birds that are going to be returned from @species.birds, that needs to be mocked as well.

@birds_in_species = mock_model(Bird)
@species = mock_model(Species)
@birds = mock_model(Bird)

As for stubbing out the method calls. There are three in total, @species.birds.find counts as two.

Species.stub!(:find).with("1").and_return(@species)
@species.stub!(:birds).and_return(@birds)
@birds.stub!(:find).and_return(@birds_in_species)

Then in our tests, we would do the following:

Species.should_receive(:find).with("1").and_return(@species)
@species.birds.should_receive(:find).and_return(@birds_in_species)

I hope you have found this article helpful on testing controllers with RSpec. If you have checked out the generated specs through the rspec_scaffold generator, there are less tests for the update method. I prefer to have many smaller tests with each testing a small portion of the controller, that way when a single line is changed, the error from the test will be more helpful since it is more specific. I have found out about this approach from Mike Mangino, though ultimately how your specs are organized is a matter of personal preference.




RSS Feed


CATEGORIES


ARCHIVES


BOOKMARKED


Add to Technorati Favorites