Karol Selak
Karol Selak

Reputation: 4804

How to DRY up RSpec tests with redundant `let!` method calling?

I how such a test block in my Rails app with RSpec:

describe "POST create" do
  describe "if we have a logged in user and he can be an owner" do
    describe "and if params are valid" do
      let!(:service_attributes_with_categories_1_and_2) {
        FactoryBot.attributes_for :service_with_categories_1_and_2
      }
      let!(:category_1) { FactoryBot.create :category, {id: 1} }
      let!(:category_2) { FactoryBot.create :category, {id: 2} }

      it "creates a new service" do
        # ...
      end
      it "creates associations with categories" do
        # ...
      end
    end
    describe "and if categories are not valid" do
      # ...
    end
    describe "and if some common service params are not valid" do
      # ...
    end
  end
  describe "if no user is logged in, but params are valid" do
    let!(:service_attributes_with_categories_1_and_2) {
      FactoryBot.attributes_for :service_with_categories_1_and_2
    }
    let!(:category_1) { FactoryBot.create :category, {id: 1} }
    let!(:category_2) { FactoryBot.create :category, {id: 2} }
    it "doesn't create a new service" do
      # ...
    end
    it "doesn't create associations with categories" do
      # ...
    end
  end
  describe "if logged user cannot be an owner, but params are valid" do
    let!(:service_attributes_with_categories_1_and_2) {
      FactoryBot.attributes_for :service_with_categories_1_and_2
    }
    let!(:category_1) { FactoryBot.create :category, {id: 1} }
    let!(:category_2) { FactoryBot.create :category, {id: 2} }

    it "doesn't create a new service" do
      # ...
    end
    it "doesn't create associations with categories" do
      # ...
    end
  end
end

As we can see, I have many redundant let! method calls, but I don't know how I could make it DRY. I cannot just define plain method, because in that case variables will be available in this method's scope only. I cannot also let my categories to be created in general scope, because in two cases they shouldn't be created because of test nature. So, how should I technically do that?

Upvotes: 1

Views: 217

Answers (2)

rhunal
rhunal

Reputation: 415

You can make your spec DRY as below :

1) Use let instead of defining method.

2) Arrange your context blocks in such a way that further cases will be accommodated easily.

describe 'POST create' do
  let!(:service_attributes_with_categories_1_and_2) {
    FactoryBot.attributes_for :service_with_categories_1_and_2
  }
  let!(:category_1) { FactoryBot.build :category, {id: 1} }
  let!(:category_2) { FactoryBot.build :category, {id: 2} }
  let(:save_categories_1_and_2) { category_1.save && category_2.save }

  context 'when user is logged in' do
    context 'when user is an owner' do
      context 'when params are valid' do
        before do
          save_categories_1_and_2
        end

        it 'creates a new service' do
        end

        it 'creates associations with categories' do
        end
      end

      context 'when categories are not valid' do
      end

      context 'when some common service params are not valid' do
      end
    end

    context 'when user is not an owner' do
      context 'when params are valid' do
        before do
          save_categories_1_and_2
        end

        it "doesn't create a new service" do
        end

        it "doesn't create associations with categories" do
        end
      end
    end
  end

  context 'when no user is logged in' do
    context 'when params are valid' do
      before do
        save_categories_1_and_2
      end

      it "doesn't create a new service" do
      end

      it "doesn't create associations with categories" do
      end
    end
  end
end

Upvotes: 1

Karol Selak
Karol Selak

Reputation: 4804

Finally I decided to split the FactoryBot.create function to two steps: FactoryBot.build and .save function run on obtained object. It allowed me to move my let! calls to the main scope, and to define method, which saves my built objects exactly in that cases where I need it. My DRY code looks like that now:

describe "POST create" do
  let!(:service_attributes_with_categories_1_and_2) {
    FactoryBot.attributes_for :service_with_categories_1_and_2
  }
  let!(:category_1) { FactoryBot.build :category, {id: 1} }
  let!(:category_2) { FactoryBot.build :category, {id: 2} }

  def save_categories_1_and_2
    category_1.save
    category_2.save
  end

  describe "if we have a logged in user and he can be an owner" do
    describe "and if params are valid" do
      before(:each) do
        save_categories_1_and_2
      end
      it "creates a new service" do
        # ...
      end
      it "creates associations with categories" do
        # ...
      end
    end
    describe "and if categories are not valid" do
      # ...
    end
    describe "and if some common service params are not valid" do
      # ...
    end
  end
  describe "if no user is logged in, but params are valid" do
    before(:each) do
      save_categories_1_and_2
    end
    it "doesn't create a new service" do
      # ...
    end
    it "doesn't create associations with categories" do
      # ...
    end
  end
  describe "if logged user cannot be an owner, but params are valid" do
    before(:each) do
      save_categories_1_and_2
    end
    it "doesn't create a new service" do
      # ...
    end
    it "doesn't create associations with categories" do
      # ...
    end
  end
end

Many thanks to @midlins and @Taryn East for pointing me to the right track. @Taryn East - your suggested adjustment would also work in the case described there, but in my app I have also more advanced cases, for which that's not enough. I think that the solution presented here is more universal.

Upvotes: 0

Related Questions