8

今天我在定义自定义 RSpec 匹配器时遇到了一个问题,我解决了这个问题,但实际上看不出为什么其中一种方法有效而另一种方法无效的任何原因,下面是代码:

方法 1 - if + else:

RSpec::Matchers.define :have_success_message do |message|
  match do |page|
    if message.nil?
      page.should have_selector('div.alert.alert-success')
    else
      page.should have_selector('div.alert.alert-success', text: message)
    end
  end
end

方法 2——如果后面跟着除非

RSpec::Matchers.define :have_success_message do |message|
  match do |page|
    page.should have_selector('div.alert.alert-success') if message.nil?
    page.should have_selector('div.alert.alert-success', text: message) unless message.nil?
  end
end

我认为第一种方法更好,因为它只检查一次条件,但是结果应该是一样的,对吧?

好吧,事实证明第一种方法的测试通过了,而第二种方法的测试没有。我完全不知道为什么会这样,如果有人能对此有所了解,我会很高兴。

编辑:

忘记添加实际测试(使用方法 2):

存在以下 HTML 标记:

<div class="alert alert-success">Profile updated</div>

我运行 4 个单独的测试:

it { should have_success_message } # fails
it { should have_success_message('Profile updated') } # passes
it { should have_selector('div.alert.alert-success') } # passes
it { should have_selector('div.alert.alert-success', text: "Profile updated") } # passes

失败带有以下消息:

1) User pages edit with valid information 
 Failure/Error: it { should have_success_message }
   expected #<Capybara::Session> to have success message
 # ./spec/requests/user_pages_spec.rb:80:in `block (5 levels) in <top (required)>'

当 HTML 标记不存在时,所有 4 个测试都失败。

编辑2:

我尝试了另一种方法来验证控制流是否正确:

方法3:

if message.nil?
  puts "In if, message is: #{message.inspect}"
  page.should(have_selector('div.alert.alert-success'))
end
unless message.nil?
  puts "In unless, message is: #{message.inspect}"
  page.should(have_selector('div.alert.alert-success', text: message))
end

使用这种方法,行为与方法 2 相同 - 第一次测试失败,然后通过 3 次。

输出如下:

在 if 中,消息是:nil
在除非,消息是:“配置文件已更新”

所以控制流看起来不错,但是

page.should(have_selector('div.alert.alert-success'))

失败,即使它通过了匹配器之外。这真是一个谜。

最终编辑:

只是为了响应批准的答案-当我像这样切换代码时:

page.should have_selector('div.alert.alert-success', text: message) unless message.nil? 
page.should have_selector('div.alert.alert-success') if message.nil?

测试如下所示:

it { should have_success_message } # passes
it { should have_success_message('Profile updated') } # fails
it { should have_selector('div.alert.alert-success') } # passes
it { should have_selector('div.alert.alert-success', text: "Profile updated") } # passes

所以我认为确实最后一行,当它不正确时,被评估为零,这会导致整个混乱。无论如何,第一种方法更好,但我很高兴我没有想到这个问题:)

4

2 回答 2

6

这对于 RSpec 来说是正确的行为,尽管它看起来很意外。

考虑这段代码:

x = nil
"foo" if x.nil?
"bar" unless x.nil?
#=> 
"foo"
nil

当条件为假时,该...unless语句返回。nil

在您的自定义匹配器中,...unless当您的消息为 nil 时,该语句返回 nil。

这是匹配块中的最后一行,所以匹配块返回 nil。

然后 RSpec 看到您的匹配块返回 nil,RSpec 认为它与 false 相同,因此 RSpec 报告您的自定义匹配器失败。

于 2012-12-01T20:48:46.297 回答
3

哇,那是一个巧妙的谜题!关键是该match方法应该返回一个布尔结果。在第一个选项中,隐式返回值是if分支的结果,即true.

为什么呢true?嗯,should是这样定义的:

      def should(matcher=nil, message=nil, &block)
        ::RSpec::Expectations::PositiveExpectationHandler.handle_matcher(self, matcher, message, &block)
      end

它委托给PositiveExpectationHandler,其handle_matcher方法如下所示:

  def self.handle_matcher(actual, matcher, message=nil, &block)
    check_message(message)
    ::RSpec::Matchers.last_should = :should
    ::RSpec::Matchers.last_matcher = matcher
    return ::RSpec::Matchers::BuiltIn::PositiveOperatorMatcher.new(actual) if matcher.nil?

    match = matcher.matches?(actual, &block)
    return match if match

    message ||= matcher.respond_to?(:failure_message_for_should) ?
                matcher.failure_message_for_should :
                matcher.failure_message

    if matcher.respond_to?(:diffable?) && matcher.diffable?
      ::RSpec::Expectations.fail_with message, matcher.expected, matcher.actual
    else
      ::RSpec::Expectations.fail_with message
    end
  end

我们可以看到,如果匹配器返回true(或实际上是任何真值),则 this 从函数返回并隐式成为 的返回值should。我不确定这种行为是否记录在任何地方。

然而,在第二个选项中,最后一个表达式/语句的值获胜,并且因为unless条件是true,所以表达式计算为nil

1.9.3-p0 :001 > x unless true
 => nil 

所以 RSpec 认为 matcher 试图报告失败是因为你nil意外返回。


要解决此问题,您可能不应该should在匹配器中使用。您可以像这样调用 Capybara 的HaveSelector匹配器:

if message.nil?
  have_selector('div.alert.alert-success').matches? page
else
  have_selector('div.alert.alert-success', text: message).matches? page
end

顺便说一句,指定text: nil最终会流到此处,它将导致空的正则表达式,因此nil无论如何都不需要检查,您可以像这样编写匹配器:

match do |page|
  have_selector('div.alert.alert-success', text: message).matches? page
end

不是很Rubyesque,我承认,但你去吧。

于 2012-12-01T22:05:21.027 回答