RSpec 的新期望語法
Myron Marston
2012 年 6 月 15 日長期以來,RSpec 一直採用類似可讀英文的語法來設定期望
foo.should eq(bar)
foo.should_not eq(bar)
RSpec 2.11 將包含此語法的新變體
expect(foo).to eq(bar)
expect(foo).not_to eq(bar)
這個新語法有一些動機,我想透過寫部落格來傳播這個觀念。
委派問題
在 method_missing
、BasicObject
和標準函式庫的 delegate
之間,Ruby 擁有非常豐富的工具來建構委派或代理物件。不幸的是,RSpec 的 should
語法,儘管讀起來很優雅,但在測試委派/代理物件時,很容易產生奇怪且令人困惑的錯誤。
考慮一個簡單的代理物件,它是 BasicObject
的子類別
# fuzzy_proxy.rb
class FuzzyProxy < BasicObject
def initialize(target)
@target = target
end
def fuzzy?
true
end
def method_missing(*args, &block)
@target.__send__(*args, &block)
end
end
很簡單;它定義了一個 #fuzzy?
謂詞,並將所有其他方法呼叫委派給目標物件。
這是一個簡單的規格來測試它的模糊性
# fuzzy_proxy_spec.rb
describe FuzzyProxy do
it 'is fuzzy' do
instance = FuzzyProxy.new(:some_object)
instance.should be_fuzzy
end
end
令人驚訝的是,這會失敗
1) FuzzyProxy is a fuzzy proxy
Failure/Error: instance.should be_fuzzy
NoMethodError:
undefined method `fuzzy?' for :some_object:Symbol
# ./fuzzy_proxy.rb:11:in `method_missing'
# ./fuzzy_proxy_spec.rb:6:in `block (2 levels) in <top (required)>'
Finished in 0.01152 seconds
1 example, 1 failure
問題在於 rspec-expectations 在 Kernel
上定義了 should
,而 BasicObject
不包含 Kernel
…因此 instance.should
會觸發 method_missing
並委派給目標物件。結果實際上是 :some_object.should be_fuzzy
,這顯然是 false(或者更確切地說,是一個 NoMethodError
)。
當在標準函式庫中使用 delegate
時,情況會變得更加混亂。它有選擇地包含 Kernel
的一些方法…這表示如果 rspec-expectations 在 delegate
之前載入,則 should
將在委派物件上正常運作,但如果 delegate
先載入,它將像我們上面的 FuzzyProxy
範例一樣代理 should
呼叫。
根本問題是 RSpec 的 should
語法:為了讓 should
正常運作,它必須在系統中的每個物件上定義… 但 RSpec 並不擁有系統中的每個物件,無法確保它始終一致地運作。正如我們所看到的,它在代理物件上的運作方式不如 RSpec 預期的那樣。請注意,這不僅是 RSpec 的問題;這也是 minitest/spec 的 must_xxx
語法的問題。
我們提出的解決方案是新的 expect
語法
# fuzzy_proxy_spec.rb
describe FuzzyProxy do
it 'is fuzzy' do
instance = FuzzyProxy.new(:some_object)
expect(instance).to be_fuzzy
end
end
這不依賴於系統中所有物件上存在的任何方法,因此完全避免了根本問題。
(幾乎)支援所有匹配器
新的 expect
語法看起來與舊的 should
語法不同,但在底層,它們本質上是相同的。您將匹配器傳遞給 #to
方法,如果它不匹配,則測試會失敗。
支援所有匹配器,但有一個重要的例外:expect
語法不直接支援運算子匹配器。
# rather than:
foo.should == bar
# ...use:
expect(foo).to eq(bar)
雖然運算子匹配器使用起來很直觀,但由於 Ruby 的優先規則,它們需要在 RSpec 中進行特殊處理才能正常運作。此外,should ==
會產生一個 ruby 警告[^foot_1],並且人們偶爾會對 should !=
的運作方式與他們預期的不同而感到驚訝[^foot_2]。
新的語法讓我們有機會徹底擺脫運算子匹配器的不一致性,而不會有破壞現有測試套件的風險,因此我們決定不支援新語法的運算子匹配器。以下是每個舊運算子匹配器(與 should
一起使用)及其 expect
對應項的列表
foo.should == bar
expect(foo).to eq(bar)
"a string".should_not =~ /a regex/
expect("a string").not_to match(/a regex/)
[1, 2, 3].should =~ [2, 1, 3]
expect([1, 2, 3]).to match_array([2, 1, 3])
您可能已經注意到我沒有列出比較匹配器(例如 x.should < 10
)——那是因為它們可以運作,但從未被推薦過。誰會說「x 應該小於 10」?它們的本意始終是與 be
一起使用,這樣讀起來更好,而且也能繼續運作
foo.should be < 10
foo.should be <= 10
foo.should be > 10
foo.should be >= 10
expect(foo).to be < 10
expect(foo).to be <= 10
expect(foo).to be > 10
expect(foo).to be >= 10
統一區塊與數值語法
expect
實際上在 RSpec 中已經存在很長一段時間了[^foot_3],以有限的形式存在,作為區塊期望更具可讀性的替代方案
# rather than:
lambda { do_something }.should raise_error(SomeError)
# ...you can do:
expect { something }.to raise_error(SomeError)
在 RSpec 2.11 之前,expect
不接受任何普通參數,也不能用於數值期望。隨著 2.11 中的更改,對於兩種期望類型都使用相同的語法是很不錯的。
組態選項
預設情況下,should
和 expect
語法都可用。但是,如果您只想使用一種語法,則可以設定 RSpec
# spec_helper.rb
RSpec.configure do |config|
config.expect_with :rspec do |c|
# Disable the `expect` sytax...
c.syntax = :should
# ...or disable the `should` syntax...
c.syntax = :expect
# ...or explicitly enable both
c.syntax = [:should, :expect]
end
end
例如,如果您要啟動一個新專案,並且想要確保為了保持一致性只使用 expect
,則可以完全停用 should
。當其中一種語法被停用時,相應的方法將被取消定義。
未來,我們計劃更改預設值,以便除非您明確啟用 should
,否則只會提供 expect
。我們可能會在 RSpec 3.0 時就執行此操作,但我們希望給使用者充足的時間來熟悉它。
請告訴我們您的想法!
[^foot_1]:正如 Mislav 報告,當警告開啟時,您可能會收到「在空上下文中無用使用 ==」的警告。
[^foot_2]:在 ruby 1.8 上,x.should != y
是 !(x.should == y)
的語法糖,RSpec 無法區分 should ==
和 should !=
。在 1.9 上,我們可以區分它們(因為 !=
現在可以定義為一個獨立的方法),但在 1.9 上支援它,但在 1.8 上不支援它會令人困惑,因此我們 決定只引發錯誤。
[^foot_3]:它最初是 3 年多前加入的!