Blog

Rspec: Shared Examples and Contexts

Rodrigo Souza
Rodrigo Souza
November 15, 2022

The best part of choosing Rspec as your tool for automated testing, is the possibility to describe your expectations with very readable sentences. Today, We will present to you two components of Rspec that can make it even more doable.

Shared Contexts

Shared contexts is a way to stick to the don’t repeat yourself rule. It allows us to reuse some test boilerplate such as let, subject, described_class, and other generalisms, for different sets of tests. Let’s see the example below:


# app/models/bottle.rb
class Bottle < ApplicationRecord 
MAX_CAPACITY = 330
  def full?
   current_content >= MAX_CAPACITY
  end
end

# app/models/bowl.rb
class Bowl < ApplicationRecord
  MAX_CAPACITY = 600
  def full?
    current_content >= MAX_CAPACITY
  end
End

Let’s imagine two models, Bottle and Bowl. As containers, they share a common method called full?. Let’s see how we could write our tests using shared_context:

 
# spec/support/shared_contexts/filled.rb
 RSpec.shared_context 'filled' do
   let(:current_capacity) { 50 }
   subject { described_class.new(current_capacity: current_capacity) }
 end
 # spec/models/bottle_spec.rb
 RSpec.describe Bottle, type: :model do
  describe 'full?' do
    context 'when the bottle is' do
      context 'not full' do
       include_context 'filled'
       it 'returns false' do
          expect(subject).to be_falsey
       end
    end
      context 'is full' do
       include_context 'filled' do
        let(:current_capacity) { 330 }
       end
       it 'returns false' do
        expect(subject).to be_falsey
      end
    end
    end
   end
 end
# spec/models/bowl_spec.rb
RSpec.describe Bowl, type: :model do
  describe 'filled?' do
   context 'when the bowl is' do
    context 'not full' do
     include_context 'filled'
      it 'returns false' do
       expect(subject).to be_falsey
     end
   end
   context 'is full' do
     include_context 'filled' do
      let(:current_capacity) { 600 }
<pre><code class="language-ruby">   end</code></pre>
     it 'returns true' do
       expect(subject).to be_truthy
     end
    end
  end
 end
End

See? The tests are using the same configuration context and build different objects to run correctly. Easy and clean, no? Perfect but, reading the specs you could say: Ok, but the tests per si seem pretty equal to me and duplicated no? I need to agree with you. The next topic will help us to improve that.

Shared Examples

Shared examples allow us to reuse a bunch of tests for multiple features of our applications. Using the same example above, we could have something like this:


# spec/support/shared_examples/fillable_spec.rb
RSpec.shared_examples 'Fillable' do |received_capacity|
  describe 'full?' do
    context "when the #{described_class.downcase}" is do
      context 'not full' do
        include_context 'filled'
        it 'returns false' do
          expect(subject).to be_falsey
        end
      end
  
      context 'is full' do
        include_context 'filled' do
          let(:current_capacity) { received_capacity }
        end
        it 'returns true' do
          expect(subject).to be_truthy
        end
      end
    end
  end
end
# spec/models/bottle_spec.rb
RSpec.describe Bottle, type: :model do
  it_behaves_like 'Fillable', 330
end
# spec/models/bowl_spec.rb
RSpec.describe Bowl, type: :model do
  it_behaves_like 'Fillable', 600
end

Clean and tidy code so when you look at it a year from now you’ll be proud. Shared context and examples are great tools to DRY up your code, make it more readable and pleasure to work with it. As with any examples, it’s best practice to be cautious about the complexity of the abstractions when using these tools. If you need to override variables of the context all the time, or over-configure the call to the shared examples, it could indicate you are in search of a different abstraction solution.