Rails and RSpec - Difference Between let and let!


Suppose you have an app where users can add each other to their friendlist. The corresponding code for the model would be:

models/user.rb

class User < ApplicationRecord
  has_many :friendships
  has_many :friends, through: :friendships
  has_many :inverse_friendships, class_name: "Friendship", foreign_key: "friend_id"
  has_many :inverse_friends, through: :inverse_friendships, source: :user

  def has_friend?(user)
    friends.where(id: user.id).any? || inverse_friends.where(id: user.id).any?
  end
end

models/friendship.rb

class Friendship < ApplicationRecord
  belongs_to :user
  belongs_to :friend, class_name: "User"
end

Now you want to test the has_friend? method with RSpec, FactoryGirl and Faker. You create factories:

spec/support/factories.rb

FactoryGirl.define do
  factory :user do
    name { Faker::Name.name }
    email { Faker::Internet.email }
    password "12345"
  end

  factory :friendship do
    user
    association :friend, factory: :user
  end
end

Then you write your tests:

spec/models/user_spec.rb

require "rails_helper"

RSpec.describe User do
  context "instance methods" do
    let(:user1) { create(:user) }
    let(:user2) { create(:user) }
    let(:friendship) { create(:friendship, user: user1, friend: user2) }

    specify ".has_friend?" do
      expect(user2.has_friend?(user1)).to be_truthy
      expect(user1.has_friend?(user2)).to be_truthy
    end
  end
end

You run your tests... and see the red error message. The has_friend? method always returns false, though when setting a breakpoint right before your expectations (for example, using the Pry gem) you do see that the friendship was established correctly. So, what's up?

The problem is with the let(:friendship) { create(:friendship, user: user1, friend: user2) } line. The let method is somewhat lazy and will not actually do anything until the object is being called anywhere in your specs. As long as the friendship is not used anywhere in our tests, it is not being set and therefore user1.has_friend?(user2) will always return false.

The solution is really simple - use let! instead of let if you want to enforce the creation of an object. Therefore the code should be:

spec/models/user_spec.rb

require "rails_helper"

RSpec.describe User do
  context "instance methods" do
    let(:user1) { create(:user) }
    let(:user2) { create(:user) }
    let!(:friendship) { create(:friendship, user: user1, friend: user2) } # <===

    specify ".has_friend?" do
      expect(user2.has_friend?(user1)).to be_truthy
      expect(user1.has_friend?(user2)).to be_truthy
    end
  end
end

Now the friendship will be created and your test will turn green!

Follow me on Twitter to get updates about new articles >>