A few months ago I started working my way through some five years of Giant Robots Smashing Into Other Giant Robots episodes. In episode 3 Josh Clayton, the maintainer of FactoryBot, described one particular feature as involving “metaprogramming all the way down”. The phrase piqued my curiosity, so I cloned the repository and began poking around.

What follows is a tour of some of my favorite parts of FactoryBot, with an emphasis on metaprogramming techniques. I hope the reader will learn a few new tricks, and will get familiar enough with the library to explore more on their own and perhaps contribute.

This post is NOT a guide on how to use FactoryBot. For that I recommend reading this guide and watching this video.

What is self?

FactoryBot offers a fairly straightforward DSL for creating factories. Calling the factory method with a symbol :user creates a factory for the User class.

class User
end

FactoryBot.define do
  factory :user
end

p FactoryBot.build(:user)
#=> #<User:0x007ff9b2595980>

But where does the factory method come from inside that block? Normally when we call a method with a block, that block will retain access to the variables and methods from the scope in which it was defined. self inside the block will be the same as self directly outside the block.

module FactoryBot
  def self.define
    yield
  end
end

p self
#=> main

FactoryBot.define do
  p self
  #=> main
end

But there is no factory method defined on main. Instead, the factory method definition appears in FactoryBot::DSL. So how do we have access to a method defined in a totally different context?

Ruby’s instance_eval allows precisely that. Rather than evaluate the block in its usual context with yield or Proc#call, FactoryBot uses instance_eval to change the block’s evaluation context. self inside the block becomes the instance_eval receiver, in this case an instance of FactoryBot::DSL.

module FactoryBot
  def self.define(&block)
    DSL.new.instance_eval(&block)
  end

  class DSL
    def factory(name)
      # ...
    end
  end
end

p self
#=> main

FactoryBot.define do
  p self
  #=> FactoryBot::DSL
end

instance_eval was not the only reasonable solution here. Passing the FactoryBot::DSL instance as an argument to the block would have worked just fine, and would have been a little faster. But it also would have added unnecessary noise to our factories. I think the trade off makes sense here; we are only likely to call define a handful of times anyway, and instance_eval allows for a cleaner DSL.

module FactoryBot
  def self.define
    yield DSL.new
  end

  class DSL
    def factory(name)
      # ...
    end
  end
end

FactoryBot.define do |dsl|
  dsl.factory :user
end

You can read more about FactoryBot’s use of instance_eval (and instance_exec) here. The important thing to remember when looking through the source code is that self is not always what you think it is.

Undefining Methods

The factory method builds a FactoryBot::Factory, populates its FactoryBot::Definition, and registers the new factory in a hash-like object for later lookup. To populate the factory definition with attributes, associations, etc., we can call the factory method with a block. Can you guess what self is inside this block? Let’s take a look:

FactoryBot.define do
  factory :user do
    p self
    # What is self?
  end
end

FactoryBot.build(:user)

#=> NoMethodError: undefined method `name' for :user:Symbol

Oops! That didn’t work at all. Thanks to instance_eval, self inside the block is an instance of FactoryBot::DefinitionProxy. But you won’t have much luck inspecting an instance of that class. You will have to either take my word for it or go check out the source code. That is because the class uses undef_method to remove all but a few allow-listed methods. It’s not easy to get a good look at an object that doesn’t respond to to_s, inspect, etc.

class DSL
  def factory(name, &blk)
    factory = Factory.new(name)

    proxy = DefinitionProxy.new(factory.definition)
    proxy.instance_eval(&blk) if block_given?

    FactoryBot.register_factory(factory)
  end
end

class DefinitionProxy
  UNPROXIED_METHODS = %w(__send__ __id__ nil? send object_id extend instance_eval initialize block_given? raise caller method)

  (instance_methods + private_instance_methods).each do |method|
    undef_method(method) unless UNPROXIED_METHODS.include?(method.to_s)
  end

  def initialize(definition)
    @definition = definition
  end

  # ...
end

Undefining methods on the proxy prevents those methods from interfering with any attributes on the object being constructed. Want to build a factory for a class like this:

class User
  attr_accessor :to_s, :inspect
end

FactoryBot.define do
  factory :user do
    to_s "why you overwrite me?"
    inspect "this seems like a bad idea"
  end
end

user = FactoryBot.build(:user)

puts user.class
#=> User

puts user
#=> why you overwrite me?

p user
#=> this seems like a bad idea

No Problem! (But please don’t actually write a class like that.) FactoryBot does provide a way to define attributes conflicting with reserved words or methods, but undefining methods minimizes the need for that.

Method Missing

Although FactoryBot::DefinitionProxy begins by undefining methods, it still responds to any method we call on it. username and email are not defined on the class, but calling them does not raise a NoMethodError.

FactoryBot.define do
  factory :user do
    username 'dodecadaniel'
    email 'daniel@example.com'
  end
end

This is because FactoryBot::DefinitionProxy overrides method missing. While Object#method_missing raises a NoMethodError, FactoryBot::DefinitionProxy#method_missing creates a FactoryBot::Declaration and adds it to the factory’s definition.

class DefinitionProxy
  # ...

  def method_missing(name, value)
    declaration = Declaration::Static.new(name, value)
    @definition.declare_attribute(declaration)
  end
end

I am leaving some things out, but this should give you an idea what the purpose of the definition proxy is. It offers an expressive DSL for adding attributes, associations, traits, and so on to the factory’s definition. Without method_missing factories might look like this:

FactoryBot.define do
  factory :user do
    add_attribute 'username', 'Daniel'
    add_attribue 'email', 'daniel@example.com'
  end
end

This is not terrible. It does simplify the FactoryBot code quite a bit; getting rid of method_missing would make undef_method unnecessary. But the calls to add_attribute add noise to our factories. As with instance_eval, method_missing provides for a cleaner, more elegant DSL.

If we ditched the proxy altogether, and avoided both method_missing and instance_eval, we might have factories like this:

 FactoryBot.define do |dsl|
   dsl.factory :user do |definition|
     defintion.declare_attribute(Declaration::Static.new('username', 'Daniel')
     definition.declare_attribute(Declaration::Static.new('email', 'daniel@example.com')
   end
 end

Gross! I just want to build a user; why do I need to know about definitions and declarations? I don’t think FactoryBot would be nearly as popular as it is today if factory definitions were that clumsy.

Strategy Pattern

After defining a factory, we can construct our object using a number of built-in methods:

FactoryBot.attributes_for(:user)
FactoryBot.build(:user)
FactoryBot.build_stubbed(:user)
FactoryBot.create(:user)

These methods are easy enough to define. We need to look up the factory, call its compile method, do a few other things that I left out here, and then call the appropriate strategy method.

module FactoryBot
  def self.attributes_for(name)
    factory = FactoryBot.factory_by_name(name)
    factory.compile
    # ...
    factory.attributes_for
  end

  def self.build(name)
    factory = FactoryBot.factory_by_name(name)
    factory.compile
    # ...
    factory.build
  end

  def self.build_stubbed(name)
    factory = FactoryBot.factory_by_name(name)
    factory.compile
    # ...
    factory.build_stubbed
  end

  def self.create(name)
    factory = FactoryBot.factory_by_name(name)
    factory.compile
    # ...
    factory.create
  end

  class Factory
    def attributes_for
      # ...
    end

    def build
      # ...
    end

    def build_stubbed
      # ...
    end

    def create
      # ...
    end
  end
end

But there are some problems with this code. For one, it is not particularly DRY. This could make it more difficult to make changes that affect all of the strategies. If, for example, we need to add a step after factory.compile (e.g. factory = factory.with_traits(@traits)) we will need to add it to each one of these methods. FactoryBot solves this problem by moving the shared logic into FactoryBot::FactoryRunner.

module FactoryBot
  def self.attributes_for(name)
    FactoryRunner.new(name, :attributes_for).run
  end

  def self.build(name)
    FactoryRunner.new(name, :build).run
  end

  def self.build_stubbed(name)
    FactoryRunner.new(name, :build_stubbed).run
  end

  def self.create(name)
    FactoryRunner.new(name, :create).run
  end

  class FactoryRunner
    def initialize(name, strategy)
      @name = name
      @strategy = strategy
    end

    def run
      factory = FactoryBot.factory_by_name(@name)
      factory.compile
      # ...
      factory.public_send(@strategy)
    end
  end
end

DRYing the code improves matters, but this still requires FactoryBot::Factory to have knowledge of every construction strategy. Adding a new strategy would require adding methods to both FactoryBot and FactoryBot::Factory. That would make it impossible for us to add custom strategies without altering the source code or monkey patching.

Instead, FactoryBot uses the Strategy Pattern. Rather than provide separate methods for each strategy, FactoryBot::Factory offers a single run method that takes the name of a strategy as its first argument. It then looks up the strategy at runtime and calls its result method.

class FactoryRunner
  def run
    factory = FactoryBot.factory_by_name(@name)
    factory.compile
    # ...
    factory.run(@strategy)
  end
end

class Factory
  def run(strategy_name)
    strategy = FactoryBot.strategy_by_name(strategy_name)
    # ...
    strategy.result
  end
end

FactoryBot also provides a method to register strategies at runtime. As long as the strategy responds to the right methods, the factory’s run method is happy. The following DuckStrategy would work just fine. If it walks like a duck…

class DuckStrategy
  def initialize(*args)
  end

  def association(*args)
  end

  def result(*args)
    :duck
  end
end

FactoryBot.register_strategy(:duck, DuckStrategy)

FactoryBot.duck(:user)
#=> :duck

Of course only a quack would write a strategy like this. FactoryBot gets us started with some reasonable default strategies, which I find are suitable for most cases.

Dynamically Defining Methods

Notice that we do not need to define FactoryBot.duck in order to use our custom strategy from the previous example. Calling FactoryBot.register_strategy takes care of defining it for us.

Since it is possible for us to define strategies with any name, FactoryBot needs a way to define methods dynamically at runtime. That is exactly what define_method is for.

module FactoryBot
  def self.register_strategy(strategy_name, strategy_class)
    strategies.register(strategy_name, strategy_class)

    define_method(strategy_name) do |name|
      FactoryRunner.new(name, strategy_name).run
    end
  end
end

The example above doesn’t quite work. define_method defines instance methods, but FactoryBot offers class methods for each strategy. There are a few ways around this (I’ll write about singleton classes at some point), but FactoryBot actually does not define the strategy methods directly on the FactoryBot module. Instead it defines them on the FactoryBot::Syntax::Methods module using module_exec and then extends FactoryBot with that module.

module FactoryBot
  def self.register_strategy(strategy_name, strategy_class)
    strategies.register(strategy_name, strategy_class)

    Syntax::Methods.module_exec do
      define_method(strategy_name) do |name|
        FactoryRunner.new(name, strategy_name).run
      end
    end
  end

  extend Syntax::Methods
end

Having the strategy methods packaged up in a module makes it possible to include just those methods into your test suite. I have always liked writing create instead of FactoryBot.create, even if it does make it less obvious where the method is coming from.

RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

descrbe User do
  let!(:user) { create(:user) }
end

Registering a strategy also defines _pair and _list methods for that strategy. Check out FactoryBot::SyntaxMethodRegistrar for the full implementation.

Building your object

After looking up the appropriate strategy, Factory#run instantiates a FactoryBot::Evaluator. Each factory gets its own evaluator class (more on this in a bit) with methods available for each attribute on that factory. Overriding attributes amounts to overriding methods on the evaluator. If you have ever used FactoryBot callbacks you may have seen an evaluator instance as the second argument to the block.

FactoryBot.define do
  factory :user do
    username "'username' becomes a method on the evaluator"

    after(:build) do |user, evaluator|
      puts evaluator.username
    end
  end
end

FactoryBot.build(:user)
# => 'username' becomes a method on the evaluator

FactoryBot::Evaluator is a lot of fun. It uses nearly every technique discussed so far, including define_method, undef_method, method_missing, and instance_eval. I encourage you to spend some time with this class.

Factory#run passes the evaluator along to a FactoryBot:AttributeAssigner, along with the constant for your class (in my examples that would be the User constant). After initializing your class, the attribute assigner assigns each attribute by calling the relevant evaluator method, then passes the value to the relevant setter method on your class.

class AttributeAssigner
  def initialize(evaluator, build_class)
    @build_class = build_class
    @evaluator = evaluator
    @attribute_list = evaluator.class.attribute_list
  end

  def object
    instance = @build_class.new

    @attribute_list.each do |attribute|
      value = @evaluator.send(attribute)
      instance.public_send("#{attribute}=", value)
    end
  end
end

Once the attribute assigner is built and ready, each strategy behaves in its own way. Since the object is already built at this point, the build strategy only needs to trigger the after_build callback. The build_stubbed strategy, on the other hand, still has to do all the work of setting the id, disabling persistence methods, etc.

Anonymous Subclasses

FactoryBot allows you to create child factories that inherit attributes from their parent factory.

FactoryBot.define do
  factory :user do
    username "dodecadaniel"
    email "daniel@example.com"

    factory :composer do
      username "composerinteralia"
    end
  end
end

composer = FactoryBot.build(:user)
composer.username
#=> "dodecadaniel"
composer.email
#=> "daniel@example.com"

composer = FactoryBot.build(:composer)
composer.username
#=> "composerinteralia"
composer.email
#=> "daniel@example.com"

Can you think of an object-oriented approach that might allow for children to inherit behavior from their parent? This sounds a lot like Inheritance, and that is exactly what FactoryBot uses. I mentioned above that each factory gets its own evaluator class with instance methods for the attributes of that factory. Inheriting attributes is fairly simple; each child factory needs an evaluator class that inherits from the evaluator class of its parent.

But how does FactoryBot know how many evaluator classes to create, and what class names to use? Actually it doesn’t need to know any of that. FactoryBot creates anonymous subclasses dynamically at runtime using Class.new.

class Evaluator
end

child_evaluator_class = Class.new(Evaluator)
p child_evaluator
#=> #<Class:0x007fde3cda8848>
p child_evaluator_class.superclass
#=> Evaluator

grandchild_evaluator_class = Class.new(child_evaluator_class)
p grandchild_evaluator_class
#=> #<Class:0x007fde3cc28c70>
p grandchild_evaluator_class.superclass
#=> #<Class:0x007fde3cda8848>
p grandchild_evaluator_class.superclass.superclass
#=> Evaluator

# etc.

Null Object pattern

To create the evaluator class for a factory, FactoryBot does something like this:

class Factory
  def evaluator_class
    @evaluator_class ||= Class.new(parent.evaluator_class)
  end
end

But this code only works for child factories. In a top-level factory parent will be nil. We can fix this by checking for nil.

class Factory
  def evaluator_class
    @evaluator_class ||= Class.new(parent.nil? ? Evaluator : parent.evaluator_class)
  end
end

But now we will need to check for nil every time we need to use parent. There is a better way. I was planning to write more about this, but Josh Clayton already did.