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.
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.