Search code examples
rspecshoulda

How to correctly check uniqueness and scope with Shoulda


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?


Solution

  • 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