labs rails

Rails 4: Testing strong parameters

UPDATE: Thanks to fellow Pivots Alex Kwiatkowski and Rick Reilly, we found that inheriting from ActionController::Parameters didn’t work for update_attribtues. Alex explains some of the changes they made. In the mean time, check out my repo for the example, including a commit for the failing test and the fix.

Since Rails 4 has been released we’ve been getting involved and learning about what has changed. One of the changes most discussed is strong parameters and I wanted to explore some ideas about how to test this new feature. Strong parameters are a way of white listing HTTP query parameters and moves the burden of whitelisting from the ActiveModel/ActiveRecord classes and into the controllers. I think this feels like a better place for this to happen.

Most examples I’ve seen encourage the following:


class UsersController < ApplicationController

  def create
    User.create(user_params)
  end

  private
  def user_params
    params.require(:user).permit(:name)
  end
end

However I think this encourages asking the question of how defensive to test the strong parameters portion of this. Should you do the defensive code for each action or is one enough?


describe "#create" do
    it 'creates a user' do
      User.should_receive(:create).
        with({name: 'Sideshow Bob'}.with_indifferent_access)
      post :create, user:
        { first_name: 'Sideshow', last_name: 'Bob', name: 'Sideshow Bob' }
    end
end

There are a few ways to extract the parameterisation whilst making it testable but the one which feels most natural for Rails is to extend the ActionController::Parameters class where it can be tested independently of the controller actions.

My first attempt lead me one way but I eventually ended up extending the ActionController::Parameters, but as Alex pointed out this doesn’t work for update_attributes


class UsersController < ApplicationController

  class UserParams < ActionController::Parameters
    def initialize params
      filtered_params = params.
        require(:user).
        permit(:name)
      super(filtered_params)
    end
  end

  def create
    User.create(UserParams.new(params))
  end

end

I went back to a previous thought and made the params call a class method.


class UsersController < ApplicationController

  class UserParams
    def build params
      params.require(:user).permit(:name)
    end
  end

  def create
    User.create(UserParams.build(params))
  end

end

Now the strong parameter aspects can be tested independently


  describe UsersController::UserParams do
    it 'cleans the params' do
      params = ActionController::Parameters.new(
        user: {foo: 'bar', name: 'baz'})
      user_params = UsersController::UserParams.build(params)
      expect(user_params).to eq({name: 'baz'}.with_indifferent_access)
    end
  end

The controller can then choose it’s interaction with the UsersController::UserParams class through strong (which requires knowing the controller and action) or weak stubbing:


describe "#create" do
    let(:http_params) do
      { user:
        { first_name: 'Sideshow', last_name: 'Bob', name: 'Sideshow Bob' }}
    end

    let(:model_params) { double(:model_params) }

    before do
      UsersController::UserParams.stub(:build) { model_params }
    end

    it 'creates a user with strong stubbing' do
      UsersController::UserParams.stub(:build).
        with(
          http_params.merge(controller: 'users', action: 'create').
            with_indifferent_access) { model_params }
      User.should_receive(:create).with(model_params)
      post :create, http_params
    end

    it 'creates a user with weak stubbing' do
      User.should_receive(:create).with(model_params)
      post :create, http_params
    end
  end

This now allows the tests to focus on the business logic of the actions rather than worrying about the defensive coding of strong parameters and also gives the developer confidence that the strong parameters are adequately tested.

The evolution of this can be seen in this repository, with thanks to Bryan Helmkamp and Alex Kwiatkowski.