ruby 我可以在自定义匹配器中使用内置的RSpec匹配器吗?

z8dt9xmd  于 2023-05-06  发布在  Ruby
关注(0)|答案(4)|浏览(148)

我在特性规范中有以下期望(非常低级,但仍然必要):

expect(Addressable::URI.parse(current_url).query_values).to include(
  'some => 'value',
  'some_other' => String
)

注意,第二个查询值是一个模糊匹配,因为我只是想确保它在那里,但我不能更具体。
我想把它提取到一个自定义匹配器中。我开始说:

RSpec::Matchers.define :have_query_params do |expected_params|
  match do |url|
    Addressable::URI.parse(url).query_values == expected_params
  end
end

但这意味着我不能在这里传递{'some_other' => String}。为了继续使用模糊匹配,我必须在自定义匹配器中使用include匹配器。
但是,RSpec::Matchers::BuiltIn中的任何内容都被标记为私有API,而Include具体记录为:

# Provides the implementation for `include`.
# Not intended to be instantiated directly.

所以我的问题是**RSpec是否支持在自定义匹配器中使用内置匹配器?”””我将如何做到这一点?

nwsw7zdq

nwsw7zdq1#

RSpec::Matchers似乎是一个受支持的API(它的rdoc没有另外说明),因此您可以用Ruby编写匹配器,而不是用匹配器DSL(受支持;参见the second paragraph of the custom matcher documentation),并像这样使用RSpec::Matchers#include
spec/support/matchers.rb

module My
  module Matchers
    def have_query_params(expected)
      HasQueryParams.new expected
    end

    class HasQueryParams
      include RSpec::Matchers

      def initialize(expected)
        @expected = expected
      end

      def matches?(url)
        actual = Addressable::URI.parse(url).query_values
        @matcher = include @expected
        @matcher.matches? actual
      end

      def failure_message
        @matcher.failure_message
      end

    end
  end
end

spec/support/matcher_spec.rb

include My::Matchers

describe My::Matchers::HasQueryParams do
  it "matches" do
    expect("http://example.com?a=1&b=2").to have_query_params('a' => '1', 'b' => '2')
  end
end
t30tvxxf

t30tvxxf2#

是的,您可以从自定义匹配器中调用内置的rspec匹配器。换句话说,在编写匹配器时,您可以使用普通的Rspec DSL而不是纯Ruby。查看this gist(不是我的主旨,但它帮助了我!).
我有一个非常复杂的控制器,它有一个选项卡界面,其中定义和选择的选项卡取决于模型示例的状态。我需要在:new、:create、:edit和:update操作的每个状态下测试选项卡设置。所以我写了这些匹配器:

require "rspec/expectations"

RSpec::Matchers.define :define_the_review_tabs do
  match do
    expect(assigns(:roles         )).to be_a_kind_of(Array)
    expect(assigns(:creators      )).to be_a_kind_of(ActiveRecord::Relation)
    expect(assigns(:works         )).to be_a_kind_of(Array)

    expect(assigns(:available_tabs)).to include("post-new-work")
    expect(assigns(:available_tabs)).to include("post-choose-work")
  end

  match_when_negated do
    expect(assigns(:roles         )).to_not be_a_kind_of(Array)
    expect(assigns(:creators      )).to_not be_a_kind_of(ActiveRecord::Relation)
    expect(assigns(:works         )).to_not be_a_kind_of(Array)

    expect(assigns(:available_tabs)).to_not include("post-new-work")
    expect(assigns(:available_tabs)).to_not include("post-choose-work")
  end

  failure_message do
    "expected to set up the review tabs, but did not"
  end

  failure_message_when_negated do
    "expected not to set up review tabs, but they did"
  end
end

RSpec::Matchers.define :define_the_standalone_tab do
  match do
    expect(assigns(:available_tabs)).to include("post-standalone")
  end

  match_when_negated do
    expect(assigns(:available_tabs)).to_not include("post-standalone")
  end

  failure_message do
    "expected to set up the standalone tab, but did not"
  end

  failure_message_when_negated do
    "expected not to set up standalone tab, but they did"
  end
end

RSpec::Matchers.define :define_only_the_review_tabs do
  match do
    expect(assigns).to     define_the_review_tabs
    expect(assigns).to_not define_the_standalone_tab
    expect(assigns(:selected_tab)).to eq(@selected) if @selected
  end

  chain :and_select do |selected|
    @selected = selected
  end

  failure_message do
    if @selected
      "expected to set up only the review tabs and select #{@selected}, but did not"
    else
      "expected to set up only the review tabs, but did not"
    end
  end
end

RSpec::Matchers.define :define_only_the_standalone_tab do
  match do
    expect(assigns).to     define_the_standalone_tab
    expect(assigns).to_not define_the_review_tabs
    expect(assigns(:selected_tab)).to eq("post-standalone")
  end

  failure_message do
    "expected to set up only the standalone tab, but did not"
  end
end

RSpec::Matchers.define :define_all_tabs do
  match do
    expect(assigns).to define_the_review_tabs
    expect(assigns).to define_the_standalone_tab
    expect(assigns(:selected_tab)).to eq(@selected) if @selected
  end

  chain :and_select do |selected|
    @selected = selected
  end

  failure_message do
    if @selected
      "expected to set up all three tabs and select #{@selected}, but did not"
    else
      "expected to set up all three tabs, but did not"
    end
  end
end

我是这样使用它们的:

should define_all_tabs.and_select("post-choose-work")
should define_all_tabs.and_select("post-standalone")
should define_only_the_standalone_tab
should define_only_the_review_tabs.and_select("post-choose-work")
should define_only_the_review_tabs.and_select("post-new-work")

非常棒的是,能够只需要几个重复的期望块,并将它们汇总到一组自定义匹配器中,而不必用纯Ruby编写匹配器。
这为我节省了几十行代码,使我的测试更具表现力,并允许我在填充这些选项卡的逻辑发生变化时在一个地方进行更改。
还要注意,您可以在自定义匹配器中访问assignscontroller等方法/变量,因此不需要显式地传递它们。
最后,我本可以在规范中内联定义这些匹配器,但我选择将它们放在spec/support/matchers/controllers/posts_controller_matchers.rb中

kt06eoxx

kt06eoxx3#

您可以使用匹配器DSL而不是编写自己的Matcher类。这是一个有点简单。

RSpec::Matchers.define :have_query_params do |expected|
  match do |actual|
    # your code
    RSpec::Matchers::BuiltIn::Include.new(expected).matches?(actual)
  end
end
oug3syen

oug3syen4#

最后,我在match块中捕获了RSpec::Expectations::ExpectationNotMetError,这样我就可以设置一个更好的错误消息。所以我做了这样的事情:

RSpec.configure do |config|
  RSpec::Matchers.define :custom_string_eq do |some_string|
    fm = nil
    match do |passed_string|
      expect(passed_string).to eq("#{some_string} EXTRA")
      true
    rescue RSpec::Expectations::ExpectationNotMetError => e
      fm = e.message
      fm += e.backtrace.find { |b| b.include?(__FILE__) }
      false
    end

    failure_message do |yara_file|
      fm || 'Unknown Error'
    end
  end
end

一些测试

RSpec.describe SomeClass do
  it 'should pass custom matcher' do
    expect('test EXTRA').to custom_string_eq('test')
  end
  it 'should not pass custom matcher' do
    expect('test').to custom_string_eq('test')
  end
end

那么在我的测试中我至少得到了一些有用的东西

Failures:

  1) SomeClass should not pass custom matcher
     Failure/Error: expect('test').to custom_string_eq('test')
     
       expected: "test EXTRA"
            got: "test"
     
       (compared using ==)
       .../spec/spec_helper.rb:18:in `block (3 levels) in <top (required)>'
     # ./spec/unit/some_file_spec.rb:56:in `block (2 levels) in <top (required)>'

相关问题