How to correctly check uniqueness and scope with Shoulda
Asked Answered
R

2

6

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?

Rolandorolandson answered 17/3, 2018 at 10:59 Comment(4)
The uniqueness matcher doesn't support using associations as scopes, there are some issues in Github. As a work around you should be using scoped_to(:user_id). But even with this change, scope_to together with case_insensitive doesn't seem to work. I think it is a bug. @Rolandorolandson will you report it? Otherwise I'll do it.Grimona
@ana06 Nice, thank you! I will open a new issue and reference the one you linked.Rolandorolandson
Issue opened here: github.com/thoughtbot/shoulda-matchers/issues/1092Rolandorolandson
This issue seems to be resolved nowCur
R
10

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
Rolandorolandson answered 5/4, 2018 at 9:1 Comment(1)
#1 fixed my issue. Thanks!Amatruda
S
0

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
Suggestible answered 12/6, 2019 at 21:5 Comment(1)
Your second test is not testing the same conditions as the shoulda test. The shoulda test looks at both rpp and RPP as it should if it's case insensitive. Your test doesn't check the second case condition, it just sets it to the same value.Rolandorolandson

© 2022 - 2024 — McMap. All rights reserved.