Back to Repositories

Testing AttributeList Management and Association Handling in FactoryBot

This test suite validates the AttributeList class functionality in FactoryBot, focusing on attribute management, associations, and filtering capabilities. The tests ensure proper handling of attribute definitions, associations, and list manipulations while maintaining data integrity.

Test Coverage Overview

The test suite provides comprehensive coverage of AttributeList functionality:

  • Attribute definition and list maintenance
  • Self-referencing association validation
  • Attribute ordering and application
  • Association filtering and management
  • Ignored vs non-ignored attribute handling

Implementation Analysis

The testing approach utilizes RSpec’s behavior-driven development patterns with extensive use of doubles for isolation. The implementation leverages RSpec’s expect syntax and context-specific describe blocks to organize related test cases.

Key testing patterns include:
  • Shared helper modules for common setup
  • Isolated attribute creation methods
  • Error condition validation

Technical Details

Testing infrastructure includes:

  • RSpec as the primary testing framework
  • Double objects for mock attribute creation
  • Custom helper methods for attribute generation
  • FactoryBot’s internal AttributeList implementation

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolated test cases with clear single responsibility
  • Comprehensive error case coverage
  • DRY implementation through shared helper methods
  • Clear test organization and naming conventions

thoughtbot/factory_bot

spec/factory_bot/attribute_list_spec.rb

            
module AttributeList
  def build_attribute_list(*attributes)
    FactoryBot::AttributeList.new.tap do |list|
      attributes.each { |attribute| list.define_attribute(attribute) }
    end
  end
end

describe FactoryBot::AttributeList, "#define_attribute" do
  it "maintains a list of attributes" do
    attribute = double(:attribute, name: :attribute_name)
    another_attribute = double(:attribute, name: :another_attribute_name)
    list = FactoryBot::AttributeList.new

    list.define_attribute(attribute)
    expect(list.to_a).to eq [attribute]

    list.define_attribute(another_attribute)
    expect(list.to_a).to eq [attribute, another_attribute]
  end

  it "returns the attribute" do
    attribute = double(:attribute, name: :attribute_name)
    list = FactoryBot::AttributeList.new

    expect(list.define_attribute(attribute)).to eq attribute
  end

  it "raises if an attribute has already been defined" do
    attribute = double(:attribute, name: :attribute_name)
    list = FactoryBot::AttributeList.new

    expect {
      2.times { list.define_attribute(attribute) }
    }.to raise_error(
      FactoryBot::AttributeDefinitionError,
      "Attribute already defined: attribute_name"
    )
  end
end

describe FactoryBot::AttributeList, "#define_attribute with a named attribute list" do
  it "raises when the attribute is a self-referencing association" do
    association_with_same_name = FactoryBot::Attribute::Association.new(:author, :author, {})
    list = FactoryBot::AttributeList.new(:author)

    expect {
      list.define_attribute(association_with_same_name)
    }.to raise_error(
      FactoryBot::AssociationDefinitionError,
      "Self-referencing association 'author' in 'author'"
    )
  end

  it "does not raise when the attribute is not a self-referencing association" do
    association_with_different_name = FactoryBot::Attribute::Association.new(:author, :post, {})
    list = FactoryBot::AttributeList.new

    expect { list.define_attribute(association_with_different_name) }.to_not raise_error
  end
end

describe FactoryBot::AttributeList, "#apply_attributes" do
  include AttributeList

  it "adds attributes in the order defined" do
    attribute1 = double(:attribute1, name: :attribute1)
    attribute2 = double(:attribute2, name: :attribute2)
    list = FactoryBot::AttributeList.new

    list.define_attribute(attribute1)
    list.apply_attributes(build_attribute_list(attribute2))
    expect(list.to_a).to eq [attribute1, attribute2]
  end
end

describe FactoryBot::AttributeList, "#associations" do
  include AttributeList

  it "returns associations" do
    email_attribute = FactoryBot::Attribute::Dynamic.new(
      :email,
      false,
      ->(u) { "#{u.full_name}@example.com" }
    )
    author_attribute = FactoryBot::Attribute::Association.new(:author, :user, {})
    profile_attribute = FactoryBot::Attribute::Association.new(:profile, :profile, {})
    list = build_attribute_list(email_attribute, author_attribute, profile_attribute)

    expect(list.associations.to_a).to eq [author_attribute, profile_attribute]
  end
end

describe FactoryBot::AttributeList, "filter based on ignored attributes" do
  include AttributeList

  def build_ignored_attribute(name)
    FactoryBot::Attribute::Dynamic.new(name, true, -> { "value" })
  end

  def build_non_ignored_attribute(name)
    FactoryBot::Attribute::Dynamic.new(name, false, -> { "value" })
  end

  it "filters #ignored attributes" do
    list = build_attribute_list(
      build_ignored_attribute(:comments_count),
      build_non_ignored_attribute(:email)
    )

    expect(list.ignored.names).to eq [:comments_count]
  end

  it "filters #non_ignored attributes" do
    list = build_attribute_list(
      build_ignored_attribute(:comments_count),
      build_non_ignored_attribute(:email)
    )

    expect(list.non_ignored.names).to eq [:email]
  end
end

describe FactoryBot::AttributeList, "generating names" do
  include AttributeList

  def build_ignored_attribute(name)
    FactoryBot::Attribute::Dynamic.new(name, true, -> { "value" })
  end

  def build_non_ignored_attribute(name)
    FactoryBot::Attribute::Dynamic.new(name, false, -> { "value" })
  end

  def build_association(name)
    FactoryBot::Attribute::Association.new(name, :user, {})
  end

  it "knows all its #names" do
    list = build_attribute_list(
      build_ignored_attribute(:comments_count),
      build_non_ignored_attribute(:last_name),
      build_association(:avatar)
    )

    expect(list.names).to eq [:comments_count, :last_name, :avatar]
  end

  it "knows all its #names for #ignored attributes" do
    list = build_attribute_list(
      build_ignored_attribute(:posts_count),
      build_non_ignored_attribute(:last_name),
      build_association(:avatar)
    )

    expect(list.ignored.names).to eq [:posts_count]
  end

  it "knows all its #names for #non_ignored attributes" do
    list = build_attribute_list(
      build_ignored_attribute(:posts_count),
      build_non_ignored_attribute(:last_name),
      build_association(:avatar)
    )

    expect(list.non_ignored.names).to eq [:last_name, :avatar]
  end

  it "knows all its #names for #associations" do
    list = build_attribute_list(
      build_ignored_attribute(:posts_count),
      build_non_ignored_attribute(:last_name),
      build_association(:avatar)
    )

    expect(list.associations.names).to eq [:avatar]
  end
end