← Back to all posts

How to share tests for before_actions in Rails

David Anderson | Apr 24, 2019

Checkout the example below to learn how I share tests using Rails testing in a similar way to RSpec’s shared examples

For this site I have an admin area where I manage the blog posts and site images. My routes look something like:

namespace :admin do
  resources :posts
  resources :images
end

I’ve created an Admin::BaseController that the other admin controllers inherit from and I put my shared code in there. For example, I have two before_actions that all admin controllers need. The first one makes sure that a user is logged in (I’m using Devise for user authentication, so I have access to its authenticate_user! method) and the second one confirms that the logged in user is a site admin.

My set up looks like this:

# app/controllers/admin/base_controller.rb
class Admin::BaseController < ApplicationController
  before_action :authenticate_user!
  before_action :authorize_admin!

  def authorize_admin!
    unless current_user.is_admin
      flash[:error] = "Access Denied"
      redirect_to root_url
    end
  end
end
# app/controllers/admin/posts_controller.rb
class Admin::PostsController < Admin::BaseController
  # index, new, create, edit, etc. actions
end
# app/controllers/admin/images_controller.rb
class Admin::ImagesController < Admin::BaseController
  # index, new, create, edit, etc. actions
end

How do you test that each admin controller makes sure that we have a current_user and that the current_user is an admin? I could write two tests and copy and paste those test for every admin controller and each of its actions, but all that repetition doesn’t feel right. I want to use something like RSpec’s shared examples, but since I’m using the default Rails testing setup I don’t have an equivalent feature. I’ll build my own way to share tests!

I’ll define two class methods in my test/test_helper.rb file for the ActiveSupport::TestCase class. Each method will take a block, execute the block, and then make the appropriate assertions. Now from each test/controllers/admin file, I can use these two methods and simply pass in the request that I want to test. Nice!

# test/test_helper.rb
class ActiveSupport::TestCase
  include Devise::Test::IntegrationHelpers

  fixtures :all

  def self.it_requires_authentication(&block)
    test "requires authentication" do
      instance_exec(&block)
      assert_redirected_to new_user_session_path
    end
  end

  def self.it_requires_admin(&block)
    test "requires an admin user" do
      sign_in users(:david)
      instance_exec(&block)
      assert_redirected_to root_url
      assert_equal 'Access Denied', flash[:error]
    end
  end
end
# test/controllers/admin/posts_controller_test.rb
class Admin::PostsControllerTest < ActionDispatch::IntegrationTest
  it_requires_authentication { get admin_posts_url }
  it_requires_admin { get admin_posts_url }

  test "should get index" do
    david = users(:david)
    david.update!(is_admin: true)
    sign_in david
    get admin_posts_url
    assert_response :success
  end
end
# test/controllers/admin/images_controller_test.rb
class Admin::ImagesControllerTest < ActionDispatch::IntegrationTest
  it_requires_authentication { get admin_posts_url }
  it_requires_admin { get admin_posts_url }

  test "should get index" do
    david = users(:david)
    david.update!(is_admin: true)
    sign_in david
    get admin_images_url
    assert_response :success
  end
end

I have to give credit where credit is due! This implementation was inspired by this blog post.

SUBSCRIBE

Drop your email in the box below to subscribe to my newsletter. Once per week you'll get Ruby/Rails tips, guides, job postings, and general thoughts from the web developer trenches.