Rspec中的'let'和'let!'有什么区别?

7

我已阅读rspec文档并搜索了其他地方,但是我很难理解Rspec的letlet!之间的区别。

据我所知,let在需要时才初始化,并且其值仅在每个示例中缓存一次。我也了解到,let!强制将变量立即存在,并强制对每个示例进行调用。由于我是新手,我很难看出这与以下示例有何关联。为什么需要使用let!设置:m1以断言页面上存在m1.content,但可以使用let设置:user来断言页面包含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
2个回答

12

因为前置块调用了visit user_path(user),所以在那里初始化了用户值,RSpec会访问那个页面。如果:m1 :m2没有使用let!,那么访问将不会产生任何内容。

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

失败是因为它期望微博在用户访问页面之前就已经创建好了。let!允许在before块被调用之前就创建微博,当测试访问页面时,微博应该已经被创建好了。

另一种编写相同测试并使其通过的方法是执行以下操作:

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
调用变量m1m2在调用visit user_path(user)之前会导致它们在页面被访问之前被初始化,从而导致测试通过。 更新 这个小例子可能更有意义:
在这个例子中,我们调用了get_all_posts,它只返回一组帖子。注意,在断言之前和it块执行之前调用了该方法。因为直到执行断言时才会调用post。
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
使用let!,当RSpec看到该方法时(在before块之前),帖子将会被创建并返回Post列表中的帖子。另外,做相同的事情的另一种方式是在调用该方法之前,在before块中调用变量名。
before do
  post
  @response = get_all_posts
end

这样可以确保在调用方法之前先调用let(:post)块,创建Post并在Post.all调用中返回。


这很有道理。所以,使用let发送对象(m1)消息(content)是不够的,因为m1尚未初始化? - KMcA
那不是测试失败的原因。测试失败的原因是因为在创建微博之前,您已经“访问过该页面”,因此页面没有内容。如果您仍然有疑问,我可以更新我的答案,使其更清晰一些? - Leo Correa
好的,在重新查看我的问题(“保存用户”后)之后,第二个测试如何检查“user.name”呢? - KMcA
从某种意义上说,let实际上执行您传递给它的块并将结果分配给(:m1)。重要的是块内部的内容。这就是创建微博记录的地方,在控制器/视图的某个位置返回该记录,导致内容与m1中的内容匹配,而m1是FactoryGirl.create(:microposts)的结果。 - Leo Correa
是的,没错。我想我需要更深入地探索一下才能完全理解。谢谢你的帮助。 - KMcA
显示剩余2条评论

0

区分的关键在于rspec执行步骤的方式。

再次查看代码:

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) }

如果我们使用let而不是let!,则m1和m2不会立即创建。然后Rspec进行访问并加载页面,但显然页面上没有m1或m2。
因此,现在如果我们调用m1和m2,它们将在内存中创建。但由于页面不会再次加载,除非我们有意这样做,否则为时已晚。因此,对页面的任何UI测试都将导致失败。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接