25 Nov 2020 · Semaphore News

    Rails Testing Antipatterns: Controllers

    5 min read
    Contents

    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 *

    Avatar
    Writen by:
    Marko Anastasov is a software engineer, author, and co-founder of Semaphore. He worked on building and scaling Semaphore from an idea to a cloud-based platform used by some of the world’s engineering teams.