DEV Community

Cover image for TIL: ruby factory bot and models association : `after(:build)`
Jean Roger Nigoumi Guiala
Jean Roger Nigoumi Guiala

Posted on

3

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

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.

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

Top comments (0)

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

👋 Kindness is contagious

Explore a sea of insights with this enlightening post, highly esteemed within the nurturing DEV Community. Coders of all stripes are invited to participate and contribute to our shared knowledge.

Expressing gratitude with a simple "thank you" can make a big impact. Leave your thanks in the comments!

On DEV, exchanging ideas smooths our way and strengthens our community bonds. Found this useful? A quick note of thanks to the author can mean a lot.

Okay