How to make FactoryBot return the right STI sub class?

2 min read 06-10-2024
How to make FactoryBot return the right STI sub class?


FactoryBot and STI: Ensuring You Get the Right Subclass

Working with Single Table Inheritance (STI) in Rails can be a powerful way to organize your data. However, when using FactoryBot to create test data, you might encounter a situation where your factory isn't returning the expected subclass. This article dives into the common reasons behind this issue and provides clear solutions to ensure your FactoryBot factories consistently return the correct STI subclass.

The Problem: Unwanted Defaults

Imagine you have a Product model with two subclasses: PhysicalProduct and DigitalProduct. You create a factory for Product but want to ensure specific tests create instances of PhysicalProduct or DigitalProduct. The default behavior of FactoryBot might create a Product object instead of the desired subclass, leading to inaccurate test results.

# models/product.rb
class Product < ApplicationRecord
  has_one :image
  
  # ... 
end

# models/physical_product.rb
class PhysicalProduct < Product
  # ... 
end

# models/digital_product.rb
class DigitalProduct < Product
  # ... 
end

# spec/factories/products.rb
FactoryBot.define do
  factory :product do
    # ...
  end
end

# spec/models/physical_product_spec.rb
describe PhysicalProduct do
  it "creates a physical product" do
    physical_product = FactoryBot.create(:product) # This might create a regular Product instead of PhysicalProduct
    expect(physical_product).to be_a(PhysicalProduct) # This test might fail
  end
end

The Solution: Specifying Subclasses

To address this, you need to explicitly define factories for each subclass. This ensures that when you call create(:physical_product), a PhysicalProduct instance is created, not a generic Product.

# spec/factories/physical_products.rb
FactoryBot.define do
  factory :physical_product, parent: :product do
    # ... specific attributes for PhysicalProduct
  end
end

# spec/factories/digital_products.rb
FactoryBot.define do
  factory :digital_product, parent: :product do
    # ... specific attributes for DigitalProduct
  end
end

Key Points:

  • Parent Factory: The parent: :product line tells FactoryBot to inherit traits and attributes from the product factory. This eliminates redundant code and ensures consistency.
  • Subclass-Specific Attributes: Add attributes specific to the subclass within the factory. This helps maintain data integrity in your tests.

Going Beyond: Customization and Flexibility

FactoryBot provides further control over STI behavior:

  • :class Trait: Use the :class trait to dynamically specify the subclass during factory creation. This is useful for scenarios where you need to create different subclasses based on test conditions.
# spec/models/physical_product_spec.rb
describe PhysicalProduct do
  it "creates a physical product" do
    physical_product = FactoryBot.create(:product, :class => PhysicalProduct) 
    expect(physical_product).to be_a(PhysicalProduct) # This test should now pass
  end
end
  • :type Trait: This trait is similar to :class but allows you to specify the type directly, eliminating the need to reference the class object.
# spec/models/physical_product_spec.rb
describe PhysicalProduct do
  it "creates a physical product" do
    physical_product = FactoryBot.create(:product, :type => 'PhysicalProduct') 
    expect(physical_product).to be_a(PhysicalProduct) 
  end
end

Best Practices for STI and FactoryBot

  • Define Factory for Each Subclass: Create dedicated factories for each STI subclass to avoid ambiguity.
  • Inherit from the Parent Factory: Leverage the parent keyword to reduce code duplication and maintain consistency.
  • Utilize :class or :type Traits: Use these traits for dynamic subclass creation when needed.
  • Keep Test Data Consistent: Ensure your factories accurately represent the expected data for each subclass.

By following these guidelines, you can seamlessly integrate FactoryBot with your STI models, ensuring your tests are robust, accurate, and easily maintainable.