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_context for 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 :users reads naturally.