Duplicated let blocks across dozens of spec files make test suites painful to maintain. RSpec’s shared_context solves this by extracting repeated variables into a single, reusable module.
What problem does shared context solve?
When multiple policy or service objects need the same user fixtures, you end up copy-pasting let(:user) and let(:admin) everywhere. Changes to the user factory then require updating every spec file individually.
Assume we have a policy object like this:
# 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
To test it we need an admin user and a resource owner. These same fixtures appear in every policy spec.
How to set up a shared context
Create a shared directory inside spec:
# 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
Load shared contexts in spec_helper.rb:
Dir[File.expand_path('shared/**/*.rb', __dir__)].each { |f| require f }
Now the spec file is much cleaner:
# 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
end
end
Practical Implementation: The USEO Approach
We apply shared contexts in a granular way. Rather than creating one massive shared_context with every possible fixture, we split them by domain: :users, :organizations, :billing_fixtures, etc. Each context file stays under 30 lines. This keeps include_context statements self-documenting and avoids loading unnecessary data into specs that do not need it.
A common mistake is stuffing mocks and stubs into shared contexts alongside let blocks. We keep shared contexts limited to data setup. Mocking behavior belongs in the spec itself or in a dedicated shared example group (shared_examples_for). Mixing the two creates hidden coupling where changing a mock in the shared file breaks unrelated specs.
When a shared context grows beyond 5-6 let variables, that is usually a signal the tested code has too many collaborators. We treat shared context bloat as a design feedback mechanism, not just a testing convenience. Pairing this approach with SimpleCov for test coverage tracking helps ensure shared contexts actually cover the code paths you care about.
Key takeaways
- Use
shared_contextfor variables repeated across multiple spec files. - Keep contexts small and domain-specific.
- You can include mocks in a shared context, but prefer separating data setup from behavior stubs.
- Name context files clearly so
include_context :usersreads naturally.