简体   繁体   中英

How to test composite Rails model scopes?

Consider the following model:

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

now to properly test each scope, I would at least provide an exaple that matches and one that doesn''t for each possible variable. Something like this:

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

But then it feels like I'm retesting .a and .b on the .a_or_b test, and if I compose it again, with yet another scope, it'll get bigger and bigger.

What is the sane way of dealing with this?

Also: is this a unit or integration test?

That's a tough one. I'd say you have to find an Aristotelian mean between full coverage, and your specs readability. It's probably not reasonable to test every combination of possible states that influence how your scopes are behaving.

Also it's not really a unit test, because it's coupled with the DB layer.

My approach is this:

Don't test simple scopes (like a and b in your example), trust that AR is well tested, make yous scope names clear and leave it.

In more complex scopes, you can invert the way you compose tests: Create specimens once, then define expectations for different scopes (and make the specimens names very clear, like you're doing already).

This will reduce the size of the spec a bit, and it's easier to read such specs. But it still will grow the spec size if you want to have full coverage for every scope.

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

To make this^ even more readable, I'd suggest creating a custom matcher , you could use like this:

  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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM