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 let
to 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
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