如何测试复合 Rails 模型范围?

How to test composite Rails model scopes?

考虑以下模型:

class Model < ActiveRecord::Base
  scope :a,       -> () { where(a: 1) }
  scope :b,       -> () { where(b: 1) }
  scope :a_or_b,  -> () { a.or(b) }
end

现在要正确测试每个范围,我至少会提供一个例子 匹配和不匹配每个可能的变量。像这样:

RSpec.describe Model do
  describe "scopes" do
    describe ".a" do
      subject { Model.a }
      let!(:with_a_0) { create(:model, a: 0) }
      let!(:with_a_1) { create(:model, a: 1) }

      it { should_not include(with_a_0) }
      it { should     include(with_a_1) }
    end

    describe ".b" do
      subject { Model.b }
      let!(:with_b_0) { create(:model, b: 0) }
      let!(:with_b_1) { create(:model, b: 1) }

      it { should_not include(with_b_0) }
      it { should     include(with_b_1) }
    end

    describe ".a_or_b" do
      subject { Model.a_or_b }
      let!(:with_a_0_and_b_0) { create(:model, a: 0, b: 0) }
      let!(:with_a_0_and_b_1) { create(:model, a: 0, b: 1) }
      let!(:with_a_1_and_b_0) { create(:model, a: 1, b: 0) }
      let!(:with_a_1_and_b_1) { create(:model, a: 1, b: 1) }

      it { should_not include(with_a_0_and_b_0) }
      it { should     include(with_a_0_and_b_1) }
      it { should     include(with_a_1_and_b_0) }
      it { should     include(with_a_1_and_b_1) }
    end
  end
end

但感觉就像我在 .a_or_b 测试中重新测试 .a.b,并且 如果我再次组合它,用另一个范围,它会变得越来越大。

处理这个问题的明智方法是什么?

另外:这是单元测试还是集成测试?

这是一个艰难的过程。我会说你必须在全面覆盖和规范可读性之间找到亚里士多德式的平均数。测试影响示波器行为方式的所有可能状态的组合可能是不合理的。

而且它不是真正的单元测试,因为它与DB层耦合。

我的做法是这样的:

不要测试简单的范围(例如您示例中的 ab),相信 AR 已经过良好测试,让您的范围名称清晰并保留它。

在更复杂的范围内,您可以颠倒编写测试的方式: 创建样本一次,然后定义不同范围的期望(并使样本名称非常清晰,就像您已经在做的那样)。

这会稍微减小规范的大小,并且更容易阅读此类规范。但如果您想全面覆盖每个范围,它仍然会增加规格大小。

RSpec.describe Model do
  describe "scopes" do
    let!(:with_a_0_and_b_0) { create(:model, a: 0, b: 0) }
    let!(:with_a_0_and_b_1) { create(:model, a: 0, b: 1) }
    let!(:with_a_1_and_b_0) { create(:model, a: 1, b: 0) }
    let!(:with_a_1_and_b_1) { create(:model, a: 1, b: 1) }

    it { expect(described_class.a).to include(with_a_1_and_b_1).and include(with_a_1_and_b_1) }
    it { expect(described_class.a).not_to include(with_a_0_and_b_0) }

    # you get the idea

为了使这个 ^ 更具可读性,我建议创建一个 custom matcher,你可以这样使用:

  it do 
    expect(described_class.a)
      .to retrieve(with_a_1_and_b_0, with_a_1_and_b_1)
      .and not_retrieve(with_a_0_and_b_0, with_a_0_and_b_1)
  end