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 lets 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 lets 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 lets 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