How to correctly check uniqueness and scope with Shoulda

3.8k Views Asked by At

I have a User model that has a child association of items. The :name of items should be unique for the user, but it should allow different users to have an item with the same name.

The Item model is currently set up as:

class Item < ApplicationRecord
  belongs_to :user
  validates :name, case_sensitive: false, uniqueness: { scope: :user }
end

And this works to validate intra-user, but still allows other users to save an Item with the same name.

How do I test this with RSpec/Shoulda?

My current test is written as:

describe 'validations' do
    it { should validate_uniqueness_of(:name).case_insensitive.scoped_to(:user) }
  end

But this test fails because:

Failure/Error: it { should validate_uniqueness_of(:name).scoped_to(:user).case_insensitive }

       Item did not properly validate that :name is case-insensitively
       unique within the scope of :user.
         After taking the given Item, setting its :name to ‹"an
         arbitrary value"›, and saving it as the existing record, then making a
         new Item and setting its :name to a different value, ‹"AN
         ARBITRARY VALUE"› and its :user to a different value, ‹nil›, the
         matcher expected the new Item to be invalid, but it was valid
         instead.

This however, is the behavior that I want (other than the weird part that Shoulda picks nil for user). When the user is different, the same name should be valid.

It's possible that I'm not using the scope test correctly or that this is impossible with Shoulda, here is the description of scoped tests. In this case, how would you write a model test to test this behavior?

2

There are 2 best solutions below

1
oneWorkingHeadphone On BEST ANSWER

The solution to doing this is three-fold:

  1. Scope to :user_id instead of :user in the model

  2. Re-write the validations on the model to include all uniqueness requirements as part of a hash

  3. Scope the test to :user_id

The code in the question will work in that it correctly checks for uniqueness case-insensitively, but it is probably best to include all uniqueness requirements as hash anyway since the example in the docs takes this form even for single declarations (also, it's the only way I can find to make Shoulda tests pass with the correct behavior).

This is what the working code looks like:

model

class Item < ApplicationRecord
  belongs_to :user
  validates :name, uniqueness: { scope: :user_id, case_sensitive: false }
end

test

RSpec.describe Item, type: :model do
  describe 'validations' do
    it { should validate_uniqueness_of(:name).scoped_to(:user_id).case_insensitive }
  end
end
1
bhfailor On

I tried this with an enum

model

  validates(:plan_type,
            uniqueness: { scope: :benefit_class_id, case_sensitive: false })

      enum plan_type: {
        rrsp: 0,
        dpsp: 1,
        tfsa: 2,
        nrsp: 3,
        rpp: 4,
      }

test

  it { should validate_uniqueness_of(:plan_type).scoped_to(:benefit_class_id).case_insensitive }

but always got an error of the type (i.e. the enum value was uppercased in the test)

  1) BenefitClass::RetirementPlan validations should validate that :plan_type is case-insensitively unique within the scope of :benefit_class_id
     Failure/Error:
       is_expected.to validate_uniqueness_of(:plan_type)
         .scoped_to(:benefit_class_id).case_insensitive

     ArgumentError:
       'RPP' is not a valid plan_type

But I was able to write an explicit test that passed.

it 'validates uniqueness of plan_type scoped to benefit_class_id' do
  rp1 = FactoryBot.create(:retirement_plan)
  rp2 = FactoryBot.build(
                         :retirement_plan,
                         benefit_class_id: rp1.benefit_class_id,
                         plan_type: rp1.plan_type,
                         )
  expect(rp2).to_not be_valid
end