Recently I’ve had to restructure our company’s API, and part of that was writing new request specs for everything.
One HUGE headache I ran into while writing request specs for the API was bypassing controller before_actions
such as require_user
, as well as stubbing out crucial controller instance methods like current_user
.
I tried searching online; I don’t exaggerate when I say that I tried nearly everything:
let(:user) { FactoryGirl.create(:user) }
controller.stub(:current_user) { user }
#=> deprecated syntax
allow(controller).to receive(:current_user) { user }
#=> couldn't recognize current_user
allow(controller).to receive(:current_user).and_return(user)
#=> still couldn't recognize current_user...
allow(ApplicationController)to receive(:current_user).and_return(user)
#=> by this point I was ready to give up...
You get the idea… Hours of searching Stack Overflow and Relish and different docs didn’t yield me the answer I needed.
Then I realized that current_user
, require_login
and all these methods were INSTANCE METHODS. Not class methods. Here’s the correct way to stub out instance methods in RSpec:
let(:user) { FactoryGirl.create(:user) }
it "tests a controller action that requires a current user" do
allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user)
get "/some/controller/action/path"
expect(response.code).to eq("200")
# ...
end
This RSpec helper allows you to stub out any instance method that exists on any class. However, my major use for this has been for stubbing out controller methods such as current_user
, any controller before actions and access tokens if I’m doing request specs on a Rails API.
Example of controller action to test:
# Here we have users#show which will
# render the occupation of the user in json
class Api::UsersController < Api::ApplicationController
doorkeeper_for :all
before_action: require_user
def show
occupation = current_user.occupation
render json: { occupation: occupation }, status: 200
end
end
In this example, we are using the Doorkeeper gem to protect the controller against unauthorized API calls (the following example will work with other token authorization methods too, the process is similar).
So in this situation we would have to stub out the access token and the current user in order to successfully test the response of our request:
describe "GET /users/show " do
let(:user) { FactoryGirl.create(:user) }
let(:token) { double acceptable?: true } # this is way to stub doorkeeper tokens, if you're using other gems or methods, please refer to their respective counterparts.
before(:each) do
allow_any_instance_of(API::ApplicationController).to receive(:doorkeeper_token).and_return(token)
allow_any_instance_of(API::ApplicationController).to receive(:current_user).and_return(user)
get "/users/show"
end
it "should respond with status 200" do
expect(response.code).to eq("200")
end
end
#=> 1 example, 1 success
We have now succesfully stubbed out current_user
and doorkeeper_token
and can now successfully test our controller#action
!
BUT WAIT. There’s a bunch of code used just for stubbing, it would suck if we had to repeat that code over and over again for every request spec that we create… So let’s move that into a spec support in order to write less code!
# /app/spec/support/api_controller_helper.rb
module ApiControllerHelper
def stub_access_token(token)
allow_any_instance_of(Api::ApplicationController).to receive(:doorkeeper_token).and_return(token)
end
def stub_current_user(user)
allow_any_instance_of(Api::ApplicationController).to receive(:current_user).and_return(user)
end
end
RSpec.configure |config| do
config.include ApiControllerHelper
end
In the end, our spec will look like this:
describe "GET /users/show " do
let(:user) { FactoryGirl.create(:user) }
let(:token) { double acceptable?: true }
before(:each) do
stub_access_token(token)
stub_current_user(user)
get "/users/show"
end
it "should respond with status 200" do
expect(response.code).to eq("200")
end
end
#=> 1 example, 1 success
It took me quite a while to figure this out but in the end it was more than worth it. Armed with this knowledge, I’m sure you’ll be able to bang out pages and pages of request specs in no time!
TL;DR Use allow_any_instance_of(Object).to receive(method_on_object).and_return(mock_method_used_for_testing)
to mock an instance method on any object. Such as current_user
on a controller.