Published on

Mastering FactoryBot

Authors

FactoryBot is an indispensable gem in the Ruby on Rails testing ecosystem, streamlining the creation of test data. With its powerful and flexible DSL (Domain Specific Language), you can generate complex objects quickly and efficiently. In this post, we’ll walk through best practices for setting up and using FactoryBot in your test suite, complete with practical examples and useful tips.



Mastering FactoryBot: A Comprehensive Guide

FactoryBot Basics

FactoryBot definitions generate test data efficiently, focusing on the smallest amount of attributes needed for a valid object. Here's a simple example:

ruby
FactoryBot.define do
  factory :bank do
    name
  end
end

Common Methods

FactoryBot provides several convenient methods for object creation, including:

  • create: Persists the object in the test database.
  • build: Creates an instance without persisting it.
  • attributes_for: Returns a hash of the object’s attributes.

You can also use list variants:

ruby
create(:bank)         # Creates and persists a single Bank object
create_list(:bank, 2) # Creates and persists two Bank objects

build(:bank)          # Builds (but does not persist) a single Bank object
build_list(:bank, 2)  # Builds two Bank objects

attributes_for(:bank) # Returns a hash of Bank attributes

Integration with RSpec

Ever wondered how you can use FactoryBot methods directly in your specs? It’s because of this line that you can add to your RSpec configuration:

ruby
RSpec.configure do |c|
  c.include FactoryBot::Syntax::Methods
end

This inclusion allows you to omit FactoryBot. before method calls, simplifying your test setup.

Best Practices and Tips

Here are a couple of conventions and best practices.

Don’t Use Faker

While Faker is a great tool for generating random data, it introduces unnecessary complexity in tests. Test data should be predictable to ensure reliable and fast test runs. The additional memory overhead from Faker’s random generators can also slow down your suite. For test clarity and efficiency, use predefined sequences.

Use Faker for seed data instead.

Sequences: Simplifying Repetitive Data

When you find yourself repeating attribute values across multiple factories, it’s time to use sequences. Sequences generate unique values for attributes in a controlled way:

ruby
# Sequences provide predictable, easily identifiable data
sequence(:first_name, 'a') { |n| "first_name_#{n}" }
sequence(:country_code, aliases: [:nationality]) { 'UK' }

Use aliases for attributes that share the same possible values. This setup keeps your data clean and debugging straightforward.

Advanced Features

FactoryBot has several features beyond basic object creation. Let’s dive into some more complex use cases.

Handling Polymorphic Associations

Polymorphic associations can be tricky, but FactoryBot’s trait method makes them manageable:

ruby
FactoryBot.define do
  factory :document do
    for_contract # Default polymorphic association

    title { 'Sample Document' }

    # Define traits for different polymorphic types
    trait :for_contract do
      association :documentable, factory: :contract
    end

    trait :for_report do
      association :documentable, factory: :report
    end
  end
end

Use traits in your tests to specify which polymorphic association to employ.

In the example above, creating a document using the factory will invoke the factory for Contract as the documentable association by default and invoke the factory for Report when called like this:

ruby
FactoryBot.create(:document, :for_report)

Non-ActiveRecord Objects

FactoryBot isn’t limited to ActiveRecord models. You can also generate hashes or other plain Ruby objects:

ruby
FactoryBot.define do
  factory :api_response, class: Hash do # Class argument
    skip_create

    status { 'success' }
    message { 'Operation completed' }

    initialize_with do
      {
        status: status,
        message: message,
        data: attributes_for(:data_object)
      }
    end
  end
end

Key concepts here are skip_create to prevent persistence and initialize_with to control how the object is initialized. This pattern is especially useful for API payloads or non-database-backed objects.

Using Structs

If you frequently create objects that group related attributes, consider using Struct or argument objects:

ruby
module Queries
  FactoryBot.define do
    factory :pagination_options, class: Hash do
      ...
    end

    factory :filters, class: Hash do
      ...
    end

    factory :search_params, class: Hash do
      skip_create

      filters # Calls above-defined factories
      pagination_options

      initialize_with { attributes } # Reserved variable for passed in arguments
    end
  end
end

This approach helps organize query parameters or similar data structures cleanly.

Conclusion

FactoryBot is a powerful tool for generating test data, and mastering its features can significantly improve the readability and maintainability of your test suite. From using sequences for cleaner data to handling polymorphic associations and creating non-ActiveRecord objects, there’s a lot you can do to optimize your testing workflow.

Remember to refer to the official FactoryBot documentation for more advanced use cases, and check out resources like the Mind-Bending Factories article for creative solutions to complex testing problems.

Happy testing!