Back to Repositories

Testing DefinitionProxy Implementation in factory_bot

This test suite validates the DefinitionProxy class functionality in FactoryBot, focusing on attribute handling, associations, callbacks, and factory definitions. It ensures proper behavior of factory definitions and their various configuration options.

Test Coverage Overview

The test suite provides comprehensive coverage of FactoryBot’s DefinitionProxy class functionality.

Key areas tested include:
  • Dynamic attribute declarations
  • Transient attribute handling
  • Association declarations
  • Sequence generation
  • Callback implementations
  • Factory and trait definitions

Implementation Analysis

The testing approach uses RSpec’s behavior-driven development patterns to verify DefinitionProxy functionality. Tests utilize mock objects and expectations to validate method behaviors and ensure proper integration with the Definition class.

Implementation specifics include:
  • Method stubbing and call verification
  • Block handling verification
  • Error case validation
  • Dynamic method delegation testing

Technical Details

Testing tools and setup:
  • RSpec as the testing framework
  • Custom matchers for factory definitions
  • Mock object implementations
  • Block-based callback testing
  • Dynamic attribute generation

Best Practices Demonstrated

The test suite demonstrates several testing best practices:

  • Isolated test cases for each feature
  • Comprehensive error case coverage
  • Clear test descriptions
  • Proper setup and teardown
  • Consistent assertion patterns
  • Focused test scenarios

thoughtbot/factory_bot

spec/factory_bot/definition_proxy_spec.rb

            
describe FactoryBot::DefinitionProxy, "#add_attribute" do
  it "declares a dynamic attribute on the factory when the proxy respects attributes" do
    definition = FactoryBot::Definition.new(:name)
    proxy = FactoryBot::DefinitionProxy.new(definition)
    attribute_value = -> { "dynamic attribute" }
    proxy.add_attribute(:attribute_name, &attribute_value)

    expect(definition).to have_dynamic_declaration(:attribute_name)
      .with_value(attribute_value)
  end

  it "declares a dynamic attribute on the factory when the proxy ignores attributes" do
    definition = FactoryBot::Definition.new(:name)
    proxy = FactoryBot::DefinitionProxy.new(definition, true)
    attribute_value = -> { "dynamic attribute" }
    proxy.add_attribute(:attribute_name, &attribute_value)
    expect(definition).to have_dynamic_declaration(:attribute_name)
      .ignored
      .with_value(attribute_value)
  end
end

describe FactoryBot::DefinitionProxy, "#transient" do
  it "makes all attributes added ignored" do
    definition = FactoryBot::Definition.new(:name)
    proxy = FactoryBot::DefinitionProxy.new(definition)
    attribute_value = -> { "dynamic_attribute" }
    proxy.transient do
      add_attribute(:attribute_name, &attribute_value)
    end

    expect(definition).to have_dynamic_declaration(:attribute_name)
      .ignored
      .with_value(attribute_value)
  end
end

describe FactoryBot::DefinitionProxy, "#method_missing" do
  it "declares an implicit declaration when called without args or a block" do
    definition = FactoryBot::Definition.new(:name)
    proxy = FactoryBot::DefinitionProxy.new(definition)
    proxy.bogus

    expect(definition).to have_implicit_declaration(:bogus).with_factory(definition)
  end

  it "declares an association when called with a ':factory' key" do
    definition = FactoryBot::Definition.new(:name)
    proxy = FactoryBot::DefinitionProxy.new(definition)
    proxy.author factory: :user

    expect(definition).to have_association_declaration(:author)
      .with_options(factory: :user)
  end

  it "declares a dynamic attribute when called with a block" do
    definition = FactoryBot::Definition.new(:name)
    proxy = FactoryBot::DefinitionProxy.new(definition)
    attribute_value = -> { "dynamic attribute" }
    proxy.attribute_name(&attribute_value)

    expect(definition).to have_dynamic_declaration(:attribute_name)
      .with_value(attribute_value)
  end

  it "raises a NoMethodError when called with a static-attribute-like argument" do
    definition = FactoryBot::Definition.new(:broken)
    proxy = FactoryBot::DefinitionProxy.new(definition)
    invalid_call = -> { proxy.static_attributes_are_gone "true" }

    expect(&invalid_call).to raise_error(
      NoMethodError,
      /'static_attributes_are_gone'.*'broken' factory.*Did you mean\? 'static_attributes_are_gone \{ "true" \}'/m
    )
  end
end

describe FactoryBot::DefinitionProxy, "#sequence" do
  def build_proxy(factory_name)
    definition = FactoryBot::Definition.new(factory_name)
    FactoryBot::DefinitionProxy.new(definition)
  end

  it "creates a new sequence starting at 1" do
    allow(FactoryBot::Sequence).to receive(:new).and_call_original
    proxy = build_proxy(:factory)

    proxy.sequence(:sequence)

    expect(FactoryBot::Sequence).to have_received(:new).with(:sequence)
  end

  it "creates a new sequence with an overridden starting value" do
    allow(FactoryBot::Sequence).to receive(:new).and_call_original
    proxy = build_proxy(:factory)
    override = "override"

    proxy.sequence(:sequence, override)

    expect(FactoryBot::Sequence).to have_received(:new)
      .with(:sequence, override)
  end

  it "creates a new sequence with a block" do
    allow(FactoryBot::Sequence).to receive(:new).and_call_original
    sequence_block = proc { |n| "user+#{n}@example.com" }
    proxy = build_proxy(:factory)
    proxy.sequence(:sequence, 1, &sequence_block)

    expect(FactoryBot::Sequence).to have_received(:new)
      .with(:sequence, 1, &sequence_block)
  end
end

describe FactoryBot::DefinitionProxy, "#association" do
  it "declares an association" do
    definition = FactoryBot::Definition.new(:definition_name)
    proxy = FactoryBot::DefinitionProxy.new(definition)

    proxy.association(:association_name)

    expect(definition).to have_association_declaration(:association_name)
  end

  it "declares an association with options" do
    definition = FactoryBot::Definition.new(:definition_name)
    proxy = FactoryBot::DefinitionProxy.new(definition)

    proxy.association(:association_name, name: "Awesome")

    expect(definition).to have_association_declaration(:association_name)
      .with_options(name: "Awesome")
  end

  it "when passing a block raises an error" do
    definition = FactoryBot::Definition.new(:post)
    proxy = FactoryBot::DefinitionProxy.new(definition)

    expect { proxy.association(:author) {} }
      .to raise_error(
        FactoryBot::AssociationDefinitionError,
        "Unexpected block passed to 'author' association in 'post' factory"
      )
  end
end

describe FactoryBot::DefinitionProxy, "adding callbacks" do
  it "adding an :after_build callback succeeds" do
    definition = FactoryBot::Definition.new(:name)
    proxy = FactoryBot::DefinitionProxy.new(definition)
    callback = -> { "my awesome callback!" }

    proxy.after(:build, &callback)

    expect(definition).to have_callback(:after_build).with_block(callback)
  end

  it "adding an :after_create callback succeeds" do
    definition = FactoryBot::Definition.new(:name)
    proxy = FactoryBot::DefinitionProxy.new(definition)
    callback = -> { "my awesome callback!" }

    proxy.after(:create, &callback)

    expect(definition).to have_callback(:after_create).with_block(callback)
  end

  it "adding an :after_stub callback succeeds" do
    definition = FactoryBot::Definition.new(:name)
    proxy = FactoryBot::DefinitionProxy.new(definition)
    callback = -> { "my awesome callback!" }

    proxy.after(:stub, &callback)
    expect(definition).to have_callback(:after_stub).with_block(callback)
  end

  it "adding both an :after_stub and an :after_create callback succeeds" do
    definition = FactoryBot::Definition.new(:name)
    proxy = FactoryBot::DefinitionProxy.new(definition)
    callback = -> { "my awesome callback!" }

    proxy.after(:stub, :create, &callback)

    expect(definition).to have_callback(:after_stub).with_block(callback)
    expect(definition).to have_callback(:after_create).with_block(callback)
  end

  it "adding both a :before_stub and a :before_create callback succeeds" do
    definition = FactoryBot::Definition.new(:name)
    proxy = FactoryBot::DefinitionProxy.new(definition)
    callback = -> { "my awesome callback!" }

    proxy.before(:stub, :create, &callback)

    expect(definition).to have_callback(:before_stub).with_block(callback)
    expect(definition).to have_callback(:before_create).with_block(callback)
  end

  it "adding both an :after_stub and a :before_create callback succeeds" do
    definition = FactoryBot::Definition.new(:name)
    proxy = FactoryBot::DefinitionProxy.new(definition)
    callback = -> { "my awesome callback!" }

    proxy.callback(:after_stub, :before_create, &callback)

    expect(definition).to have_callback(:after_stub).with_block(callback)
    expect(definition).to have_callback(:before_create).with_block(callback)
  end
end

describe FactoryBot::DefinitionProxy, "#to_create" do
  it "accepts a block to run in place of #save!" do
    definition = FactoryBot::Definition.new(:name)
    proxy = FactoryBot::DefinitionProxy.new(definition)
    to_create_block = ->(record) { record.persist }
    proxy.to_create(&to_create_block)

    expect(definition.to_create).to eq to_create_block
  end
end

describe FactoryBot::DefinitionProxy, "#factory" do
  it "without options" do
    definition = FactoryBot::Definition.new(:name)
    proxy = FactoryBot::DefinitionProxy.new(definition)
    proxy.factory(:child)

    expect(proxy.child_factories).to include([:child, {}, nil])
  end

  it "with options" do
    definition = FactoryBot::Definition.new(:name)
    proxy = FactoryBot::DefinitionProxy.new(definition)
    proxy.factory(:child, awesome: true)

    expect(proxy.child_factories).to include([:child, {awesome: true}, nil])
  end

  it "with a block" do
    definition = FactoryBot::Definition.new(:name)
    proxy = FactoryBot::DefinitionProxy.new(definition)
    child_block = -> {}
    proxy.factory(:child, {}, &child_block)

    expect(proxy.child_factories).to include([:child, {}, child_block])
  end
end

describe FactoryBot::DefinitionProxy, "#trait" do
  it "declares a trait" do
    definition = FactoryBot::Definition.new(:name)
    proxy = FactoryBot::DefinitionProxy.new(definition)
    male_trait = proc { gender { "Male" } }
    proxy.trait(:male, &male_trait)

    expect(definition).to have_trait(:male).with_block(male_trait)
  end
end

describe FactoryBot::DefinitionProxy, "#initialize_with" do
  it "defines the constructor on the definition" do
    definition = FactoryBot::Definition.new(:name)
    proxy = FactoryBot::DefinitionProxy.new(definition)
    constructor = proc { [] }
    proxy.initialize_with(&constructor)

    expect(definition.constructor).to eq constructor
  end
end