Search code examples
ruby-on-railsrspecspree

Error in custom uniqueness validation for rspec


I am trying to create a rspec test for custom validation in a spree extension(like a gem) I need to validate uniqueness of a variants option values for a product (all Spree models) Here is the basic structure of models(Although they are part of spree, a rails based e-commerce building):

class Product
  has_many :variants
  has_many :option_values, through: :variants #defined in the spree extension, not in actual spree core
  has_many :product_option_types
  has_many :option_types, through: :product_option_types
end


class Variant
  belongs_to :product, touch: true
  has_many :option_values_variants
  has_many :option_values, through: option_values
end

class OptionType
  has_many :option_values
  has_many :product_option_types
  has_many :products, through: :product_option_types
end

class OptionValue
  belongs_to :option_type
  has_many :option_value_variants
  has_many :variants, through: :option_value_variants
end

So I have created a custom validation to check the uniqueness of a variants option values for a certain product. That is a product(lets say product1) can have many variants. And a variant with option values lets say (Red(Option_type: Color) and Circle(Option_type: Shape)) have to unique for this product

Anyway this is the custom validator

 validate :uniqueness_of_option_values

 def uniqueness_of_option_values
 #The problem is in product.variants, When I use it the product.variants collection is returning be empty. And I don't get why.
   product.variants.each do |v|
   #This part inside the each block doesn't matter though for here.
     variant_option_values = v.option_values.ids
     this_option_values = option_values.collect(&:id)
     matches_with_another_variant = (variant_option_values.length == this_option_values.length) && (variant_option_values - this_option_values).empty?
     if  !option_values.empty? &&  !(persisted? && v.id == id) && matches_with_another_variant
       errors.add(:base, :already_created)
     end
   end
 end

And finally here are the specs

require 'spec_helper'

describe Spree::Variant do

  let(:product) { FactoryBot.create(:product) }
  let(:variant1) { FactoryBot.create(:variant, product: product) }

  describe "#option_values" do
    context "on create" do
      before do
        @variant2 = FactoryBot.create(:variant, product: product, option_values: variant1.option_values)
      end

      it "should validate that option values are unique for every variant" do
      #This is the main test. This should return false according to my uniqueness validation. But its not since in the custom uniqueness validation method product.variants returns empty and hence its not going inside the each block.
        puts @variant2.valid?


        expect(true).to be true #just so that the test will pass. Not actually what I want to put here
      end
    end
  end
end

Anybody know whats wrong here. Thanks in advance


Solution

  • I have a guess at what's happening. I think a fix would be to change your validation with the following line:

    product.variants.reload.each do |v|
    

    What I think is happing is that when you call variant1 in your test, it is running the validation for variant1, which calls variants on the product object. This queries the database for related variants, and gets an empty result. However, since variant2 has the same actual product object, that product object will not re-query the database, and remembers (incorrectly) that its variants is an empty result.

    Another change which might make your test run is to change your test as follows:

    before do
      @variant2 = FactoryBot.create(:variant, product_id: product.id, option_values: variant1.option_values)
    end
    

    It is subtle and I'd like to know if it works. This sets the product_id field on variant2, but does not set the product object for the association to be the actual same product object that variant1 has. (In practice this is more likely to happen in your actual code, that the product object is not shared between variant objects.)

    Another thing for your correct solution (if all this is right) is to do the reload but put all your save code (and your update code) in a transaction. That way there won't be a race condition of two variants which would conflict, because in a transaction the first must complete the validation and save before the second one does its validation, so it will be sure to detect the other one which just saved.

    Some suggested debugging techniques:

    • If possible, watch the log to see when queries are made. You might have caught that the second validation did not query for variants.
    • Check the object_id. You might have caught that the product objects were in fact the same object.
    • Also check new_record? to make sure that variant1 saved before you tested variant2. I think it does save, but it would have be nice to know you checked that.