Back to Repositories

Testing Transient Attributes Implementation in Factory Bot

This test suite validates the implementation and behavior of transient attributes in Factory Bot, a key feature that allows defining temporary attributes used during object creation but not persisted to the final instance. The tests cover various scenarios including default values, sequence generation, and complex object relationships.

Test Coverage Overview

The test suite provides comprehensive coverage of Factory Bot’s transient attributes functionality:

  • Basic transient attribute behavior and accessibility
  • Integration with sequences and dynamic value generation
  • Complex object relationships and attribute assignment
  • Edge cases including name conflicts and default values
  • Verification of attribute presence in factory output

Implementation Analysis

The testing approach employs RSpec’s describe/context/it blocks to organize related test scenarios. The implementation leverages Factory Bot’s DSL for factory definitions, utilizing transient blocks, after callbacks, and dynamic attribute assignment through blocks. The tests demonstrate both simple and complex use cases of transient attributes.

Technical Details

  • RSpec as the testing framework
  • Factory Bot’s define_model helper for test setup
  • Custom class definitions for complex object relationships
  • Factory sequences for generating unique values
  • Transient blocks for temporary attribute definition

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Clear test organization using context blocks
  • Comprehensive coverage of edge cases
  • DRY test setup using before blocks
  • Explicit expectations and assertions
  • Proper isolation of test scenarios

thoughtbot/factory_bot

spec/acceptance/transient_attributes_spec.rb

            
describe "transient attributes" do
  before do
    define_model("User", name: :string, email: :string)

    FactoryBot.define do
      sequence(:name) { |n| "John #{n}" }

      factory :user do
        transient do
          four { 2 + 2 }
          rockstar { true }
          upcased { false }
        end

        name { "#{FactoryBot.generate(:name)}#{" - Rockstar" if rockstar}" }
        email { "#{name.downcase}#{four}@example.com" }

        after(:create) do |user, evaluator|
          user.name.upcase! if evaluator.upcased
        end
      end
    end
  end

  context "returning attributes for a factory" do
    subject { FactoryBot.attributes_for(:user, rockstar: true) }
    it { should_not have_key(:four) }
    it { should_not have_key(:rockstar) }
    it { should_not have_key(:upcased) }
    it { should have_key(:name) }
    it { should have_key(:email) }
  end

  context "with a transient variable assigned" do
    let(:rockstar) { FactoryBot.create(:user, rockstar: true, four: "1234") }
    let(:rockstar_with_name) { FactoryBot.create(:user, name: "Jane Doe", rockstar: true) }
    let(:upcased_rockstar) { FactoryBot.create(:user, rockstar: true, upcased: true) }
    let(:groupie) { FactoryBot.create(:user, rockstar: false) }

    it "generates the correct attributes on a rockstar" do
      expect(rockstar.name).to eq "John 1 - Rockstar"
      expect(rockstar.email).to eq "john 1 - [email protected]"
    end

    it "generates the correct attributes on an upcased rockstar" do
      expect(upcased_rockstar.name).to eq "JOHN 1 - ROCKSTAR"
      expect(upcased_rockstar.email).to eq "john 1 - [email protected]"
    end

    it "generates the correct attributes on a groupie" do
      expect(groupie.name).to eq "John 1"
      expect(groupie.email).to eq "john [email protected]"
    end

    it "generates the correct attributes on a rockstar with a name" do
      expect(rockstar_with_name.name).to eq "Jane Doe"
      expect(rockstar_with_name.email).to eq "jane [email protected]"
    end
  end

  context "without transient variables assigned" do
    let(:rockstar) { FactoryBot.create(:user) }

    it "uses the default value of the attribute" do
      expect(rockstar.name).to eq "John 1 - Rockstar"
    end
  end
end

describe "transient sequences" do
  before do
    define_model("User", name: :string)

    FactoryBot.define do
      factory :user do
        transient do
          sequence(:counter)
        end

        name { "John Doe #{counter}" }
      end
    end
  end

  it "increments sequences correctly" do
    expect(FactoryBot.build(:user).name).to eq "John Doe 1"
    expect(FactoryBot.build(:user).name).to eq "John Doe 2"
  end
end

describe "assigning values from a transient attribute" do
  before do
    define_class("User") do
      attr_accessor :foo_id, :foo_name
    end

    define_class("Foo") do
      attr_accessor :id, :name
      def initialize(id, name)
        @id = id
        @name = name
      end
    end

    FactoryBot.define do
      factory :user do
        transient do
          foo { Foo.new("id-of-foo", "name-of-foo") }
        end

        foo_id { foo.id }
        foo_name { foo.name }
      end
    end
  end

  it "does not ignore an _id attribute that is an alias for a transient attribute" do
    user = FactoryBot.build(:user, foo: Foo.new("passed-in-id-of-foo", "passed-in-name-of-foo"))
    expect(user.foo_id).to eq "passed-in-id-of-foo"
    expect(user.foo_name).to eq "passed-in-name-of-foo"
  end
end