何时使用RSpec let()?

时间:2011-03-19 02:10:04

标签: ruby rspec

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

  

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

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

10 个答案:

答案 0 :(得分:591)

由于以下几个原因,我总是更喜欢let到实例变量:

  • 实例变量在引用时才会存在。这意味着如果你手指操作实例变量的拼写,将创建一个新的并初始化为nil,这可能导致细微的错误和误报。由于let创建了一种方法,因此当你拼错它时,你会得到一个NameError,我认为这更好。它也使得重构规范变得更容易。
  • 在每个示例之前运行before(:each)挂钩,即使该示例不使用挂钩中定义的任何实例变量。这通常不是什么大问题,但如果实例变量的设置需要很长时间,那么你就是在浪费周期。对于let定义的方法,初始化代码仅在示例调用它时运行。
  • 您可以直接将示例中的局部变量重构为let而不更改 引用示例中的语法。如果重构实例变量,则必须更改 如何引用示例中的对象(例如,添加@)。
  • 这有点主观,但正如Mike Lewis指出的那样,我认为它使规范更容易阅读。我喜欢用let定义所有依赖对象的组织,并保持我的it块好看又短。

可以在此处找到相关链接:http://www.betterspecs.org/#let

答案 1 :(得分:79)

使用实例变量与let()之间的区别在于let() 懒惰评估。这意味着let()在第一次运行它定义的方法之前不会被评估。

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

答案 2 :(得分:17)

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

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

除了懒惰的评估,另一个优点是,结合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-let-in-before-blocks

答案 3 :(得分:13)

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

答案 4 :(得分:8)

通常,let()是一种更好的语法,它可以帮助您在整个地方输入@name个符号。但是,告诫者!我发现let()也会引入微妙的错误(或者至少是头部划痕)因为变量在您尝试使用之前并不存在...告诉故事签名:如果在puts之后添加let()以查看变量是否正确,则允许规范通过,但如果规范失败,则{-1}}会发现这一细微之处。 / p>

我还发现puts似乎并未在所有情况下缓存!我在我的博客中写道:http://technicaldebt.com/?p=1242

也许只是我?

答案 5 :(得分:5)

让它起作用,因为它本质上是一个过程。也是它的缓存。

我得到的一个问题就是......在一个正在评估变化的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块时,由于延迟实例化而实际发生数据库更改。

<强>更新

只需添加备注。小心玩code golf或在这种情况下使用此答案的rspec golf。

在这种情况下,我只需要调用一个对象响应的方法。所以我在对象上调用_.persisted? _方法作为它的真实。我要做的就是实例化对象。你可以打电话给空吗?还是没有?太。关键不在于测试,而是通过调用它来引导生命。

所以你无法重构

object.persisted?.should eq true

object.should be_persisted 

因为对象尚未实例化...它的懒惰。 :)

更新2

利用let! syntax创建即时对象,这应该完全避免这个问题。请注意,虽然它会击败非撞击的懒惰的许多目的。

同样在某些情况下,您可能实际上想要利用subject syntax代替let,因为它可能会为您提供其他选项。

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

答案 6 :(得分:2)

约瑟夫的注意事项 - 如果您在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 :(得分:1)

“之前”默认情况下隐含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

答案 8 :(得分:1)

这里有不同的声音:经过5年的重新规范,我不太喜欢let

1。懒惰的评估通常会使测试设置令人困惑

当在安装程序中声明的某些事物实际上并未影响状态而另一些事物确实影响状态时,就很难对安装程序进行推理了。

最终,出于挫败感,人们只是将let更改为let!(这是没有延迟评估的事情),以便使他们的规范正常工作。如果这对他们有效,那么就会养成新的习惯:将新规范添加到较旧的套件中而无法使用时,作者尝试的第一首先是在随机{ {1}}个电话。

很快所有的性能优势都消失了。

2。特殊语法对于非rspec用户是不常见的

我更愿意向我的团队教Ruby,而不是rspec的技巧。实例变量或方法调用在该项目以及其他项目中都非常有用,let语法仅在rspec中有用。

3。 “好处”使我们可以轻松忽略良好的设计更改

let适用于我们不想一遍又一遍创建的昂贵依赖项。 它还可以与let()配对,使您可以避免重复调用多参数方法

昂贵的依赖关系多次重复出现,带有大签名的方法都是我们可以使代码变得更好的两个方面:

  • 也许我可以引入一个新的抽象,将依赖与我的其余代码隔离开(这意味着更少的测试需要它)
  • 也许正在测试的代码做得太多
  • 也许我需要注入更智能的对象,而不是一长串的基元
  • 也许我违反了告诫
  • 也许可以使昂贵的代码更快(稀有-请提防过早的优化)

在所有这些情况下,我都可以使用舒缓的rspec魔术来解决困难测试的症状,或者我可以尝试解决原因。我觉得过去几年我花了太多时间在前者上,现在我想要一些更好的代码。

要回答最初的问题:我不愿意,但我仍然使用subject。我主要是用它来适应团队其他成员的风格(似乎世界上大多数Rails程序员现在都已经深入到他们的rspec魔术中了,这种情况经常出现)。有时,当我在无法控制的代码中添加测试或没有时间重构更好的抽象时使用它:即,唯一的选择是止痛药时。

答案 9 :(得分:0)

我使用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

这使规格保持清洁和可读。