As a developer, I constantly look for improving my code. Also in terms of tests. For me, it's important to keep tests clean in order to add new tests to be a pleasure, not a drudgery.
First time I stumbled across the shared context when I was looking for something that can help me to extract some constantly repeated variables in tests. The shared context was exactly what I was looking for. I will show you how to use it and what are the benefits of that.
What do we test?
Assume we have to write a few policy objects. They can look like this following one:
# lib/policies/user_settings_policy.rb
class UserSettingsPolicy
def can_show?
user.admin? || user.id == resource.id
end
private
attr_reader :user, :resource
def initialize(user, resource)
@user = user
@resource = resource
end
end
Somewhere we have defined User
and UserSettings
classes which instances are connected by user_id
. They may resemble ActiveRecord objects and a database relationship or they may be objects built based on events. Having that we can go to write tests. In this example, first_name
, last_name
, and email
are required to create a user object.
To test the policy object we need an admin user and a user who is the owner of a resource. Obviously the resource as well.
# spec/policies/user_settings_policy_spec.rb
RSpec.describe UserSettingsPolicy do
let(:user) do
Models::User.new(
id: SecureRandom.uuid,
first_name: 'Nick',
last_name: 'Owen',
email: 'nick.owen@exam.com'
)
end
let(:admin) do
Models::User.new(
id: SecureRandom.uuid,
first_name: 'Jack',
last_name: 'Jones',
email: 'jones@exam.com',
role: :admin
)
end
let (:resource) { Models::UserSettings.new(user_id: user.id) }
describe '#can_show?' do
context 'when a user has an admin role' do
subject { described_class.new(admin, resource) }
it 'returns true' do
expect(subject.can_show?).to be_truthy
end
end
context 'when a user has a user role' do
subject { described_class.new(user, resource) }
...
end
end
end
You have to admit these let
s took somewhat place. Ok, we can take care of testing the next policy object. Hmmmm... it means we need to declare the user and the admin in the subsequent spec file. This isn't the way.
It's time for the shared context
I've created the shared
directory inside of the spec
catalogue. Inside of that, I've created the users
file. I named it so to accurately specify the content of the file.
# spec/shared/users.rb
RSpec.shared_context :users do
let(:user_id) { SecureRandom.uuid }
let(:user) do
Models::User.new(
id: user_id,
first_name: 'Nick',
last_name: 'Owen',
email: 'nick.owen@exam.com'
)
end
let(:admin_id) { SecureRandom.uuid }
let(:admin) do
Models::User.new(
id: admin_id,
first_name: 'Jack',
last_name: 'Jones',
email: 'jones@exam.com',
role: :admin
)
end
end
The content of this file includes let
s that we saw before in the policy spec file. The only difference is that I extracted ids to separate variables because I don't want to use [user.id](http://user.id)
every time. There is still one thing to do, we have to load the shared context in the spec_helper.rb
file, like so:
Dir[File.expand_path('shared/**/*.rb', __dir__)].each { |f| require f }
The line above loads all .rb
files from the shared
directory.
Having that we can start to use the shared context what is pretty simple. Let's take a look at the spec file. Now, it's clearer, is it?
# spec/policies/user_settings_policy_spec.rb
RSpec.describe UserSettingsPolicy do
include_context :users
let (:resource) { Models::UserSettings.new(user_id: user_id) }
describe '#can_show?' do
context 'when a user has an admin role' do
subject { described_class.new(admin, resource) }
it 'returns true' do
expect(subject.can_show?).to be_truthy
end
end
context 'when a user has a user role' do
subject { described_class.new(user, resource) }
...
end
end
end
As you could notice, we need to include the context by using include_context :users
.
After that, we can use everything that we've defined inside of our context file. Now, we can write another policy object and tests for it without duplicating the users declarations.
Summary
The shared context is a good place to put variables that we use in more than one spec file. If you need you can also put mocks in your context. Sometimes it's hard to write a shared context that not contains unused variables but no worry, unused variables are let
s and they are initialized when you use them in a test file. Despite that, I really encourage you to write small shared contexts in order to easily include just what you need and keep the proper naming of context files.
Thanks
Geralt for the background photo
Sebastian Wilgosz for inspiring and encouraging me to write this article