TIL: ruby factory bot and models association : `after(:build)`

TIL: ruby factory bot and models association : `after(:build)`

dev.to - Jan 25

so I had an issue today , I have a model (we will call it StatusMain) that embeds another model(we will call it HistoricalStatus), an example below

# ruby '3'

class StatusMain
  include Mongoid::Document
  include Mongoid::Timestamps
  include AASM


  field :status, type: String, default: 'initiated'

  embeds_many :historical_statuses

  scope :initiated, -> { where(status: 'initiated') }
  scope :rejected, -> { where(status: 'rejected') }

  aasm column: :status do
    after_all_transitions :create_history

    state :initiated, initial: true
    state :rejected

    event :reject do
      transitions from: :initiated, to: :rejected
    end
  end

  def to_p
    Protos:: StatusInitial.new(
      id: id.as_json['$oid'],
      status: status,
      historical_statuses: historical_statuses.collect(&:to_p),
    )
  end

  private

  def create_history
    historical_statuses.create(
      status_main_id: id,
      status: aasm.to_state,
      created_at: Time.now,
      updated_at: Time.now,
    )
  end
end
Enter fullscreen mode Exit fullscreen mode

and the historical_status model looks something like this

# ruby '3'

class HistoricalStatus
  include Mongoid::Document
  include Mongoid::Timestamps

  field :status_main_id, type: BSON::ObjectId
  field :status, type: String

  validates :status_main, presence: true
  validates :status, presence: true

  embedded_in :status_main

  def to_p
    Protos::HistoricalStatus.new(
      id: id.as_json['$oid'],
      status_main_id: status_main_id.as_json['$oid'],
      status: status,
      created_at: created_at.iso8601,
      updated_at: updated_at.iso8601,
    )
  end
end

Enter fullscreen mode Exit fullscreen mode

so as you can see, the goal is to have the historical_status store every status transitions, and be embedded in my StatusMain

so as I was working on my spec , I designed my factories like this

  • status_main factory
# ruby '3'

FactoryBot.define do
  factory :status_main do
    status { 'initiated' }

    historical_statuses do
      [
        create(:historical_status, status_main: instance),
      ]
    end
  end
end

Enter fullscreen mode Exit fullscreen mode

*historical_status factory

# ruby '3'

FactoryBot.define do
  factory :historical_status do
    status_main

    status_main_id { status_main.id }
    status { 'initiated' }

    trait :initiated do
      status { 'initiated' }
    end

    trait :rejected do
      status { 'rejected' }
    end
  end
end

Enter fullscreen mode Exit fullscreen mode

So as you can see , in my factories, historical_status receives the status_main, that way , in status_main factory, I can create the historical_status with the correct status_main id.

The problem

so the issues I had is that , my status_main_id was always nil in my historical_status , that caused my test to fail because as you can guess .as_json['$oid'], does not exist on nil object.

Tried multiple things and I was surprised that my status_main_id is nil... How can it be nil when I'm passing it and doing it correctly.
Turns out my status_main object that I'm passing to historical_status was nil... now it's interesting, I continued investigating and found out , that my status_main object was set AFTER my historical_status was created, thus status_main_id was nil in historical_status.

The solution

let me first paste the updated factory and then I will explain.

# ruby '3'

FactoryBot.define do
  factory :historical_status do
    status_main

    status { 'initiated' }

    after(:build) do |historical_status, evaluator|
      historical_status.status_main_id = evaluator.status_main.id
    end

    trait :initiated do
      status { 'initiated' }
    end

    trait :rejected do
      status { 'rejected' }
    end
  end
end

Enter fullscreen mode Exit fullscreen mode

so as you can notice , we now have an after(:build)
In the new factory, I'm using the after(:build) callback to set the status_main_id after the historical_status object has been built, and before it's saved, this way you can use the evaluator to get the id of the status_main and set it to the status_main_id field. This is important because the factory is trying to use the id of the status_main object that is associated with the historical_status and if the id is not set yet it will raise an error.

The evaluator is an instance of FactoryBot::Evaluator that is passed to the block, which is used to access the attributes of the factory and the objects that are being built/created by the factory.

By setting the status_main_id in the after(:build) callback, you ensure that the status_main_id field is set before the historical_status object is saved, and the id will be the same as the original status_main id field as wanted.

Conclusion

So here is what I learned today , feel free to comment a better way to achieve that if you have some, or if you can explain in a different way what is happening.

MORE ARTICLES