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('[email protected]') }
it { should have_selector('title', text: user.name) }
it { should have_success_message('Welcome') }
it { should have_link('Sign out') }
end
You can use let! to force the method's invocation before each example. The difference between let, and let! is that let! is called in an implicit before block. So the result is evaluated and cached before the it block.
What does let do? let generates a method whose return value is memoized after the first call. This is known as lazy loading because the value is not loaded into memory until the method is called. Here is an example of how let is used within an RSpec test.
Running tests by their file or directory names is the most familiar way to run tests with RSpec. RSpec can take a file name or directory name and run the file or the contents of the directory. So you can do: rspec spec/jobs to run the tests found in the jobs directory.
Normally you want to use a Mock Object when you want to delegate some functionality to other object but you don't want to test the real functionality on your current test, so you replace that object with other that is easier to control. Let's call this object "dependency"...
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With