DEV Community

Cover image for RSpec(Pt. 2): Hooks, Subject, Shared Examples
Ethan Gustafson
Ethan Gustafson

Posted on • Edited on

RSpec(Pt. 2): Hooks, Subject, Shared Examples

When writing tests with RSpec, you may need code to run at certain times. You also will need certain objects to test. This blog serves as a guide for using hooks, the let method, subject and shared examples.

Hooks

IMPORTANT NOTE: Hooks require the use of instance variables in order to make objects available to examples.

1.) Before

config.before(:suite) do # suite Scope
# You'd put this in your spec_helper file.
# This code runs one time at the very start. You can use it to
# run any setup before running any tests. 
end

before(:context) do # context scope
# The before block takes an argument like: (:context, :example)
# context scope refers to the describe or context blocks.
# So once before every describe/context block, this code will run.
end

before(:example) do # example Scope
# This code will run once before every example. So before every
# 'it' block, this code will run.
end
Enter fullscreen mode Exit fullscreen mode

2.) After -> Is exactly like the before block, except it is called in a different order.

This is what the Relishapp documentation for RSpec states about the order in which hooks are called:

before and after blocks are called in the following order:

before :suite
before :context
before :example
after  :example
after  :context
after  :suite
Enter fullscreen mode Exit fullscreen mode

As you can see, before runs in order from :suite, :context to :example. after runs in reverse from :example, :context to :suite.

3.) Around

around runs after before and after. With around, you set a block argument. It is like having both a before and after block. This allows us to do everything in one step.

around(:example) do |example| # example Scope
puts "I run before example.run" # This would be code run before the example
example.run # this will run the example
puts "I run after example.run" # This would be code run after the example
end

it "runs in the middle" do
puts "example.run runs the example test"
end
Enter fullscreen mode Exit fullscreen mode

The output for running these tests would be:

I run before example.run
example.run runs the example test
I run after example.run
Enter fullscreen mode Exit fullscreen mode

IMPORTANT NOTE: Hooks require the use of instance variables in order to make objects available to examples.

The let method & subject

The let method is a helper method. It takes one argument which is the name of the method you would like to use:

let(:person)
Enter fullscreen mode Exit fullscreen mode

Then you use a block to set an instance variable object you will use in other examples:

let(:person) { Person.new }
Enter fullscreen mode Exit fullscreen mode

This is a method. The above let method is doing exactly what this is:

before(:context) do
  def Person
    @person ||= Person.new
  end
end
Enter fullscreen mode Exit fullscreen mode

Remember that hooks need to use instance variables so that the object can be passed in to the examples.

Now you can use one instance of a person before every example.

subject

The subject method is exactly like let, except for a few things:

1.) When using a class name for the example group(the describe block), you don't have to define the name of the method using let. The name of the method will be the argument of the describe block, so you don't have to name the method:

subject { Person.new }
Enter fullscreen mode Exit fullscreen mode

2.) If you are keeping it simple, subject already made a helper method for you. So you wouldn't have to define the object either.

describe Person do
  context "attributes" do
    it "#name" do
      subject.name = "Ethan"
      expect(subject.name).to eq("Ethan")
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

See how I didn't have to define subject at all? The argument name of the example group was Person. subject already created a helper method with a simple new instance, Person.new.

Shared Examples

What if you have code that is repeated in multiple tests?

There are two things you can use:

  1. shared_examples_for()
  2. it_behaves_like()

In my previous RSpec blog I used my personal project "One Piece", which had two classes share the same tests. Here is how I used shared_examples_for and it_behaves_like

I created a separate file called character_devil_fruit.rb and in it:

shared_examples_for("character_devil_fruit") do
    it "Have attributes for :bio, :start_i, :end_i" do 
        expect(subject).to respond_to(:bio, :start_i, :end_i) 
    end 

    it ":start_i and :end_i must be integers" do
        subject.start_i = 2
        subject.end_i = 4
        expect(subject.start_i).to be_an(Integer)
        expect(subject.end_i).to be_an(Integer)
    end

    it "includes Instance Methods Module" do
        expect(described_class.included_modules).to include(InstanceMethods)
    end
end
Enter fullscreen mode Exit fullscreen mode
  • shared_examples_for allows you to contain code used in multiple spec tests. It takes one argument, which is the block name.
  • Which is also why we can use subject here so that we don't hard-code our class names.

And in character_spec.rb:

require_relative '../../lib/classes/character.rb'
require 'shared_examples/character_devil_fruit'

describe Character do

    context "attributes" do

        subject { Character.new("Luffy", "https://onepiece.fandom.com/wiki/Monkey_D._Luffy") } # Subject is just the starting describe argument name

        it "Instantiate with a name & URL" do
            expect(subject).to have_attributes(:name => "Luffy", :url => "https://onepiece.fandom.com/wiki/Monkey_D._Luffy")
        end

        it_behaves_like('character_devil_fruit')

        it ":name, :url & :bio must be strings" do
            subject.bio = "Kaizoku-ō ni ore wa naru!"
            expect(subject.name).to be_an(String)
            expect(subject.url).to be_an(String)
            expect(subject.bio).to be_an(String)
        end

    end

    context 'class Methods' do
        it '.all method which will record all instances of the class' do
            expect(Character.all).to be_an(Array)
        end
    end

end
Enter fullscreen mode Exit fullscreen mode
  • As you can see in the middle of the file, there is the it_behaves_like method which takes an argument of the shared example block argument name
  • Doing this will essentially be like 'copying' and 'pasting' the code in this file.

Top comments (0)