Pants
Pants

Reputation: 2772

Run before block once with RSpec 3

I can't find a way around this.

This is my test:

require 'rails_helper'

RSpec.describe V1::UsersController do

  describe '#create' do
    let(:post_params) do
      {
        first_nm: Faker::Name.first_name,
        last_nm: Faker::Name.last_name ,
        password: "test123456",
        password_confirmation: "test123456",
        email_address: Faker::Internet.email
      }
    end

    before do
      post :create, params: post_params
    end

    context 'successful create' do
      subject(:user) { User.find_by_email(post_params[:email_address]) }

      it 'persists the user' do
        expect(user).not_to be_nil
      end

      it 'user data is correct' do
        post_params.except(:password, :password_confirmation).each do |k, v|
          expect(user.send(k)).to eq(v)
        end
      end

      it 'returns responsde code of 201' do
        expect(response.status).to eq(201)
      end

    end
  end
end

I only want this controller to be hit once. However, I can't seem to get that to work.

I have tried setting before(:context) and I get an error

RuntimeError:
       let declaration `post_params` accessed in a `before(:context)` hook at:

       `let` and `subject` declarations are not intended to be called
       in a `before(:context)` hook, as they exist to define state that
       is reset between each example, while `before(:context)` exists to
       define state that is shared across examples in an example group.

I don't want multiple users to be persisted for such a simple test. I also dont want to be hitting the api for every example.

I want the before block to run once. How can I do this?

Upvotes: 2

Views: 2227

Answers (2)

Myron Marston
Myron Marston

Reputation: 21800

As the error message states, let and subject are specifically for managing per-example state. But before(:context)/before(:all) hooks get run outside the scope of any specific example, so they are fundamentally incompatible. If you want to use before(:context), you can't reference any let definitions from the hook. You'll have to manage the post_params state yourself without using let. Here's a simple way to do that:

require 'rails_helper'

RSpec.describe V1::UsersController do
  describe '#create' do
    before(:context) do
      @post_params = {
        first_nm: Faker::Name.first_name,
        last_nm: Faker::Name.last_name ,
        password: "test123456",
        password_confirmation: "test123456",
        email_address: Faker::Internet.email
      }

      post :create, params: @post_params
    end

    context 'successful create' do
      subject(:user) { User.find_by_email(@post_params[:email_address]) }

      it 'persists the user' do
        expect(user).not_to be_nil
      end

      it 'user data is correct' do
        @post_params.except(:password, :password_confirmation).each do |k, v|
          expect(user.send(k)).to eq(v)
        end
      end

      it 'returns responsde code of 201' do
        expect(response.status).to eq(201)
      end
    end
  end
end

That should solve your problem; however it's not the approach I would recommend. Instead, I recommend you use the aggregate_failures feature of RSpec 3.3+ and put all of this in a single example, like so:

require 'rails_helper'

RSpec.describe V1::UsersController do
  describe '#create' do
    let(:post_params) do
      {
        first_nm: Faker::Name.first_name,
        last_nm: Faker::Name.last_name ,
        password: "test123456",
        password_confirmation: "test123456",
        email_address: Faker::Internet.email
      }
    end

    it 'successfully creates a user with the requested params', :aggregate_failures do
      post :create, params: post_params

      expect(response.status).to eq(201)
      user = User.find_by_email(post_params[:email_address])
      expect(user).not_to be_nil

      post_params.except(:password, :password_confirmation).each do |k, v|
        expect(user.send(k)).to eq(v)
      end
    end
  end
end

aggregate_failures gives you a failure report indicating each expectation that failed (rather than just the first one like normal), just like if you had separated it into 3 separate examples, while allowing you to actually make it a single example. This allows you to incapsulate the action you are testing in a single example, allowing you to only perform the action once like you want. In a lot of ways, this fits better with the per-example state sandboxing provided by RSpec's features like before hooks, let declarations and the DB-transaction rollback provided by rspec-rails, anyway. And

I like the aggregate_failures feature so much that I tend to configure RSpec to automatically apply it to every example in spec_helper.rb:

RSpec.configure do |c|
  c.define_derived_metadata do |meta|
    meta[:aggregate_failures] = true unless meta.key?(:aggregate_failures)
  end
end

Upvotes: 2

Yuvraj Jaiswal
Yuvraj Jaiswal

Reputation: 1723

What you are looking for is before(:all), which will run once before all the cases. There is similar after(:all) as well.

Interestingly, the before is basically a shorter way of saying before(:each) (which IMO makes more sense).

Upvotes: -1

Related Questions