Rails Testing Antipatterns: Controllers

This is the third post in the series about antipatterns in testing Rails applications. See part 1 for thoughts on fixtures and factories, and part 2 on models.

Skipping controller tests

When starting out with Rails, with all the “magic” going on it can be easy to forget that controllers are just classes that need tests as well. If it seems to be hard to write a test for a controller, you should embrace the pain; it is telling you that your controller is doing too much and should be refactored.

A controller handles the request and prepares a response. So it shouldn’t be subscribing a user to a newsletter or preparing an order. In general we’ve found that controllers should be limited to:

– Extracting parameters;
– Checking for permissions, if necessary;
– Directly calling one thing from the inside of the app;
– Handling any errors, if they affect the response;
– Rendering a response or redirecting.

I’ve found that controllers are best tested with a mocking approach, since they tie many things together. The freedom you get from mocking should be used as an advantage in molding the controller method’s code to an ideal shape.

Creating records in controller tests

Because Rails controllers should be designed as “callers” of logic defined – and tested – elsewhere, it is pure overhead to involve the database in controller tests. Use mocks and test doubles to decouple from other application layers and stay focused on the task of specifying the functional behavior of the controller.

This is not really a controller spec but a semi-integration test:

describe "GET show" do
  let(:user) { create(:user) }
  let(:report) { create(:report, :user => user) }

  it "fetches user's report" do
    user_id = user.id
    report_file = report
    get :show, :user_id => user_id, :id => report_file.id
    assigns(:report).id.should eql(report.id)
    response.status.should eql(200)
  end
end

The code above is more like an attempt to write a passable test for an existing implementation. Contrast that with writing a test first, thinking only about interfaces, one assertion at a time, without interacting with the database:

describe "GET show" do
  let(:user) { build_stubbed(User) }
  let(:report) { build_stubbed(Report) }

  before do
    User.stub(:find) { user }
    user.stub(:find_report) { report }
  end

  describe "GET show" do
    it "finds the user's report" do
      user.should_receive(:find_report) { report }

      get :show, :user_id => user.id, :id => report.id
    end

    it "assigns a report" do
      get :show, :user_id => user.id, :id => report.id

      assigns(:report).should eql(report)
    end

    it "renders the 'show' template" do
      get :show, :user_id => user.id, :id => report.id

      response.should render_template("show")
      response.code.should eql(200)
    end
  end
end

Sometimes however, data is sensitive enough and goes through some kind of an API to warrant an integration test with real records. This is a use case for _request specs_.

# spec/requests/login_attempts_spec.rb
require "spec_helper"

describe "login attempts" do

  context "failed" do

    it "records a new failed login attempt" do
      expect {
        post user_session_path,
          :user => { :email => "user@example.com", :password => "badpassword" }
      }.to change(LoginAttempt, :count).by(1)
    end
  end
end

No controller tests, only integration

One might argue that it is not necessary to test controllers when there are comprehensive integration tests.

In practice, creating an integration test for every possible edge case is much harder and never really done. Integration tests are also much slower, because they always work with the database and may involve UI testing. Controller specs that use mocks and test doubles are extremely fast.

Complex controller spec setup

Epic setup is generally a code smell, telling us that our class is doing too much. See this context for verifying that a welcome email for a newly registered user goes out:

describe UsersController do

  describe "POST create" do

    context "on successful save" do

      before do
        @user = double(User)
        @user.stub(:save).and_return(true)

        User.stub(:new) { @user }

        @mailer = double(UserMailer)
        UserMailer.stub(:welcome) { @mailer }
        @mailer.stub(:deliver)

        ReferralConnector.stub(:execute)
        NewsletterSignupService.stub(:execute)
      end

      it "sends a welcome email" do
        @mailer.should_receive(:deliver)

        post :create, :user => { :email => "jo@example.com" }
      end
    end
  end
end

Difficult tests always go hand in hand with a problematic implementation. The spec above is for a controller that looks something like this:

class UsersController

  def create
    @user = User.new(user_params)

    if @user.save
      UserMailer.welcome(@user).deliver
      ReferralConnector.execute(@user)
      NewsletterSignupService.execute(email, request.ip)

      redirect_to dashboard_path
    else
      render :new, :error => "Registration failed"
    end
  end

  private

  def user_params
    params[:user].permit(*User::ACCESSIBLE_ATTRIBUTES)
  end
end

A developer following the BDD approach and thinking a bit in advance could write a spec for a method handling user registration so that all post-signup things, whatever they may be, are delegated to a PostSignup class, which can be tested in isolation.

describe "POST create" do

  context "on successful save" do

    before do
      User.any_instance.stub(:save) { true }
      PostSignup.any_instance.stub(:run)
    end

    it "runs the post signup process" do
      PostSignup.should_receive(:new) { post_signup }
      post_signup.should_receive(:run)

      post :create
    end

    it "redirects to dashboard" do
      post :create
      response.should redirect_to(dashboard_path)
    end
  end
end

Leave a Reply

Your email address will not be published. Required fields are marked *

Sign up for a weekly Semaphore newsletter