Controllers are powerful and complicated objects that easily accumulate responsibilities. Even when following the classic Rails convention of "Skinny Controllers, Fat Models," controllers still have a lot to handle. One extremely important responsibility is checking user authentication and authorization. The typical way to handle these two critical aspects of a web application in Rails is through the use of before_action
or before_filter
statements. These lines are called before specified actions in the controller and are capable of redirecting users before reaching those actions. For example:
We don't want any random person browsing the web to access the index or show pages of our top secret nuclear launch codes, so we define a before_action
filter to make sure a user is signed in. This method redirects every request to the login page unless a user is present. An unauthenticated browser cannot ever access those actions. In addition to that rule, our new
and create
actions have even tighter security: they can only be performed by admin users. Like the authenticate_user
filter, require_admin
will redirect any user to the home page unless they are an admin user.
This relatively simple technique actually works great for production environments. However, it definitely complicates the spec: if we try to run these actions in a test, the filters will boot us away from any action before we reach it, and thus all tests will fail. We could create a user in the spec file and log in before running the specs, but that can get extremely complicated very quickly. The users table may have validations that we'd need to know about and keep track of to successfully create a test user, and we'd need to manipulate the session hash to simulate being logged in, etc. But do not fear—there is a better way.
Authentication
First let's address authentication. Hopefully, logging in and out is tested somewhere else in the application (it had better be!), so we know that if those tests pass, the filter will work here, too. If it's safe to assume in the context of this test that the authentication method works, we just want to bypass it. It turns out we can do this fairly easily by stubbing the before_action
. Stubbing is a concept that often seems intimidating, but is actually quite simple: stubbing a method is like saying, "Any time this method is called, don't actually call it, just return X." The syntax looks like this: object.stub(:method) { return_value }
. In this case, our object is the controller being tested, the method is the authenticate_user
method, and it turns out our return value is nothing at all. Controller filters don't have a return value when they pass, but instead simply allow the controller to continue on to the requested action. So in our spec, we can simply write this to pass our index
and show
specs:
(I'm hiding the details of a spec_helper factory, which generates test launch codes.)
When get :index
and get :show
are called, the controller intends to make the before_action :authenticate_user
call. But the controller recognizes that the method has been stubbed out, so it ignores the production code and continues right along. Perfect!
Authorization
What about the new
and create
actions? We could stub out the call to the require_admin
filter like we do for authentication, but let's assume that this is the only place in our app that requires admin permissions. We want to be 100 percent sure that regular users can't create new launch codes, and this is the only place that will really be tested.
The trick here is to allow the before_action :require_admin
call to be made, but simulate being logged in as an admin user in one test and as a regular user in another. But remember, we want to do this without making user objects. Fortunately, current_user
is a method on the controller that returns and/or fetches the current user from the session hash (technically this would be in the ApplicationController, but for readability I've put it in the controller under test). Because current_user
is a method call, we can stub it and tell it to return some dummy admin user. However, that returned dummy can't just be a primitive Ruby object like a String, because it needs to have the method admin?
defined (otherwise we'd get an "undefined method" error). What we need is a "double":
The first line creates the double. The first argument, 'a user'
, is little more than an identifier, but the second argument is critical: it allows the object to have the method admin?
called on it and return true
. Essentially it is defining the method and stubbing it, all in one.
Once the double is set up, we stub the controller's current_user method
to return that double. The line can also be written like this: allow(controller).to receive(:current_user).and_return(admin_user)
.
We can now write the test for the new
action:
Here, get :new
is called. The controller recognizes new
as an action that necessitates first calling the require_admin
filter. The filter will redirect to the root page, unless current_user.admin?
. What is current_user
, a variable? A method? It's a method that has been stubbed on the controller to return a double object. Okay, next call admin?
on that object. Is it defined? Yes, in the definition/instantiation of the double, admin?
is set to return true
. The filter therefore does not redirect to root, but instead allows the request get :new
to continue on.
Final Refactor
After some refactoring, our controller spec becomes quite readable—the requisite high-level behavior is provided without cluttering up the specs, keeping the focus on the object under test: the controller.