Christopher
Stoll

Testing the Rails

A new year, a new programming language. I have recently started developing web applications using Ruby on Rails, rather than React+Redux on Flask+SQLAlchemy+Sqitch on Python. I’m enjoying the relative simplicity; it allows me to focus on creating complexity in other places, places where it will actually help me produce more features in less time. One area where I like to do more with less is in tests. No one likes to spend coding time writing tests, but it makes development easier and more productive in the long run, so it’s an absolute requirement.

One project I’m working on has an administrative area which is only accessible to privileged users. I needed to write test which ensure privileged users get access and everyone else does not. I could just fill in the default Rails generated tests for the authorized user, then copy and modify them for unauthorized users, but that becomes tedious if I want to check more than two roles.

Screenshot of testing code

To cover the other roles I first convert the original and copied tests to modules. Next, I structured the test into a hierarchy of inherited classes. At the leaf nodes of the hierarchy I sign in with the appropriate user and then include the relevant test module. This runs the the test modules as the desired user.

With the tests in place, there were some rough edges found in the authorization process. To smooth out the roughness, the privileged sections were moved to an administrative namespace. That required updating all the path helpers, which were still duplicated throughout the tests. To dry out that aspect of the test, helper methods were created on the base test class.

Below is a simple example which incorporates all of the design features mentioned above. It should seem that this complexity is too much for the test cases covered, the example was simplified to make the patterns easier to see. It takes more time to set up a test like this, but once controller access complexity increases it quickly becomes justified.

require 'test_helper'

module Admin::ThingyControllerTestPrivilegedUsers
  def test_should_get_index
    perform_get_index
    assert_response :success
  end

  def test_should_get_new
    perform_get_new
    assert_response :success
  end

  def test_should_create_thingy
    assert_difference('Thingy.count') do
      perform_post_create
    end
    assert_redirected_to admin_thingies_path()
  end

  def test_should_get_edit
    perform_get_edit
    assert_response :success
  end

  def test_should_update_thingy
    perform_patch_update
    assert_redirected_to admin_thingies_path()
  end

  def test_should_destroy_thingy
    assert_difference('Thingy.count', -1) do
      perform_delete
    end
    assert_redirected_to admin_thingies_path()
  end
end

module Admin::ThingyControllerTestUnprivilegedUsers
  def test_should_get_index
    perform_get_index
    perform_assert_unauthorized
  end

  def test_should_get_new
    perform_get_new
    perform_assert_unauthorized
  end

  def test_should_create_thingy
    assert_no_difference('Thingy.count') do
      perform_post_create
    end
    perform_assert_unauthorized
  end

  def test_should_get_edit
    perform_get_edit
    perform_assert_unauthorized
  end

  def test_should_update_thingy
    perform_patch_update
    perform_assert_unauthorized
  end

  def test_should_destroy_thingy
    assert_no_difference('Thingy.count') do
      perform_delete
    end
    perform_assert_unauthorized
  end
end

class Admin::ThingyControllerTest < ActionDispatch::IntegrationTest
  include Devise::Test::IntegrationHelpers

  setup do
    @thingy = thingies(:dinge_thingy)
  end

  def perform_get_index
    get admin_thingies_url()
  end

  def perform_get_new
    get new_admin_thingy_url()
  end

  def perform_post_create
    post admin_thingies_url(), params: {
      thingy: {
        location: @thingy.location_id,
      },
    }
  end

  def perform_get_edit
    get edit_admin_thingy_url(@thingy)
  end

  def perform_patch_update
    patch admin_thingy_url(@thingy), params: {
      thingy: {
        location: @thingy.location_id,
      },
    }
  end

  def perform_delete
    delete admin_thingy_url(@thingy)
  end

  def perform_assert_unauthorized
    assert_redirected_to root_url
    assert_equal 'You are not authorized to access this page.', flash[:error]
  end
end

class Admin::ThingyControllerTest::Authd < Admin::ThingyControllerTest
end

class Admin::ThingyControllerTest::Authd::Root < Admin::ThingyControllerTest
  include Admin::ThingyControllerTestPrivilegedUsers

  setup do
    sign_in users(:root)
  end
end

class Admin::ThingyControllerTest::Authd::Manager < Admin::ThingyControllerTest
  include Admin::ThingyControllerTestPrivilegedUsers

  setup do
    sign_in users(:manager)
  end
end

class Admin::ThingyControllerTest::Authd::Lead < Admin::ThingyControllerTest
  include Admin::ThingyControllerTestUnprivilegedUsers

  setup do
    sign_in users(:lead)
  end
end

class Admin::ThingyControllerTest::Authd::User < Admin::ThingyControllerTest
  include Admin::ThingyControllerTestUnprivilegedUsers

  setup do
    sign_in users(:user)
  end
end

The screenshot in this post was courtesey of carbon.

Tags: Testing Ruby Rails

Published: 2018-02-04