Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 16, 2024 03:30 am GMT

FactoryBot: the secret weapon called @overrides

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:

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:

class Restaurant  has_many :dishesendclass Order  belongs_to :restaurantendclass Dish  belongs_to :restaurantendclass OrderedDish  belongs_to :order  belongs_to :dishend

And factories will look like this:

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) }  endend

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:

ordered_dish = create(:ordered_dish)order_restaurant = ordered_dish.order.restaurantdish_restaurant = ordered_dish.dish.restaurantorder_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:

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:

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:

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:

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.

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:

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) }  endend

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

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!


Original Link: https://dev.to/povilasjurcys/factorybot-the-secret-weapon-called-overrides-n31

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To