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 的内置匹配器提供了一个或多个类似的别名。我们尝试使用一致的措辞(通常是“a [对象类型] [动词]ing”),这样它们很容易猜测。您将在下面看到很多示例,RSpec 3 文档将提供完整列表。
还有一个公共 API,它使定义您自己的别名变得非常简单(无论是针对 RSpec 的内置匹配器还是自定义匹配器)。以下是 rspec-expectations 中提供 a_string_starting_with
的 start_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(即,如果它们是同一个对象),则预期将通过。