提问



我倾向于在块之前使用来设置实例变量。然后,我在我的示例中使用这些变量。我最近遇到了let()。根据RSpec文档,它习惯了



  ...定义一个memoized帮助方法。该值将在同一示例中的多个调用中缓存,但不跨示例缓存。



这与在块之前使用实例变量有什么不同?还应该何时使用let() vs before()?

最佳参考


我总是更喜欢let到一个实例变量,原因有两个:



  • 实例变量在引用时才会存在。这意味着如果你手指操作实例变量的拼写,将创建一个新的并初始化为nil,这可能导致细微的错误和误报。由于let创建了一个方法,当你拼错它时你会得到一个NameError,我认为这更好。它也更容易重构规范。

  • before(:each)挂钩将在每个示例之前运行,即使该示例不使用挂钩中定义的任何实例变量。这通常不是很重要,但如果实例的设置变量需要很长时间,然后你就会浪费周期。对于let定义的方法,初始化代码只在示例调用时运行。

  • 您可以直接将示例中的局部变量重构为let而不更改
    引用示例中的语法。如果重构实例变量,则必须更改
    如何引用示例中的对象(例如添加@)。

  • 这有点主观,但正如Mike Lewis指出的那样,我认为它使规范更容易阅读。我喜欢用let定义所有依赖对象的组织,并保持我的it块好看又短。


其它参考1


使用实例变量和let()之间的区别在于let()延迟评估。这意味着直到它定义的方法第一次运行才会评估let()


beforelet之间的区别在于let()为您提供了一种以级联方式定义一组变量的好方法。通过这样做,通过简化代码,规范看起来更好一些。

其它参考2


我在我的rspec测试中完全替换了实例变量的所有用法以使用let()。我为一位朋友写了一个简短的例子,用它来教一个小的Rspec类:http://ruby-lambda.blogspot.com/2011/02/agile-rspec-with-let.html [48]


正如其他一些答案所说的那样,let()是惰性求值的,因此它只会加载需要加载的那些。它会破坏规范并使其更具可读性。事实上我已经将我的控制器中使用的Rspec let()代码移植到了inherited_resource gem的样式中.http://ruby-lambda.blogspot.com/2010/06/stealing-let-from-rspec.html [49]


除了懒惰的评估,另一个优点是,结合ActiveSupport :: Concern,以及load-everything-in规范/支持/行为,您可以创建特定于您的应用程序的自己的spec mini-DSL。我已经编写过针对Rack和RESTful资源进行测试的文档。


我使用的策略是Factory-everything(通过Machinist + Forgery/Faker)。但是,可以将它与before(:each)块结合使用,为整个示例组预加载工厂,从而使规范运行得更快:http://makandra.com/notes/770-taking-advantage -of-rspec的-S-让合前块[50]

其它参考3


重要的是要记住,进行延迟评估,而不是将副作用方法放入其中,否则您将无法在之前更改为 (:每个)很容易。
您可以使用 let! 代替,以便在每个方案之前对其进行评估。

其它参考4


一般来说,let()是一种更好的语法,它可以节省你在整个地方输入@name符号。但是, caveat emptor!我发现let()也会引入微妙的错误(或者至少是头部划痕),因为变量在您尝试使用它之前并不存在......讲述故事sign:如果在let()之后添加puts以查看变量是否正确允许spec传递,但没有puts规范失败 - 你已经发现了这个微妙之处。


我还发现let()似乎并没有在所有情况下缓存!我在我的博客中写到:http://technicaldebt.com/?p=1242 [51]


也许只是我?

其它参考5


let是功能性的,因为它本质上是一个Proc。也是它的缓存。


我得到的一个问题就是......在一个正在评估变化的Spec块中。


let(:object) {FactoryGirl.create :object}

expect {
  post :destroy, id: review.id
}.to change(Object, :count).by(-1)


你需要确保在你的期望区之外调用let,即你在let区块中调用FactoryGirl.create。我通常通过验证对象是否持久来做到这一点。


object.persisted?.should eq true


否则,当第一次调用let块时,由于延迟实例化而实际发生数据库更改。


更新


只需添加备注。小心打码高尔夫或在这种情况下rspec高尔夫与这个答案。 [52]


在这种情况下,我只需要调用一个对象响应的方法。所以我在对象上调用_.persisted? _方法作为它的真实。我试图做的就是实例化对象。你可以调用空的吗?或者也可以调用nil?这一点不是测试,而是通过调用它来引导对象的生命。


所以你不能重构


object.persisted?.should eq true


成为


object.should be_persisted 


因为对象没有被实例化......它的懒惰。:)


更新2


利用让!即时对象创建的语法,应该完全避免这个问题。请注意,虽然它会打败很多非撞击的懒惰的目的。[53]


此外,在某些情况下,您可能实际上想要利用主题语法而不是let,因为它可能会为您提供其他选项。[54]


subject(:object) {FactoryGirl.create :object}

其它参考6


约瑟夫的注意事项 - 如果你在before(:all)中创建数据库对象,他们不会在事务中被捕获,你更有可能在测试数据库中留下瑕疵。请改用before(:each)


使用let及其懒惰评估的另一个原因是,你可以通过覆盖上下文中的let来获取一个复杂的对象并测试单个部分,就像在这个非常人为的例子中一样:


context "foo" do
  let(:params) do
     { :foo => foo,  :bar => "bar" }
  end
  let(:foo) { "foo" }
  it "is set to foo" do
    params[:foo].should eq("foo")
  end
  context "when foo is bar" do
    let(:foo) { "bar" }
    # NOTE we didn't have to redefine params entirely!
    it "is set to bar" do
      params[:foo].should eq("bar")
    end
  end
end

其它参考7


默认情况下,之前意味着before(:each)。 Ref The Rspec Book,copyright 2010,page 228。


before(scope = :each, options={}, &block)


我使用before(:each)为每个示例组播种一些数据,而不必调用let方法在it块中创建数据。在这种情况下,it块中的代码更少。


如果我想在某些示例中使用某些数据而不是其他示例,则使用let


之前和之前都非常适合干掉它块。


为避免混淆,let与before(:all)不同。 让重新评估每个示例(it)的方法和值,但在同一个示例中将值缓存到多个调用中。你可以在这里阅读更多相关信息:https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let [55]

其它参考8


我使用let使用上下文在我的API规范中测试我的HTTP 404响应。


要创建资源,我使用let!。但是为了存储资源标识符,我使用let。看看它的样子:


let!(:country)   { create(:country) }
let(:country_id) { country.id }
before           { get "api/countries/#{country_id}" }

it 'responds with HTTP 200' { should respond_with(200) }

context 'when the country does not exist' do
  let(:country_id) { -1 }
  it 'responds with HTTP 404' { should respond_with(404) }
end


这使规格清晰可读。