Using Before Blocks In RSpec
RSpec is a handy tool for writing tests in Ruby. Writing tests means constantly learning. It takes a long time to learn what to test, how to test, and how not to bite yourself in the ass later. And even then, you’ll learn more as you write your next set of tests.
The latest tests I’ve worked with are controller specs, to assert the behavior and output from Rails controller actions.
They have evolved to look something like:
Note: The code blocks below have some pseudocode. I want to convey my meaning here, rather than give 100% working code.
require 'spec_helper'
describe SomethingsController do
describe 'GET index' do
describe 'with an existing something' do
let!(:something) { FactoryGirl.create :something }
let(:action) { get :index }
# This is what we're interested in, for this post.
before do
action
end
it 'returns okay' do
expect(actual_status).to eq expected_status
end
it 'has the right output' do
expect(actual_output).to eq expected_output
end
end
end
end
The topic of this post is the before
block. It calls the action before each
it
block runs.
This helps reduce duplication across assertions, particularly when there are many. It also ensures we run the action for each assertion. It can be easy to overlook a missing action call.
But, there is a downside to this pattern. I believe RSpec runs before
blocks
as they are encountered. This means that, if you include shared examples, which
may include other contexts and other shared examples, then your tests might not
run as you expect.
Consider if we now want to authorize an API token given with the request. Since we want to do this across many controllers, we can include contexts and shared examples to ensure this same behavior in many areas.
require 'spec_helper'
describe SomethingsController do
describe 'GET index' do
describe 'with an existing something'
# NEW!
include_context 'with API token'
let!(:something) { FactoryGirl.create :something }
let(:action) { get :index }
before do
action
end
# NEW!
include_examples 'authorize API token'
it 'returns okay' do
expect{actual_status}.to eq expected_status
end
it 'has the right output' do
expect{actual_output}.to eq expected_output
end
end
end
end
The shared example would look something like:
shared_examples 'authorize API token' do
describe 'with no API token' do
before do
remove_header :api_token
end
it 'returns unauthorized status' do
expect(actual_status).to eq unauthorized_status_code
end
end
describe 'with invalid API token' do
before do
set_header :api_token, 'AnInvalidApiTokenHere'
end
it 'returns unauthorized status' do
expect(actual_status).to eq unauthorized_status_code
end
end
end
Now, when we run the specs, we get a failure. The actual_status
is 200
instead of the 401
we expect. Why is this?
In the shared examples, we modify the request’s headers, right? But we didn’t
modify the action like we expected to. The action already ran, thanks to that
before
block in the controller spec. So the action used the regular, valid
headers, because the contexts in which we modify the headers were included
after the action had run.
We didn’t modify the action/request before it ran, like we wanted. So we didn’t get the status we expected.
But we can fix this issue. The solution is to yank the before { action }
piece, and call the action in each it
block. This way we can modify contexts
as we need, and only run the action right before we check the assertion. This
is really what we want.
The controller specs, updated as described above, would look something like:
require 'spec_helper'
describe SomethingsController do
describe 'GET index' do
describe 'with an existing something'
include_context 'with API token'
let!(:something) { FactoryGirl.create :something }
let(:action) { get :index }
# We removed the before block with the action...
include_examples 'authorize API token'
it 'returns okay' do
#NEW!
action
expect{actual_status}.to eq expected_status
end
it 'has the right output' do
#NEW!
action
expect{actual_output}.to eq expected_output
end
end
end
end
And here are the shared examples:
shared_examples 'authorize API token' do
describe 'with no API token' do
before do
remove_header :api_token
end
it 'returns unauthorized status' do
# NEW!
action
expect(actual_status).to eq unauthorized_status_code
end
end
describe 'with invalid API token' do
before do
set_header :api_token, 'AnInvalidApiTokenHere'
end
it 'returns unauthorized status' do
# NEW!
action
expect(actual_status).to eq unauthorized_status_code
end
end
end
We must remember to manually call the action for each assertion, but we gain the benefit of greater flexibility. This is important for the tests we write now, and also those we’ll write in the future.
Oh yeah, since we kept things simple and reduced astonishment, our specs run like we expect them to. Now our tests are back to green!