原文:Let vs. Let! vs. Instance Variables in RSpec
原文作者:Cezar Halmagean
很多人可能有这样的困惑,在 RSpec 中应该使用 let、 let! 还是 @ 开头的实例变量来共享变量(原文为 store state,这里译为共享变量)。
在本文中,我将尝试解释这三者的不同,你可以据此来判断使用哪一个。
当使用 let 的时候
下面是官方文档中 let 的定义(为便于理解就不翻译了 -.-)
Use let to define a memoized helper method. The value will be cached across multiple calls in the same example but not across examples. Note that let is lazy-evaluated: it is not evaluated until the first time the method it defines is invoked.
这段话的意思是,如果你在一个测试用例(it块)中调用了多次,它只会在第一次求值,后续调用返回第一次执行的结果。
describe GetTime do
let(:current_time) { Time.now }
it "gets the same time over and over again" do
puts current_time # => 2018-07-19 09:35:29 +0300
sleep(3)
puts current_time # => 2018-07-19 09:35:29 +0300
end
it "gets the time again" do
puts current_time # => 2018-07-19 09:35:32 +0300
end
end
如你所见,第一个用例中(即第一个it块),虽然两次调用之间延迟了3s,但是返回的值还是一样的。
这是因为第一次 current_time 调用后,它的返回值被缓存了下来,在这个块中再次调用 current_time 时,就会返回已缓存的值。
换句话说,{ Time.now } 只在每个测试用例(it)块中求值一次。
因此,当你在第二个 it 块中再次调用时,{ Time.now } 将会重新求值。然后就跟前面一样,这个值会被缓存以用于后续的调用。
惰性求值意味着 let 块只会在被调用时才开始执行。下面的例子可以证明这一点。
describe "GetTime" do
let(:current_time) { Time.now }
before(:each) do
puts Time.now # => 2018-07-19 09:45:59 +0300
end
it "gets the time" do
sleep(3)
puts current_time # => 2018-07-19 09:46:02 +0300
end
end
当 current_time 被定义时,{ Time.now } 并未立即求值,你可以对比 current_time 得到的值和 before 块中 { Time.now } 得到的值。
我们可能会认为 let 的作用是定义一个变量,但事实上它是先定义一个定义变量的方法,然后在被调用时才初始化该变量。
当使用 let! 的时候
下面是 let! 的官方定义
You can use let! to force the method’s invocation before each example.
let 和 let! 的区别在于: let! 会在一个隐含的 before 块中被调用,所以 let! 在 it 块之前就已经被调用,并且缓存了下来
describe "GetTime" do
let!(:current_time) { Time.now }
before(:each) do
puts Time.now # => 2018-07-19 09:57:52 +0300
end
it "gets the time" do
sleep(3)
puts current_time # => 2018-07-19 09:57:52 +0300
end
end
所以尽管存在时间延迟,但不影响求值的结果,因为这个值已经在 before 块中求出并且缓存了。
当你需要在 it 块前设置一些状态时,这个行为会很有用。
实例变量
实例变量带来的问题是:当实例变量被引用时,就会被自动创建。所以即使你不小心拼写错了实例变量的名称,也不会收到一个报错。拼写错误的实例变量名称将会导致创建一个该名称的实例变量,其值为 nil
这将导致一些微妙并且难以追踪的错误
不过,你可以开启 warnings 设置,这样一来,在代码运行时如果发现实例变量名拼写错误将会抛出一个警告提示。
相比之下,使用 let 时,如果你使用了拼写错误的变量,程序将会抛出 NameError 的异常。
另一件需要注意的事情是,如果你在 before 块中初始化了一个实例变量,那么每个块执行前都会执行这个操作,无论你是否使用这个变量。(这是当然的)
所以如果这个初始化过程非常耗时的话,就会导致你的测试过程变慢
最后我们得出的结论是:当你想要惰性求值时使用 let ,反之使用 let! ,实例变量嘛...好吧,我找不到在你的测试中使用它的理由。