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
,这显然是错误的(或者更确切地说,是 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)
虽然运算符匹配器使用起来很直观,但为了让它们正常工作,它们需要 RSpec 对其进行特殊处理,因为 Ruby 的优先级规则。此外,should ==
会产生一个 ruby 警告[^foot1],人们偶尔会对 should !=
的工作方式感到惊讶,因为它并不像他们预期的那样工作[^foot2]。
新语法使我们有机会从运算符匹配器的不一致中干净地断开,而不必担心破坏现有的测试套件,因此我们决定在新语法中不支持运算符匹配器。以下列出了每个旧运算符匹配器(与 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 年多前添加的!