FactoryBot: the secret weapon called @overrides
Published: 2024-04-16 03:30:54

## Intro

FactoryBot is a great tool that simplifies test setup logic by hiding object build complexity within factory files. Thus,  instead of repeating the same model logic again and again, you simply write something like this:

```ruby
create(:restaurant, :fancy)
create(:restaurant_order, :in_fancy_restaurant)
create(:dish_order, :in_fancy_restaurant, :steak)
```

Properly written factories look good and are more descriptive than a bunch of code lines that just build valid models. The factory will do all the dirty work for you and hide the model build login inside the factory file. The downside: in many cases, you are also the person who needs to create and maintain that good-looking-from-outside factory file.

As with every tool, there are some side effects that you should be aware of if you do not want to ruin your day thinking about weird things happening to you and what's the meaning of life. 

In this post, we are going to focus on one of such challenges. We will talk about complex many-to-many, cross-dependent factories and how to properly create them. 

So, how hard it can be to create a factory with multiple dependencies? Easy!.. If you know how to use factory bot internals in your favor. Read on and I will show you how!

## Problem with many-to-many dependencies

Everything is fun and games until you introduce many-to-many associations. Imagine an Uber Eats-like app where you can choose your lunch from various restaurants and place orders. Each restaurant has its dishes and separate orders - you can't make a single order with dishes from different restaurants. It is a quite common limitation. A simplified relationship between models will look like this:

```ruby
class Restaurant
  has_many :dishes
end

class Order
  belongs_to :restaurant
end

class Dish
  belongs_to :restaurant
end

class OrderedDish
  belongs_to :order
  belongs_to :dish
end
```

And factories will look like this:

```ruby
FactoryBot.define do
  factory :restaurant do
    name { 'TastyPizza' }
  end

  factory :order do
    restaurant { association(:restaurant) }
  end

  factory :dish do
    restaurant { association(:restaurant) }
  end

  factory :ordered_dish do
    order { association(:order) }    
    dish { association(:dish) }
  end
end
```

At first glance, it looks plain and simple, but hold your horses! Look closer to the `:ordered_dish` factory. There are hidden associations in it and they work incorrectly. When you create `:ordered_dish` you will also create one Order record, one Dish record, and... wait for it... **two** Restaurant records:

```ruby
ordered_dish = create(:ordered_dish)
order_restaurant = ordered_dish.order.restaurant
dish_restaurant = ordered_dish.dish.restaurant

order_restaurant == dish_restaurant #=> false
```

It's like ordering KFC's chicken wings from McDonalds. It would be nice, but it's not how life works. 

The simplest solution involves modifying our factory to reuse the restaurant instance from one of the associations, as shown below:

```ruby
factory :ordered_dish do
  order { association(:order) }    
  dish { association(:dish, order.restaurant) }
end
```

This is great, but there is a catch: each developer has to know that you are not allowed to pass the custom `dish` attribute otherwise you will end up with the same two-Restaurants-instead-of-one problem:

```ruby
dish = create(:dish)
ordered_dish = create(:ordered_dish, dish: dish)

dish.restaurant == ordered_dish.order.restaurant #=> false
```

This happens because passing the custom `dish` attribute does not trigger the `dish { association(:dish, order.restaurant) }` block, so the `restaurant` instance is not shared. You could make a recursive dependency like this:

```ruby
factory :ordered_dish do
  order { association(:order, restaurant: dish.restaurant) }    
  dish { association(:dish, order.restaurant) }
end
```

In this case, it will work nicely as long as you pass custom `order` or `dish`, but it will give you a "stack level too deep" error if you try to create `ordered_dish` with default associations:
```
create(:ordered_dish) # => error :(
```

It looks like there is no silver bullet in this situation. You have to choose and no choice is perfect. If only we could know in advance which attribute is non-default and passed by the user...

And there is a way how to know this.

## Using @overrides for two-way dependencies

Have you ever wondered how FactoryBot DSL works? I mean, you write the name of an attribute or association and you do not get an `undefined method` error. Well, FactoryBot developers put a lot of thought into that, used the `method_missing` technique, and ensured that their DSL handler which they call Evaluator has almost zero predefined methods. Here is a stripped version of that class:

```ruby
class FactoryBot::Evaluator
  class_attribute :attribute_lists
 
  private_instance_methods.each do |method|
    undef_method(method) unless method.match?(/^__|initialize/)
  end

  def method_missing(method_name, ...)
    if @instance.respond_to?(method_name)
      @instance.send(method_name, ...)
    else
      SyntaxRunner.new.send(method_name, ...)
    end
  end
  # ...
end
```

So there are only a few methods that you can't use in your factories. If you need to access some Evaluator-specific variables, you need to use instance variables. I do not like using instance variables, but given the context - I understand why this is an exception. I'm telling you this because I would like to introduce you to the `@overrides` instance variable that you could use to check which attributes were customized. Its name is self-explaining. It's a Hash that contains all the custom attributes that were passed when you create a factory.

```ruby
create(:oder_dish, order: create(:order))
# @overrides will be { order: #<Oder:0x00007f...>}
```

Now, with the help of `@overrides`, we could check which attribute was customized and assign `restaurant` accordingly. The code won't be as pretty as before, but it will work like a charm:

```ruby
factory :ordered_dish do
  order do 
    restaurant = @overrides[:dish]&.restaurant
    restaurant ||= association(:restaurant)

    association(:order, restaurant: restaurant) }
  end

  dish do
    restaurant = @overrides[:order]&.restaurant
    restaurant ||= association(:restaurant)
    
    association(:dish, restaurant: restaurant) }
  end
end
```

You can make this code a bit cleaner if you are OK with using transient attributes:

```ruby
factory :ordered_dish do
  transient do
    restaurant do 
      @overrides[:order]&.restaurant ||
      @overrides[:dish]&.restaurant ||
      association(:restaurant)
    end
  end

  order { association(:order, restaurant: restaurant) }    
  dish { association(:dish, restaurant: restaurant) }
end
```

The final result is longer, but it does not have any complex logic in it. It simply checks multiple places for restaurant presence. Most importantly, it makes the factory work flawlessly no matter how you use it. And this is the most important part. We did the impossible - we managed to deal with the cross-dependency challenge.

## Summary

In our journey with FactoryBot, we've explored how to tackle complex scenarios like many-to-many associations and cross-dependencies between factories. It all started with a simple idea of creating clean, reusable factory code to streamline test setup.

We learned that creating factories isn't just about writing straightforward code. When dealing with interchained relationships things can get tricky. For instance, we discovered the problem of accidentally creating duplicate records when we shouldn't.

To overcome this challenge, we had to dive deep into FactoryBot's internals. We explored using the `@overrides` instance variable, which helped us identify customized attributes passed during factory creation. By leveraging this knowledge, we were able to craft factories that adapted intelligently to different scenarios.

Remember, even in the world of coding, challenges are opportunities in disguise. By understanding the tools at our disposal and delving into their inner workings, we can conquer any coding puzzle that comes our way. So, keep exploring, keep learning, and keep building!

Until next time, happy coding!