我倾向于使用before块来设置实例变量。然后在示例中使用这些变量。我最近偶然发现let()。根据RSpec文档,它被用来
... 定义一个记忆helper方法。该值将在同一个示例中的多个调用之间缓存,但不会在多个示例之间缓存。
这与在块之前使用实例变量有什么不同?以及什么时候应该使用let() vs before()?
我倾向于使用before块来设置实例变量。然后在示例中使用这些变量。我最近偶然发现let()。根据RSpec文档,它被用来
... 定义一个记忆helper方法。该值将在同一个示例中的多个调用之间缓存,但不会在多个示例之间缓存。
这与在块之前使用实例变量有什么不同?以及什么时候应该使用let() vs before()?
使用实例变量和let()之间的区别在于let()是延迟求值的。这意味着let()直到它定义的方法第一次运行时才会被求值。
before和let之间的区别在于,let()提供了一种以“级联”风格定义一组变量的好方法。这样做可以简化代码,使规范看起来更好一些。
我总是更喜欢使用let而不是实例变量,原因如下:
Instance variables spring into existence when referenced. This means that if you fat finger the spelling of the instance variable, a new one will be created and initialized to nil, which can lead to subtle bugs and false positives. Since let creates a method, you'll get a NameError when you misspell it, which I find preferable. It makes it easier to refactor specs, too. A before(:each) hook will run before each example, even if the example doesn't use any of the instance variables defined in the hook. This isn't usually a big deal, but if the setup of the instance variable takes a long time, then you're wasting cycles. For the method defined by let, the initialization code only runs if the example calls it. You can refactor from a local variable in an example directly into a let without changing the referencing syntax in the example. If you refactor to an instance variable, you have to change how you reference the object in the example (e.g. add an @). This is a bit subjective, but as Mike Lewis pointed out, I think it makes the spec easier to read. I like the organization of defining all my dependent objects with let and keeping my it block nice and short.
相关链接可以在这里找到: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
重要的是要记住,let是惰性求值的,不要在其中放置副作用方法,否则您将无法轻松地从let更改到(:each)之前。 你可以用let!而不是let,以便在每个场景之前对其进行评估。
一般来说,let()是一种更好的语法,它节省了您在所有地方输入@name符号的时间。但是,买者自负!我发现let()也会引入一些微妙的错误(或者至少让人头疼),因为在你尝试使用它之前,这个变量并不真正存在……提示符号:如果在let()之后添加一个put以查看变量是否正确,则允许规范通过,但如果没有put,则规范失败——您已经发现了这个微妙之处。
我还发现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”和前面的(: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块中创建。我通常通过验证对象是持久化的来做到这一点。
object.persisted?.should eq true
否则,当第一次调用let块时,由于延迟实例化,数据库将实际发生更改。
更新
只是加个注释。小心玩代码高尔夫球,或者在这种情况下,用这个答案的rspec高尔夫球。
在这种情况下,我只需要调用对象响应的某个方法。所以我调用_。persist ?_方法作为对象的真值。我要做的就是实例化对象。你可以称之为空?还是零?了。重点不在于测试本身,而在于通过调用来赋予对象生命。
所以你不能重构
object.persisted?.should eq true
是
object.should be_persisted
由于对象还没有被实例化…它的懒惰。:)
更新2
利用让!即时对象创建的语法,应该可以完全避免这个问题。注意,虽然它会挫败很多懒惰的目的,非撞让。
此外,在某些情况下,你可能实际上想要利用主题语法而不是let,因为它可能会给你额外的选项。
subject(:object) {FactoryGirl.create :object}
请Joseph注意——如果您在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,我不太喜欢让。
1. 惰性计算常常使测试设置令人困惑
当在setup中声明的一些东西实际上不会影响状态,而其他东西则会影响状态时,很难对setup进行推理。
最终,出于无奈,有人才把let改成了let!(没有懒惰的评估也是一样的),以便让他们的规范工作。如果这对他们来说是可行的,一个新的习惯就会诞生:当一个新的规范被添加到一个旧的套件中,但它不起作用时,作者尝试的第一件事就是在随机let调用中添加bangs。
很快,所有的性能优势都消失了。
2. 对于非rspec用户来说,特殊语法是不常见的
我宁愿把Ruby教给我的团队,而不是rspec的技巧。实例变量或方法调用在这个项目和其他项目中都很有用,让语法只在rspec中有用。
3.这些“好处”让我们很容易忽略好的设计变更
Let()适用于不希望反复创建的昂贵依赖项。 它还可以很好地与subject配合使用,允许您停止重复调用多参数方法
重复多次的昂贵依赖项和具有大签名的方法都是我们可以改进代码的地方:
也许我可以引入一种新的抽象,将依赖项与我的其余代码隔离开来(这将意味着更少的测试需要它)。 可能被测试的代码做的太多了 也许我需要注入更智能的对象,而不是一长串原语 也许我违反了“告诉-不要问”的规定 也许昂贵的代码可以做得更快(很少-注意这里的过早优化)
在所有这些情况下,我可以使用rspec魔法来缓解困难测试的症状,也可以尝试解决原因。我觉得过去几年我在前者上花费了太多时间,现在我想要一些更好的代码。
回答最初的问题:我宁愿不使用,但我仍然使用let。我主要使用它来适应团队其他成员的风格(似乎世界上大多数Rails程序员现在都深入到他们的rspec魔法中,所以这是经常发生的)。有时,当我在一些我无法控制的代码中添加测试,或者没有时间重构到更好的抽象时,我就会使用它:即当唯一的选择是止痛药时。