Back to Repositories

Testing ActiveSupport Instrumentation Integration in Factory Bot

This test suite validates ActiveSupport::Instrumentation integration with Factory Bot, focusing on tracking factory compilation and execution metrics. It ensures proper instrumentation of factory creation timing and payload data collection across different factory configurations and strategies.

Test Coverage Overview

The test suite provides comprehensive coverage of ActiveSupport::Instrumentation integration with Factory Bot.

Key areas tested include:
  • Factory execution timing measurements
  • Factory compilation performance tracking
  • Payload data accuracy for different factory strategies
  • Complex factory configurations with traits and associations

Implementation Analysis

The testing approach utilizes RSpec to verify instrumentation behavior through callback monitoring and payload inspection. Implementation patterns include:

  • Subscription-based event tracking
  • Time measurement validation
  • Factory strategy verification
  • Trait composition testing

Technical Details

Testing infrastructure includes:
  • RSpec test framework
  • ActiveSupport::Notifications for instrumentation
  • Factory Bot factory definitions
  • Model definitions with associations
  • Custom callback implementations

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolated test contexts
  • Comprehensive factory configuration testing
  • Proper setup and teardown patterns
  • Clear separation of concerns between test cases
  • Thorough edge case coverage

thoughtbot/factory_bot

spec/acceptance/activesupport_instrumentation_spec.rb

            
unless ActiveSupport::Notifications.respond_to?(:subscribed)
  module SubscribedBehavior
    def subscribed(callback, *args)
      subscriber = subscribe(*args, &callback)
      yield
    ensure
      unsubscribe(subscriber)
    end
  end

  ActiveSupport::Notifications.extend SubscribedBehavior
end

describe "using ActiveSupport::Instrumentation to track run_factory interaction" do
  let(:slow_user_factory) { FactoryBot::Internal.factory_by_name("slow_user") }
  let(:user_factory) { FactoryBot::Internal.factory_by_name("user") }
  before do
    define_model("User", email: :string)
    define_model("Post", user_id: :integer) do
      belongs_to :user
    end

    FactoryBot.define do
      factory :user do
        email { "[email protected]" }

        factory :slow_user do
          after(:build) { Kernel.sleep(0.1) }
        end
      end

      factory :post do
        trait :with_user do
          user
        end
      end
    end
  end

  it "tracks proper time of creating the record" do
    time_to_execute = 0
    callback = ->(_name, start, finish, _id, _payload) { time_to_execute = finish - start }
    ActiveSupport::Notifications.subscribed(callback, "factory_bot.run_factory") do
      FactoryBot.build(:slow_user)
    end

    expect(time_to_execute).to be >= 0.1
  end

  it "builds the correct payload" do
    tracked_invocations = {}

    callback = ->(_name, _start, _finish, _id, payload) do
      factory_name = payload[:name]
      strategy_name = payload[:strategy]
      factory = payload[:factory]
      tracked_invocations[factory_name] ||= {}
      tracked_invocations[factory_name][strategy_name] ||= 0
      tracked_invocations[factory_name][strategy_name] += 1
      tracked_invocations[factory_name][:factory] = factory
    end

    ActiveSupport::Notifications.subscribed(callback, "factory_bot.run_factory") do
      FactoryBot.build_list(:slow_user, 2)
      FactoryBot.build_list(:user, 5)
      FactoryBot.create_list(:user, 2)
      FactoryBot.attributes_for(:slow_user)
      user = FactoryBot.create(:user)
      FactoryBot.create(:post, user: user)
      FactoryBot.create_list(:post, 2, :with_user)
    end

    expect(tracked_invocations[:slow_user][:build]).to eq(2)
    expect(tracked_invocations[:slow_user][:attributes_for]).to eq(1)
    expect(tracked_invocations[:slow_user][:factory]).to eq(slow_user_factory)
    expect(tracked_invocations[:user][:build]).to eq(5)
    expect(tracked_invocations[:user][:factory]).to eq(user_factory)
    expect(tracked_invocations[:user][:create]).to eq(5)
  end
end

describe "using ActiveSupport::Instrumentation to track compile_factory interaction" do
  before do
    define_model("User", name: :string, email: :string)

    FactoryBot.define do
      factory :user do
        sequence(:email) { |n| "user_#{n}@example.com" }

        name { "User" }

        trait :special do
          name { "Special User" }
        end
      end
    end
  end

  it "tracks proper time of compiling the factory" do
    time_to_execute = 0
    callback = ->(_name, start, finish, _id, _payload) { time_to_execute = finish - start }
    ActiveSupport::Notifications.subscribed(callback, "factory_bot.compile_factory") do
      FactoryBot.build(:user)
    end

    expect(time_to_execute).to be > 0
  end

  it "builds the correct payload" do
    tracked_payloads = []
    callback = ->(_name, _start, _finish, _id, payload) { tracked_payloads << payload }

    ActiveSupport::Notifications.subscribed(callback, "factory_bot.compile_factory") do
      FactoryBot.build(:user, :special)
    end

    factory_payload = tracked_payloads.detect { |payload| payload[:name] == :user }
    expect(factory_payload[:class]).to eq(User)
    expect(factory_payload[:attributes].map(&:name)).to eq([:email, :name])
    expect(factory_payload[:traits].map(&:name)).to eq(["special"])

    trait_payload = tracked_payloads.detect { |payload| payload[:name] == "special" }
    expect(trait_payload[:class]).to eq(User)
    expect(trait_payload[:attributes].map(&:name)).to eq([:name])
    expect(trait_payload[:traits].map(&:name)).to eq(["special"])
  end

  context "when factory with base traits" do
    before do
      define_model("Company", name: :string, email: :string)

      FactoryBot.define do
        trait :email do
          email { "#{name}@example.com" }
        end

        factory :company, traits: [:email] do
          name { "Charlie" }
        end
      end
    end

    it "builds the correct payload" do
      tracked_payloads = []
      callback = ->(_name, _start, _finish, _id, payload) { tracked_payloads << payload }

      ActiveSupport::Notifications.subscribed(callback, "factory_bot.compile_factory") do
        FactoryBot.build(:company)
      end

      factory_payload = tracked_payloads.detect { |payload| payload[:name] == :company }
      expect(factory_payload[:class]).to eq(Company)
      expect(factory_payload[:attributes].map(&:name)).to eq([:name])
      expect(factory_payload[:traits].map(&:name)).to eq([])

      trait_payload = tracked_payloads.detect { |payload| payload[:name] == "email" }
      expect(trait_payload[:class]).to eq(Company)
      expect(trait_payload[:attributes].map(&:name)).to eq([:email])
      expect(trait_payload[:traits].map(&:name)).to eq([])
    end
  end

  context "when factory with additional traits" do
    before do
      define_model("Company", name: :string, email: :string)

      FactoryBot.define do
        trait :email do
          email { "#{name}@example.com" }
        end

        factory :company do
          name { "Charlie" }
        end
      end
    end

    it "builds the correct payload" do
      tracked_payloads = []
      callback = ->(_name, _start, _finish, _id, payload) { tracked_payloads << payload }

      ActiveSupport::Notifications.subscribed(callback, "factory_bot.compile_factory") do
        FactoryBot.build(:company, :email)
      end

      factory_payload = tracked_payloads.detect { |payload| payload[:name] == :company }
      expect(factory_payload[:class]).to eq(Company)
      expect(factory_payload[:attributes].map(&:name)).to eq([:name])
      expect(factory_payload[:traits].map(&:name)).to eq([])

      trait_payload = tracked_payloads.detect { |payload| payload[:name] == "email" }
      expect(trait_payload[:class]).to eq(Company)
      expect(trait_payload[:attributes].map(&:name)).to eq([:email])
      expect(trait_payload[:traits].map(&:name)).to eq([])
    end
  end
end