通常我們在寫測試的時候,有遇到重複需要的參數,會把他拉到 let
出來寫,避免每個 example 寫了一堆事前的參數準備。
那麼 let
和 let!
有什麼區別呢?
Instance variables 在測試裡又可以如何運用呢?
這裡先簡單回答
let
只在被呼叫時觸發參數賦值let!
等同於放在 before 內,不需要等到真正呼叫的時候才生成
那麼接下來就是比較透徹釐清的部分了。
關於 let
官方的例子是這樣的
$count = 0
describe "let" do
let(:count) { $count += 1 }
it "memoizes the value" do
count.should == 1
count.should == 1
end
it "is not cached across examples" do
count.should == 2
end
end
所以當 count
在 "memoizes the value"
這個 example 被呼叫兩次時的行為分別如下
- 第一次:執行
{ $count += 1 }
並指向 count - 第二次:直接使用 count 的 cache 結果,而不重複執行
其實這邊很簡單,把他想成如下的概念就行
count ||= $count += 1
但是 cache 的值不會跨 example,所以在第二個 "is not cached across examples"
時,他還是會重新呼叫並賦值。
查看原始碼可以找到
def let(name, &block)
...
if block.arity == 1
define_method(name) { __memoized.fetch_or_store(name) { super(RSpec.current_example, &nil) } }
else
define_method(name) { __memoized.fetch_or_store(name) { super(&nil) } }
end
...
end
簡單來說,我們傳遞 name
和 &block
給 let
,結果會返回一個 define_method
那這樣就可以完全理解 let
做的事情了,等於是幫你在一個 example block 裡面建立一個 method 可以使用,並且呼叫過後會 cache 住,這樣也能解釋為什麼跨 example 不能共用了。
注意: let 有 lazy load 的特性,只有呼叫時才執行,如果你寫了一堆 let 但 example 裡沒使用,那麼就不會被賦值。
關於 let! (驚嘆號)
我覺得網路上有各種解釋,不如直接看原始碼還比較好懂
def let!(name, &block)
let(name, &block)
before { __send__(name) }
end
其實 let!
多一個驚嘆號的差別在於,他會先在 before 就幫你做了,而不會等到你真正呼叫的時候才做。
關於 Instance variables
在某些情況下,拿 instance variables 換掉 let
可以換來執行的速度提升
假設場景會在所有的 example 裡面用到 user,然而這個 user 可以是同一個沒關係
那麼你可以這樣用
before(:all) do
@user = create(:user)
end
如此一來,當每個 example 調用 @user
時,都可以拿到已經賦值好的參數,而不會在每個 example 重複創建。
注意這邊用的是 before(:all)
,如果你沒有加上 :all
那麼預設是 :each
等於每個 example 都會重新賦值,這樣下來還是會慢喔。
所以在這種場景下,用 instance variables 會比 let
快上許多,避免重複創建。
但 instance variables 也不能濫用,我認為他還是有一些問題存在
- 如果調用時拼寫錯誤,那麼他的初始值會是
nil
,可能會在不知情的情況下有難以追蹤的 bug - 沒有 lazy load 的特性,放在 before 裡面就算沒用到也會執行賦值
關於 subject
一樣直接看官方原始碼
def subject(name=nil, &block)
if name
let(name, &block)
alias_method :subject, name
self::NamedSubjectPreventSuper.__send__(:define_method, name) do
raise NotImplementedError, "`super` in named subjects is not supported"
end
else
let(:subject, &block)
end
end
重點在於呼叫 let
後 alias_method :subject, name
所以 subject
本身就是 let
,系出同源,從語言角度來看就是做 delegation
那為什麼要有 subject
存在呢?
其實他跟 should
是一組的
官方原始碼裡面有說明
# When `should` is called with no explicit receiver, the call is
# delegated to the object returned by `subject`. Combined with an
# implicit subject this supports very concise expressions.
subject 可以拿來做隱式調用 (參考 ruby china 上的帖子)
# 不用 subject
describe "Checking Account initialization" do
it "should have balance with $50" do
account = CheckingAccount.new(Money.new(50, :USD))
account.should have_a_balance_of(Money.new(50, :USD)) # should_have_a_balance 是自定义 matcher
end
end
# 使用 subject
describe CheckingAccount, "with $50" do
# 直接用的 Class Name,若此时没有显式定义 subject,那么默认的 subject 就是 CheckingAccount.new,可通过在代码中输出 subject 获知
subject { CheckingAccount.new(Money.new(50, :USD)) }
it { should have_a_balance_of(Money.new(50, :USD)) }
end
所以說如果是測 model,就會知道為什麼可以用 should 這種簡短的寫法了
describe Post do
# 這裡沒有定義 subject 的話,預設就是 Post.new(因為他會直接拿 describe.class)
it { should belong_to(:user) }
it { should validate_presence_of(:title) }
end
這種隱式調用的寫法容易帶來困擾,所以現在 rspec 都希望採用主動式的 expectation
,所以也禁用這種 should 的寫法了(單行敘述不在此限)。
小結
- 重複的參數用
let
, 有 lazy load 特性 - 需要跑在 before each 的 let 要加驚嘆號變成
let!
- 跨 example 重複可以使用的參數可以塞
instance variables
會比用 let 快上許多 - subject 其實和 should 是一組的,但為了更好讀請用
expectation
取代