RSpec(Pt. 2): Hooks, Subject, Shared Examples
Tags: ruby, rspec, testing, tdd
Published: 2020-08-11 21:00:15

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

```ruby
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
```

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: 

```ruby
before and after blocks are called in the following order:

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

- More on Before/After blocks: [Before/After Rspec Hooks](https://relishapp.com/rspec/rspec-core/v/3-7/docs/hooks/before-and-after-hooks)

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.

```ruby
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
```

The output for running these tests would be:

```
I run before example.run
example.run runs the example test
I run after example.run
```

- More on Around blocks: [RSpec Around Hook](https://relishapp.com/rspec/rspec-core/v/3-7/docs/hooks/around-hooks)

**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:

```ruby
let(:person)
```

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

```ruby
let(:person) { Person.new }
```

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

```ruby
before(:context) do
  def Person
    @person ||= Person.new
  end
end
```

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:

```ruby
subject { Person.new }
```

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.
```ruby
describe Person do
  context "attributes" do
    it "#name" do
      subject.name = "Ethan"
      expect(subject.name).to eq("Ethan")
    end
  end
end
```
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:

```ruby
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
```
- `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`:

```ruby
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
```

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