我可以为嵌套的RSpec匹配器添加别名吗?

时间:2015-05-29 22:00:31

标签: ruby rspec rspec3 rspec-expectations

我有几个RSpec示例共享以下复杂期望,数组records和浮点数min_longmax_longmin_lat,{{1}在这些例子之间变化。

max_lat

(期望检查相应测试产生的所有记录是否都具有形状(在我的情况下为RGeo Polygon)完全包含在特定于测试的边界框中。)

为了减少重复并通过在其上添加名称来使复杂期望的意图更清晰,我将其提取为方法:

  expect(records).to all have_attributes(
    shape: have_attributes(
      exterior_ring: have_attributes(
        points: all(
          have_attributes(
            longitude: be_between(min_long, max_long),
            latitude: be_between(min_lat, max_lat)
          )
        )
      )
    )
  )

这很好用,但现在我必须用例如

来调用该方法
def expect_in_bbox(records, min_long, max_long, min_lat, max_lat)
  expect(records).to all have_attributes(
    shape: have_attributes(
      exterior_ring: have_attributes(
        points: all(
          have_attributes(
            longitude: be_between(min_long, max_long),
            latitude: be_between(min_lat, max_lat)
          )
        )
      )
    )
  )
end

在我的例子中。

这在RSpec的规范DSL中看起来很陌生。 我更愿意写

expect_in_bbox(valid_records, 12.55744, 12.80270, 51.36250, 51.63187)

expect(valid_records).to be_in_bbox(12.55744, 12.80270, 51.36250, 51.63187)

代替。

是否有推荐的方法来实现这一目标?

我认为我不能使用RSpec的匹配器别名设施,因为它们似乎只将matcher 名称映射到其他匹配器名称,而不是完整的匹配器调用参数。虽然,alias_matcherexpect(valid_records).to all be_in_bbox(12.55744, 12.80270, 51.36250, 51.63187) 参数可能就是为了那个?

当然,我也可以实现一个自定义匹配器,但是我可能会被迫提供一个返回一个布尔值的实现,这个布尔值与已经存在的匹配器相矛盾。 (并不是说它很难,但我喜欢使用像optionsall这样的实现。)

最后,我还可以将be_between元素的类修补为具有valid_records属性,以便RSpec自动提供相应的in_bbox?(min_long, max_long, min_lat, max_lat)匹配器。

2 个答案:

答案 0 :(得分:3)

当然可以这样做。将其设为helper method

助手方法

这些只是普通的Ruby方法。您可以在任何示例组中定义它们。这些辅助方法暴露于定义它们的组中的示例,并且嵌套在该组中,但不是父组或兄弟组。

def be_in_bbox(min_long, max_long, min_lat, max_lat)
  all(
    have_attributes(
      shape: have_attributes(
        exterior_ring: have_attributes(
          points: all(
            have_attributes(
              longitude: be_between(min_long, max_long),
              latitude: be_between(min_lat, max_lat)
            )
          )
        )
      )
    )
  )
end

我建议将该方法放在spec/support中存储的有用命名文件中。可能是诸如spec/support/rgeo_matchers.rb之类的东西。如上所述,这将在main上定义帮助程序,将其混合到Kernel中,这使得它可用于Ruby中的每个对象。您需要确保所有必需的规范文件中都需要此辅助文件:require 'support/rgeo_matchers'

我建议使用placing them in a module来防止全局泄漏,而不是在main上定义帮助程序:

module MyProject
  module RGeo
    module Matchers
      def be_in_bbox(...)
        # ...
      end
    end
  end
end

由于匹配器位于模块中,因此您需要在include MyProject::RGeo::Matchers块中添加RSpec.describe

另一种方法是将其设为shared context

RSpec.shared_context "RGeo matchers" do
  def be_in_bbox(...)
    # ...
  end
end

使用共享上下文,您需要使用include_context代替includeinclude_context "RGeo matchers"

复杂匹配器

虽然您描述的匹配器是嵌套的,但如果它适合您的域模型并描述了一个连贯的“单元”,那么我的书中可以接受。 “测试一件事”并不一定意味着只测试一个属性。它意味着测试“连贯的概念”或“单位”。这意味着什么取决于域模型。

正如您所演示的那样,将composable matcherscompound expectations结合起来,为撰写custom matcher提供了一种简单而有效的替代方案。

替代

根据您的建议,也许从帮助程序中删除all,以便匹配器仅描述“在边界框中”:

def be_in_bbox(min_long, max_long, min_lat, max_lat)
  have_attributes(
    # ...
  )
end

这使得匹配器更易于重复使用。因为它确实描述了“一件事”(例如“在一个边界框内”)。这允许您将其用作独立匹配器或与其他匹配器组合:

it "returns matching bounding boxes" do
  expect(valid_records).to all be_in_bbox(12.55744, 12.80270, 51.36250, 51.63187)
end

it "is in bounding box defined by [(12.55744..12.80270), (51.36250..51.63187)]" do
  expect(generated_box).to be_in_bbox(12.55744, 12.80270, 51.36250, 51.63187)
end

答案 1 :(得分:1)

首先,根据经验,您应该永远不会有复杂的期望,而应该将每个嵌套预期值的一体化测试分成一个。

这不仅更接近于rspec尝试做的事情,而且还能让您更好地了解您的期望值。

但是,您可以将自己的期望添加到匹配器模块:

RSpec::Matchers.define :be_in_bbox do |expected|
 match do |actual|
   # pseudo code:
   actual.has_attributes( 
     {:shape => has_attributes(
       ...
      )}
   )
 end
end

我知道这只是一个暗示要做什么,你必须自己弄清楚实际的代码...但我不认为这是你想要的方法;)

我认为,虽然这对您来说可能看起来很直观,但是更多地编写规范文件实际上会清除单个测试用例实际上应该更清楚地做些什么。

这实际上是编程社区中一个经过深思熟虑的嫌疑人,但我真的更喜欢这种方式。 Something roughly related to this topic