2

我有几个 RSpec 示例共享以下复杂的期望,其中数组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)
          )
        )
      )
    )
  )

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

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

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

这很好用,但现在我必须用例如调用该方法

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

在我的例子中。

这在 RSpec 的规范 DSL 中看起来很陌生。我希望能够写作

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

或者

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

反而。

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

我认为我不能为此使用 RSpec 的匹配器别名工具,因为它们似乎只将匹配器 名称映射到其他匹配器 名称,而不是完整的带有参数的匹配器调用。不过,也许 的options论点alias_matcher是为此而生的?

当然,我也可以实现一个自定义匹配器,但是我可能会被迫提供一个返回布尔值的实现,这与它由现有的匹配器组成相矛盾。(并不是说这很难,但我喜欢使用all和之类的东西来实现be_between。)

最后,我还可以对元素的类进行猴子补丁valid_records以具有in_bbox?(min_long, max_long, min_lat, max_lat)属性,以便 RSpec 自动提供相应的be_in_bbox(min_long, max_long, min_lat, max_lat)匹配器。

4

2 回答 2

3

你当然可以这样做。使其成为辅助方法

辅助方法

这些只是普通的 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'.

main我建议不要在 上定义助手,而是将它们放在一个模块中以防止全局泄漏:

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

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

另一种方法是使其成为共享上下文

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

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

复杂匹配器

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

正如您所展示的,将可组合匹配器复合期望相结合,为编写自定义匹配器提供了一种简单有效的替代方案。

备择方案

根据您的建议,也许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
于 2015-05-29T22:39:13.753 回答
1

首先,根据经验,您永远不应该有一个复杂的期望,而应该将您的多合一测试拆分为一个,针对每个嵌套的期望值。

这不仅更接近 rspec 试图做的事情,而且还给你一个更好的提示,让你知道你的期望在哪里被打破了。

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

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

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

我认为,尽管这对您来说可能看起来与直觉相反,但过多地编写规范文件实际上会更清楚地清除单个测试用例实际上应该做什么。

这实际上是编程社区中一个被广泛讨论的嫌疑人,但我真的更喜欢这种方式。与此主题大致相关的内容

于 2015-05-29T22:27:04.163 回答