Search code examples
rspeclet

Trouble differentiating Rspec's 'let' vs 'let!'


I have read the rspec docs and have searched a number of other places but I am having a difficult time grasping the difference between Rspec's let and let!

I've read that let isn't initialized until it's needed and that its value is only cached per example. I've also read that let! forces the variable into immediate existence, and forces invocation for each example. I guess since I'm new, I'm having a difficult time seeing how this relates to the following examples. Why does :m1 need to be set with let! to assert m1.content is present on the page, but :user can be set with letto assert that the page contains text: user.name?

  subject { page }

  describe "profile page" do
    let(:user) { FactoryGirl.create(:user) }
    let!(:m1) { FactoryGirl.create(:micropost, user: user, content: "Foo") }
    let!(:m2) { FactoryGirl.create(:micropost, user: user, content: "Bar") }

    before { visit user_path(user) }

    it { should have_selector('h1',    text: user.name) }
    it { should have_selector('title', text: user.name) }

    describe "microposts" do
      it { should have_content(m1.content) }
      it { should have_content(m2.content) }
      it { should have_content(user.microposts.count) }
    end
  end

  describe "after saving the user" do
    before { click_button submit }
    let(:user) { User.find_by_email('user@example.com') }

    it { should have_selector('title', text: user.name) }
    it { should have_success_message('Welcome') } 
    it { should have_link('Sign out') }
  end

Solution

  • Because the before block is calling visit user_path(user) the user value gets initialized there and RSpec will visit that page. If the :m1 :m2 were not using let! then the visit would yield no content making

    it { should have_content(m1.content) }
    it { should have_content(m2.content) }
    

    fail because it expects the microposts to be created before the user visits the page. let! allows the microposts to be created before the before block gets called and when the tests visit the page the microposts should've already been created.

    Another way to write the same tests and have them pass is doing the following:

    describe "profile page" do
      let(:user) { FactoryGirl.create(:user) }
      let(:m1) { FactoryGirl.create(:micropost, user: user, content: "Foo") }
      let(:m2) { FactoryGirl.create(:micropost, user: user, content: "Bar") }
    
      before do
        m1
        m2
        visit user_path(user)
      end
    

    calling the variables m1 and m2 before visit user_path(user) causes them to be initialized before the page is visited and causing the tests to pass.

    UPDATE This small example would make more sense:

    In this example we are calling get_all_posts which just returns an array of posts. Notice that we're calling the method before the assertion and before the it block gets executed. Since post doesn't get called until the assertion is executed.

    def get_all_posts
      Post.all
    end
    
    let(:post) { create(:post) }
    
    before { @response = get_all_posts }
    
    it 'gets all posts' do 
      @response.should include(post)
    end
    

    by using let! the post would get created as soon as RSpec sees the method (before the before block) and the post would get returned in the list of Post

    Again, another way to do the same would be to call the variable name in the before block before we call the method

    before do
      post
      @response = get_all_posts
    end
    

    as that will ensure that the let(:post) block gets called before the method itself is called creating the Post so that it gets returned in the Post.all call