什么时候使用RSpec let()?

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

... 定义一个记忆helper方法。该值将在同一个示例中的多个调用之间缓存,但不会在多个示例之间缓存。

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

136403 次浏览

使用实例变量和let()的区别在于let()lazy-evaluated。这意味着let()在它定义的方法第一次运行之前不会被求值。

beforelet的区别在于,let()提供了一种以“级联”风格定义一组变量的好方法。这样做可以简化代码,使规范看起来更好一些。

我总是更喜欢let而不是实例变量,原因有几个:

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

相关链接可以在这里找到:< A href="http://www.betterspecs.org/#let" rel="noreferrer">http://www.betterspecs.org/#let

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

正如这里的一些其他答案所说,let()是延迟计算的,因此它只加载需要加载的对象。它dry了规范,使其更具可读性。事实上,我已经将Rspec let()代码移植到我的控制器中,以inherited_resource gem的风格使用。http://ruby-lambda.blogspot.com/2010/06/stealing-let-from-rspec.html

除了惰性求值外,另一个优点是,结合ActiveSupport::Concern和load-everything-in规范/support/行为,您可以创建自己的特定于应用程序的规范mini-DSL。我已经编写了针对Rack和RESTful资源的测试。

我使用的策略是工厂-一切(通过机械师+伪造/Faker)。但是,可以将它与before(:each)块结合使用,为整个示例组预加载工厂,从而使规范运行得更快:http://makandra.com/notes/770-taking-advantage-of-rspec-s-let-in-before-blocks

重要的是要记住是惰性求值的,并且没有在其中放置副作用方法,否则您将无法轻松地从更改为之前(每个)。 你可以使用< em >让!< / em >代替< em >让< / em >,以便在每个场景之前计算它

一般来说,let()是一个更好的语法,它可以节省你在所有地方输入@name符号。但是,购者自慎!我发现let()也引入了微妙的错误(或至少是挠头),因为变量并不真正存在,直到你尝试使用它…说明符号:如果在let()之后添加puts以查看变量是否正确,则可以通过规范,但没有puts则规范失败——你已经发现了这个微妙之处。

我还发现let()似乎在所有情况下都不缓存!我把它写在我的博客:http://technicaldebt.com/?p=1242

也许只有我这样?

"before"默认为before(:each)。参考Rspec Book,版权2010年,第228页。

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

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

如果在某些示例中需要某些数据,而在其他示例中不需要,则使用let

before和let都可以很好地晾干“it”块。

为了避免混淆,"let"与before(:all)不同。“Let”为每个示例(“it”)重新计算它的方法和值,但在同一个示例中跨多个调用缓存值。你可以在这里阅读更多关于它:https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let

let是功能性的,因为它本质上是一个过程。

我马上就发现了一个陷阱……在正在评估变更的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高尔夫与这个答案。

在这种情况下,我只需要调用对象响应的某个方法。所以我在对象上调用_.persisted?_方法作为它的真值。我要做的就是实例化对象。你可以称之为空?还是零?了。重点不在于测试本身,而在于通过调用来赋予对象生命。

所以你不能重构

object.persisted?.should eq true

object.should be_persisted

由于对象还没有被实例化…它的懒惰。:)

更新2

利用让!语法来即时创建对象,这应该可以完全避免这个问题。注意,虽然它会挫败很多懒惰的目的,非撞让。

此外,在某些情况下,你可能实际上想利用主题语法而不是let,因为它可能会给你额外的选项。

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

约瑟夫注意——如果你在before(:all)中创建数据库对象,它们不会在事务中被捕获,你更有可能在测试数据库中留下cruft。请改用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

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

这样可以保持规范的干净和可读性。

反对的声音在这里:经过5年的rspec,我不太喜欢let

1. 惰性计算常常使测试设置令人困惑

当在setup中声明的一些东西实际上不会影响状态,而其他东西则会影响状态时,很难对setup进行推理。

最终,出于沮丧,有人只是将let更改为let!(没有惰性计算也是一样的),以使他们的规范工作。如果这对他们有效,一个新的习惯就诞生了:当一个新的规范被添加到一个旧的套件中,而它不起作用时,作者尝试的第一个事情是向随机的let调用添加bangs。

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

2. 对于非rspec用户来说,特殊语法是不常见的

我宁愿把Ruby教给我的团队,而不是rspec的技巧。实例变量或方法调用在这个项目和其他项目中都很有用,let语法只在rspec中有用。

3.这些“好处”让我们很容易忽略好的设计变更

let()适用于我们不想反复创建的昂贵依赖项。 它也很好地与subject配对,允许你停止重复调用多参数方法

重复多次的昂贵依赖项和具有大签名的方法都是我们可以改进代码的地方:

  • 也许我可以引入一种新的抽象,将依赖项与我的其余代码隔离开来(这将意味着更少的测试需要它)。
  • 可能被测试的代码做的太多了
  • 也许我需要注入更智能的对象,而不是一长串原语
  • 也许我违反了“告诉-不要问”的规定
  • 也许昂贵的代码可以做得更快(很少-注意这里的过早优化)

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

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