ruby-on-railsvalidationrspecassociationsfactory-bot

Factory Bot - Is there a way to make an attribute value conditional based on overrides?


I have not found a solution, but found this unanswered question from years ago: factory girl - know when an attribute is overridden in factory

The idea is that I would like the factory to be able to make decisions based on what was overridden when create is called.

Here's the example:

class Contract < ApplicationRecord
  # has start_date and end_date attributes

  has_many :visits
end

class Visit < ApplicationRecord
  # has date attribute

  belongs_to :contract

  validates :date, within_period: true # throws error if date < contract.start_date or date > contract.end_date
end

If the factory for Visit looks like this:

FactoryBot.define do
  factory :visit do
    association :contract, start_date: Date.current, end_date: Date.current
    date { Date.current }
  end
end

The specs run into this:

# Case 1
create(:visit) # creates a valid visit with Date.current in everything, which is ok

# Case 2
create(:visit, date: '01/01/2001') # does not create a visit, because it throws the date error

# Case 3
create(:visit, contract: @contract) # could create a visit or throw the date error, depending on @contract's attributes

If there is a way for the factory to know what is overridden:

In Case 2, it could send dates to the contract factory that actually work with the overridden date.

In Case 3, it could set its own date to be something that passes validation based on the contract object received.

It seems this could be solved with something like create(:visit, contract: @contract, date: @contract.start_date) or maybe the use of traits. I'm wondering, though, if these approaches wouldn't be considered to go against some principle of testing, given that they would make the test have to know and care about this rule, even when the test itself may not be about dates at all.

For now I think I'll solve and move forward with the first approach, so the specs will explicitly build the objects honring the validation rule, but I'm curious what folks out there have seen, tried, or would recommend.


Solution

  • You can use the @overrides instance variable to see what was passed into the create method.

    FactoryBot.define do
      factory :visit do
        # if contract is passed use contract.start_date as date
        # Date.current otherwise
        # if date is given, the block is ignored.
        date { @overrides[:contract]&.start_date || Date.current }
        # If contract is not given, build a contract with dates from the date attribute
        association :contract, start_date: date, end_date: date
      end
    end
    

    If this doesn't work with the blockless association definition, you can change to the association definition with a block.