Create Dependent Associations in FactoryBot

Imagine the following set of models and relationships:

A user can add a time_entry to a job. The time_entry has a task, and that task has a rate which depends upon the job. So I need to validate that the associated time_entry on a job is associated with a rate that is also associated with that job. Basically, I want to make sure the correct rate is being applied to the job.

Models

class TimeEntry < ApplicationRecord
  belongs_to :job
  belongs_to :user
  belongs_to :task

  validates :job_id,
            inclusion: {
              in: :associated_rates_jobs,
            },
            unless:
              Proc.new { |time_entry|
                time_entry.task.nil? || time_entry.job.nil?
              }

  private

  def associated_rates_jobs
    @associated_rates_jobs =
      self
        .task
        .rates
        .where(job: self.job, task: self.task)
        .map { |rate| rate.job_id }
  end
end
class Rate < ApplicationRecord
  belongs_to :task
  belongs_to :job
end
class Task < ApplicationRecord
  has_many :time_entries, dependent: :destroy
  has_many :rates, dependent: :destroy
  has_many :jobs, through: :rates
end
class Job < ApplicationRecord
  has_many :time_entries, dependent: :destroy
  has_many :rates, dependent: :destroy
  has_many :tasks, through: :rates
end

I was able to configure this validation in my TimeEntry model using the custom associated_rates_jobs method. However, this made building valid factories really difficult. I needed my time_entry factory to be associated with a rate that shared the same job.

In order to do this, I used Transient Attributes.

Transient attributes will be ignored within attributes_for and won’t be set on the model, even if the attribute exists or you attempt to override it.

I added a transient attribute to create a rate from my rate Factory. Then, I used the values from that attribute to dynamically assign the values for the job and task attributes.

Factories

Before

FactoryBot.define do
  factory :time_entry do
    job
    task
  end
end

After

FactoryBot.define do
  factory :time_entry do
    transient { rate { create(:rate) } }
    job { rate.job }
    task { rate.task }
  end
end