Everyday Rails

How I learned to test my Rails applications, Part 4: Controller specs

By Aaron Sumner, April 07, 2012. File under: .
Controller testing has been soft-deprecated in Rails, but you're still likely to see controller specs in many Rails codebases. The basics covered in this post still apply. Be sure to read my posts on replacing controller specs with feature specs and request specs for an up-to-date take on testing your code. Thanks!

Poor controllers. As Rails developers we keep them skinny (which is a good thing) and often don’t give them due attention in our tests (which is a bad thing; more on that in a moment). As you continue to improve your application’s test coverage, though, controllers are the next logical chunk of code to tackle.

If you’re new to RSpec or Rails testing in general, I recommend reading through the previous posts in this series first:

Why test controllers?

Following the lead of some prominent Ruby developers I stopped working on controller specs for awhile, in favor of covering this functionality in my request specs (integration tests). At the time I liked this idea a lot–using tests that more closely mirrored how controller actions are accessed made good sense–but since then I’ve come back to testing controllers more explicitly, for a couple of primary reasons:

  1. Controllers are models too, as Piotr Solnica indicated in an excellent blog post. And in Rails applications, they’re pretty important models–so it’s a good idea to put them on equal footing, spec-wise, as your Rails models.
  2. Controller specs can be written more quickly than their integration spec counterparts. For me, this becomes critical when I encounter a bug that’s residing at the controller level, or I want to add additional specs to verify some refactoring. Writing a solid controller spec is a comparatively straightforward process, since I can generate very specific input to the method I’m testing without the overhead of request specs. This also means that
  3. Controller specs run more quickly than request specs, making them very valuable during bug fixing and checking the bad paths your users can take (in addition to the good ones, of course).

Controller testing basics

Scaffolds, when done correctly, are a great way to learn coding techniques. The spec files generated for controllers, at least as of RSpec 2.8, are pretty nice and provide a good template to help you build your own specs. Look at the scaffold generator in rspec-rails’ source, or generate a scaffold in your properly configured Rails application to begin getting a sense of these tests. (Another generator to look at is the one in Nifty Generator’s scaffolds).

A controller spec is broken down by controller method—each example is based off of a single action and, optionally, any params passed to it. Here’s a simple example:

it "redirects to the home page upon save" do
  post :create, contact: Factory.attributes_for(:contact)
  response.should redirect_to root_url
end

If you’ve been following along with this series, you may notice similarities to earlier specs we’ve written:

  • The description of the example is written in explicit, active language.
  • The example only expects one thing: After the post request is processed, a redirect should be returned to the browser.
  • A factory generates test data to pass to the controller method; note the use of Factory Girl’s attributes_for option, which generates a hash of values as opposed to a Ruby object.

However, there are also a couple of new things to look at:

  • The basic syntax of a controller spec—it’s REST method (post), controller method (:create), and, optionally, parameters being passed to the method.
  • The aforementioned attributes_for call to Factory Girl—not rocket science, but worth mentioning again because I had a habit early on of forgetting to use it versus default factories.

Organization

Let’s start with a top-down approach. As I mentioned in the previous post on model specs, it’s helpful to think about a spec as an outline. Continuing our use of an address book app as an example, here are some things I might need to test:

# spec/controllers/contacts_controller_spec.rb
require 'spec_helper'

describe ContactsController do
  describe "GET #index" do
    it "populates an array of contacts"
    it "renders the :index view"
  end
  
  describe "GET #show" do
    it "assigns the requested contact to @contact"
    it "renders the :show template"
  end
  
  describe "GET #new" do
    it "assigns a new Contact to @contact"
    it "renders the :new template"
  end
  
  describe "POST #create" do
    context "with valid attributes" do
      it "saves the new contact in the database"
      it "redirects to the home page"
    end
    
    context "with invalid attributes" do
      it "does not save the new contact in the database"
      it "re-renders the :new template"
    end
  end
end

And so on. As in our model specs, we can use RSpec’s describe and context blocks to organize examples into a clean hierarchy, based on a controller’s actions and the context we’re testing—in this case, the happy path (a user passed valid attributes to the controller) and the unhappy path (a user passed invalid or incomplete attributes). If your application includes an authentication or authorization layer, you can include these as additional contexts—say, testing with and without a logged-in user, or testing based on a user’s assigned role within the app.

With some organization in place, let’s go over some things you might want to test in your application and how those tests would actually work.

Setting up data

Just as in model specs, controller specs need data. Here again we’ll use factories to get started—once you’ve got the hang of it you can swap these out with more efficient means of creating test data, but for our purposes (and this small app) factories will work great.

We’ve already got a factory to generate a valid contact:

# spec/factories/contacts.rb
factory :contact do |f|
  f.firstname { Faker::Name.first_name }
  f.lastname { Faker::Name.last_name }
end

Now let’s add one to return an invalid contact:

factory :invalid_contact, parent: :contact do |f|
  f.firstname nil
end

Notice the subtle difference: The :invalid_contact factory uses the :contact factory as a parent. It replaces the specified attributes (in this case, firstname with its own; everything else will defer to the original :contact factory.

Setting up session data

To date, the little address book app we’ve built is pretty basic. It doesn’t even require a user to log in to view it or make changes. I’ll revisit this in a future post; for now I want to focus on general controller testing practices.

Testing GET methods

A standard Rails controller is going to have four GET-based methods: #index, #show, #new, and #edit. Looking at the outline started above we can add the following tests:

# spec/controllers/contacts_controller_spec.rb

# ... other describe blocks omitted omitted
describe "GET #index" do
  it "populates an array of contacts" do
    contact = Factory(:contact)
    get :index
    assigns(:contacts).should eq([contact])
  end
  
  it "renders the :index view" do
    get :index
    response.should render_template :index
  end
end

describe "GET #show" do
  it "assigns the requested contact to @contact" do
    contact = Factory(:contact)
    get :show, id: contact
    assigns(:contact).should eq(contact)
  end
  
  it "renders the #show view" do
    get :show, id: Factory(:contact)
    response.should render_template :show
  end
end

Pretty simple stuff for the methods that, typically, have the lightest load to carry in controllers. In this address book app, the new method is a little more complicated, though—it’s building some new phones to be nested in the contact information form:

# app/controllers/contacts_controller.rb

# ... other code omitted

def new
  @contact = Contact.new
  %w(home office mobile).each do |phone|
    @contact.phones.build(phone_type: phone)
  end
end

How to test iterating through those phone types? Maybe something like this:

# spec/controllers/contacts_controller_spec.rb

# ... other specs omitted

describe "GET #new" do
  it "assigns a home, office, and mobile phone to the new contact" do
    get :new
    assigns(:contact).phones.map{ |p| p.phone_type }.should eq %w(home office mobile)
  end
end

The point here is if your controller methods are doing things besides what a generator might yield, be sure to test those additional steps, too.

Testing POST methods

Let’s move on to our controller’s :create method. Referring back to our outline, we’ve got two contexts to test: When a user passes in attributes for a valid contact, and when an invalid contact is entered. The resulting examples look something like this:

# spec/controllers/contacts_controller_spec.rb

# rest of spec omitted ...

describe "POST create" do
  context "with valid attributes" do
    it "creates a new contact" do
      expect{
        post :create, contact: Factory.attributes_for(:contact)
      }.to change(Contact,:count).by(1)
    end
    
    it "redirects to the new contact" do
      post :create, contact: Factory.attributes_for(:contact)
      response.should redirect_to Contact.last
    end
  end
  
  context "with invalid attributes" do
    it "does not save the new contact" do
      expect{
        post :create, contact: Factory.attributes_for(:invalid_contact)
      }.to_not change(Contact,:count)
    end
    
    it "re-renders the new method" do
      post :create, contact: Factory.attributes_for(:invalid_contact)
      response.should render_template :new
    end
  end 
end

# rest of spec omitted ...

Let’s talk about that expect {} Proc for a minute. RSpec’s readability shines here—except this code to (or to not) do something. This one little example succinctly tests that an object is created and stored. (If Proc objects seem magical to you, refer to this post by Alan Skorkin and this one by Robert Sosinski to learn more.) Become familiar with this technique, as it’ll be very useful in testing a variety of methods in controllers, models, and eventually at the integration level.

Testing PUT methods

On to our controller’s update method, where we need to check on a couple of things—first, that the attributes passed into the method get assigned to the model we want to update; and second, that the redirect works as we want. Then we need to test that those things don’t happen if invalid attributes are passed through the params:

# spec/controllers/contacts_controller_spec.rb

# rest of spec omitted ...
describe 'PUT update' do
  before :each do
    @contact = Factory(:contact, firstname: "Lawrence", lastname: "Smith")
  end
  
  context "valid attributes" do
    it "located the requested @contact" do
      put :update, id: @contact, contact: Factory.attributes_for(:contact)
      assigns(:contact).should eq(@contact)      
    end
  
    it "changes @contact's attributes" do
      put :update, id: @contact, 
        contact: Factory.attributes_for(:contact, firstname: "Larry", lastname: "Smith")
      @contact.reload
      @contact.firstname.should eq("Larry")
      @contact.lastname.should eq("Smith")
    end
  
    it "redirects to the updated contact" do
      put :update, id: @contact, contact: Factory.attributes_for(:contact)
      response.should redirect_to @contact
    end
  end
  
  context "invalid attributes" do
    it "locates the requested @contact" do
      put :update, id: @contact, contact: Factory.attributes_for(:invalid_contact)
      assigns(:contact).should eq(@contact)      
    end
    
    it "does not change @contact's attributes" do
      put :update, id: @contact, 
        contact: Factory.attributes_for(:contact, firstname: "Larry", lastname: nil)
      @contact.reload
      @contact.firstname.should_not eq("Larry")
      @contact.lastname.should eq("Smith")
    end
    
    it "re-renders the edit method" do
      put :update, id: @contact, contact: Factory.attributes_for(:invalid_contact)
      response.should render_template :edit
    end
  end
end

# rest of spec omitted ...

The examples I want to point out here are the two that verify whether or not an object’s attributes are actually changed by the update method. Note that we have to call reload on @contact to check that our updates are actually persisted.

Testing DELETE methods

Testing destroy is relatively straightforward:

# spec/controllers/contacts_controller_spec.rb

# rest of spec omitted ...

describe 'DELETE destroy' do
  before :each do
    @contact = Factory(:contact)
  end
  
  it "deletes the contact" do
    expect{
      delete :destroy, id: @contact        
    }.to change(Contact,:count).by(-1)
  end
    
  it "redirects to contacts#index" do
    delete :destroy, id: @contact
    response.should redirect_to contacts_url
  end
end

# rest of spec omitted ...

By now you should be able to correctly guess what everything’s doing. The first expectation checks to see if the destroy method in the controller actually deleted the object; the second expectation confirms that the user is redirected back to the index upon success.

Summary

Now that I’ve shared some of the many things you can test in your controllers, let me be honest with you—it wasn’t until recently that I began testing at this level of my apps with such thoroughness. In fact, for a long time my controller specs just tested a few basics. But as you can see from RSpec’s generated examples, there are several things you can—and should—test at the controller level.

And with thoroughly tested controllers, you’re well on your way to thorough test coverage in your application as a whole. By now (between this post and the one on testing Rails models) you should be getting a handle on good practices and techniques for the practical use of RSpec, Factory Girl, and other helpers to make your tests and code more reliable.

In the MVC triumvarate, we’ve now covered the Model and Controller layers. Next time we’ll integrate the two—along with view—with RSpec request specs. Thanks as always for reading, and let me know what you think in the comments.

What do you think? Follow along on on Mastodon, Facebook, or Bluesky to let me know what you think and catch my latest posts. Better yet, subscribe to my newsletter for updates from Everyday Rails, book picks, and other thoughts and ideas that didn't quite fit here.
Buy Me A Coffee

Test with confidence!

If you liked my series on practical advice for adding reliable tests to your Rails apps, check out the expanded ebook version. Lots of additional, exclusive content and a complete sample Rails application.

Newsletter

Ruby on Rails news and tips, and other ideas and surprises from Aaron at Everyday Rails. Delivered to your inbox on no particular set schedule.