RSpec 3 新功能:可組合的匹配器
Myron Marston
2014年1月14日RSpec 3 的一大新功能在 3.0.0.beta2 中推出:可組合的匹配器。此功能支援更強大、更不容易出錯的期望,並開啟了新的可能性。
範例
在 RSpec 2.x 中,我曾經多次寫出類似這樣的程式碼
# background_worker.rb
class BackgroundWorker
attr_reader :queue
def initialize
@queue = []
end
def enqueue(job_data)
queue << job_data.merge(:enqueued_at => Time.now)
end
end
# background_worker_spec.rb
describe BackgroundWorker do
it 'puts enqueued jobs onto the queue in order' do
worker = BackgroundWorker.new
worker.enqueue(:klass => "Class1", :id => 37)
worker.enqueue(:klass => "Class2", :id => 42)
expect(worker.queue.size).to eq(2)
expect(worker.queue[0]).to include(:klass => "Class1", :id => 37)
expect(worker.queue[1]).to include(:klass => "Class2", :id => 42)
end
end
在 RSpec 3 中,可組合的匹配器允許你將匹配器作為引數(或巢狀於作為引數傳遞的資料結構中)傳遞給其他匹配器,從而簡化像這樣的規格
# background_worker_spec.rb
describe BackgroundWorker do
it 'puts enqueued jobs onto the queue in order' do
worker = BackgroundWorker.new
worker.enqueue(:klass => "Class1", :id => 37)
worker.enqueue(:klass => "Class2", :id => 42)
expect(worker.queue).to match [
a_hash_including(:klass => "Class1", :id => 37),
a_hash_including(:klass => "Class2", :id => 42)
]
end
end
我們確保了在這種情況下,失敗訊息能清楚地顯示,選擇使用所提供匹配器的 description
而非 inspect
輸出。例如,如果我們透過註解掉 queue << ...
這行程式碼來「破壞」此規格所測試的實作,它會產生以下失敗訊息
1) BackgroundWorker puts enqueued jobs onto the queue in order
Failure/Error: expect(worker.queue).to match [
expected [] to match [(a hash including {:klass => "Class1", :id => 37}), (a hash including {:klass => "Class2", :id => 42})]
Diff:
@@ -1,3 +1,2 @@
-[(a hash including {:klass => "Class1", :id => 37}),
- (a hash including {:klass => "Class2", :id => 42})]
+[]
# ./spec/background_worker_spec.rb:19:in `block (2 levels) in <top (required)>'
匹配器別名
您可能已經注意到,上面的範例使用了 a_hash_including
來代替 include
。RSpec 3 為所有內建匹配器提供了類似的別名,這些別名在語法上更易讀,並提供更好的失敗訊息。
例如,比較此期望和失敗訊息
x = "a"
expect { }.to change { x }.from start_with("a")
expected result to have changed from start with "a", but did not change
…與此
x = "a"
expect { }.to change { x }.from a_string_starting_with("a")
expected result to have changed from a string starting with "a",
but did not change
雖然 a_string_starting_with
比 start_with
更冗長,但它產生的失敗訊息實際上更容易理解,因此您不會被奇怪的語法結構絆倒。我們為 RSpec 的所有內建匹配器提供了一個或多個類似的別名。我們嘗試使用一致的措辭(通常是「一個 [物件類型] [動詞]ing」),以便它們容易猜測。您將在下面看到許多範例,並且 RSpec 3 文件將包含完整列表。
還有一個公開的 API,可以輕鬆定義您自己的別名(適用於 RSpec 的內建匹配器或自訂匹配器)。以下是 rspec-expectations 中提供 start_with
的別名 a_string_starting_with
的程式碼片段
RSpec::Matchers.alias_matcher :a_string_starting_with, :start_with
複合匹配器表達式
Eloy Espinaco 貢獻了一個新功能,提供了另一種組合匹配器的方式:複合的 and
和 or
匹配器表達式。例如,不必寫成這樣
expect(alphabet).to start_with("a")
expect(alphabet).to end_with("z")
…您可以將它們組合成一個期望
expect(alphabet).to start_with("a").and end_with("z")
您可以使用 or
來執行相同的操作。雖然較不常見,但這對於表達有效值列表中的一個值很有用(例如,當確切的值不確定時)
expect(stoplight.color).to eq("red").or eq("green").or eq("yellow")
我認為這對於使用 Jim Weirich 的 rspec-given 來表達不變量特別方便。
複合匹配器表達式也可以作為引數傳遞給另一個匹配器
expect(["food", "drink"]).to include(
a_string_starting_with("f").and ending_with("d")
)
注意:在此範例中,ending_with
是 end_with
匹配器的另一個別名。
哪些匹配器支援匹配器引數?
在 RSpec 3 中,我們更新了許多匹配器以支援接收匹配器作為引數,但並非所有都支援。一般而言,我們更新了所有我們認為有意義的匹配器。不支援匹配器的匹配器是那些具有精確匹配語義,不允許匹配器引數的匹配器。例如,eq
匹配器的文件說明是當且僅當 actual == expected
時才會通過。eq
支援接收匹配器引數是沒有意義的[^foot_1]。
我在下面編譯了一個列表,其中包含所有支援接收匹配器作為引數的內建匹配器。
change
change
匹配器的 by
方法可以接收匹配器
k = 0
expect { k += 1.05 }.to change { k }.by( a_value_within(0.1).of(1.0) )
您還可以將匹配器傳遞給 from
或 to
s = "food"
expect { s = "barn" }.to change { s }.
from( a_string_matching(/foo/) ).
to( a_string_matching(/bar/) )
contain_exactly
contain_exactly
是 match_array
的新別名。語義比 match_array
更清晰(現在 match
也可以匹配陣列,但 match
需要順序匹配,而 match_array
則不需要)。它還允許您將陣列元素作為個別引數傳遞,而不必像 match_array
預期的那樣強制傳遞單個陣列引數。
expect(["barn", 2.45]).to contain_exactly(
a_value_within(0.1).of(2.5),
a_string_starting_with("bar")
)
# ...which is the same as:
expect(["barn", 2.45]).to match_array([
a_value_within(0.1).of(2.5),
a_string_starting_with("bar")
])
include
include
允許您比對集合的元素、雜湊的鍵,或雜湊中鍵/值配對的子集
expect(["barn", 2.45]).to include( a_string_starting_with("bar") )
expect(12 => "twelve", 3 => "three").to include( a_value_between(10, 15) )
expect(:a => "food", :b => "good").to include(
:a => a_string_matching(/foo/)
)
match
除了比對字串與正規表示式或其他字串之外,match
現在還可以對任意陣列/雜湊資料結構進行操作,巢狀深度不受限制。匹配器可以用於該巢狀結構的任何層級
hash = {
:a => {
:b => ["foo", 5],
:c => { :d => 2.05 }
}
}
expect(hash).to match(
:a => {
:b => a_collection_containing_exactly(
an_instance_of(Fixnum),
a_string_starting_with("f")
),
:c => { :d => (a_value < 3) }
}
)
raise_error
raise_error
可以接收一個匹配器來比對異常類別,或者一個匹配器來比對訊息,或者兩者都比對。
RSpec::Matchers.define :an_exception_caused_by do |cause|
match do |exception|
cause === exception.cause
end
end
expect {
begin
"foo".gsub # requires 2 args
rescue ArgumentError
raise "failed to gsub"
end
}.to raise_error( an_exception_caused_by(ArgumentError) )
expect {
raise ArgumentError, "missing :foo arg"
}.to raise_error(ArgumentError, a_string_starting_with("missing"))
startwith 和 endwith
這些都是不言自明的
expect(["barn", "food", 2.45]).to start_with(
a_string_matching("bar"),
a_string_matching("foo")
)
expect(["barn", "food", 2.45]).to end_with(
a_string_matching("foo"),
a_value < 3
)
throw_symbol
您可以將匹配器傳遞給 throw_symbol
來比對隨附的引數
expect {
throw :pi, Math::PI
}.to throw_symbol(:pi, a_value_within(0.01).of(3.14))
yieldwithargs 和 yieldsuccessiveargs
匹配器可以用於指定這些匹配器的 yield 引數
expect { |probe|
"food".tap(&probe)
}.to yield_with_args( a_string_starting_with("f") )
expect { |probe|
[1, 2, 3].each(&probe)
}.to yield_successive_args( a_value < 2, 2, a_value > 2 )
結論
這是我對 RSpec 3 最興奮的新功能之一,我希望您能理解原因。這應該可以更輕鬆地避免編寫脆弱的規格,使您能夠指定 *確切* 的期望(不多也不少)。
[^foot_1]:您當然可以將匹配器傳遞給 eq
,但它會像對待任何其他物件一樣對待它:它會使用 ==
將其與 actual
進行比較,如果結果為 true(也就是說,如果它是同一個物件),期望就會通過。