steamingramen
steamingramen

Reputation: 197

How to test uniqueness of an attribute on an association

Given that a User can have_many Addresses, I'm trying to validate that a given user can only have one address for a given address_type. For example, a user can have a primary address and a billing address, but the user cannot have two primary addresses. How do I enforce that rule on my model, and how to I test it? My current best guess is that I need to validate address_type's uniqueness scoped to user_id, but this code is preventing two addresses from existing of the same type. I've seen other people write code very similar to this, but checking on strings instead of on enums.

<!-- language: lang-ruby -->
# user.rb
class User < ApplicationRecord
  has_many :addresses
end

# address.rb
class Address < ApplicationRecord
  belongs_to :user
  enum :address_type => { :primary, :mailing, :billing }
  validates :address_type, :uniqueness => { :scope => :user_id }
end

Upvotes: 0

Views: 712

Answers (1)

max
max

Reputation: 101811

The Rails uniqueness validation works perfectly fine with integer columns. However your enum definition is not valid Ruby syntax.

class Address < ApplicationRecord
  belongs_to :user
  enum :address_type => [ :primary, :mailing, :billing ]
  # or preferably with Ruby 2.0+ hash syntax
   enum address_type: [ :primary, :mailing, :billing ]
  # ...
end

You can test validations by calling .valid? on a model instance and checking the errors object:

require 'rails_helper'

RSpec.describe Address, type: :model do
  let(:user) { create(:user) }
  it "should require the user id to be unique" do
    Address.create(user: user, address_type: :primary)
    duplicate = Address.new(user: user, address_type: :primary)
    expect(duplicate.valid?).to be_falsy
    expect(duplicate.errors.full_messages_for(:address_type)).to include "Address type has already been taken"
  end
end

Beware of just testing expect(duplicate.valid?).to be_falsy and expect(duplicate.valid?) as it can lead to false positives/negatives. Instead test for the specific error message or key. Shoulda-matchers is pretty nice for this purpose but not strictly necessary.

require 'rails_helper'

RSpec.describe Address, type: :model do
  # shoulda-matchers takes care of the boilerplate
  it { should validate_uniqueness_of(:address_type).scoped_to(:user_id) }
end

You should also consider adding a compound unique index on addresses.address_type and addresses.user_id as this will prevent race issues.

Upvotes: 2

Related Questions