Skip to content

Instantly share code, notes, and snippets.

@elia
Last active March 29, 2021 17:08
Show Gist options
  • Save elia/14367eb7dee7bdfd290c365822227ec6 to your computer and use it in GitHub Desktop.
Save elia/14367eb7dee7bdfd290c365822227ec6 to your computer and use it in GitHub Desktop.
Upgrading FactoryBot on Solidus 2.11+

Updating the factories configuration

Overview

The way Solidus was loading FactoryBot factories up until v2.11 was a hack that worked against Rails reloading and made them substantially incompatible with Spring and other tools, in addition to being at odds with the installation instruction that could be found in the FactoryBot and FactoryBotRails respective READMEs.

What's wrong with the previous setup?

The basic loading mechanism for factories is still loading the factory file with either require or load. Over time FactoryBot moved to collecting the paths were factories are defined and loading them in bulk when FactoryBot.find_definitions is called. Solidus itself, many of its extensions, and most apps ended up in a mix of calling both require and find_definitions.

Usually this was the result of trying to apply FactoryBot's instructions and bumping into some issue with the loading sequence, or weird errors that ended up being resolved only by a complete app restart (or spring stop, etc.).

How a loading factories in the wrong order can break your app?

One of the main issues with Solidus factories used inside your Rails application is that of factories modifying existing ones. In fact if you add an attribute to, say, Spree::User you also probably need to update its factory with code that looks more ore less like this:

spec/factories/users.rb

FactoryBot.modify do
  factory :user do
    my_custom_column { :foo_bar }
  end
end

This kind of factory strictly requires the :user factory defined by solidus in [solidus-core-root]/lib/spree/testing_support/factories/user_factory.rb to be loaded first.

Even worse the situation in which your application needs to modify a factory that is defined in Solidus and then modified by one or more extensions.

The solution (in 2 easy steps)

Give all of that we needed a solution that could give a way forward without wreaking havoc in all existing apps and extensions. For this reason doing nothing will just work, and show some deprecations, but we strongly encourage the upgrade to benefit from app start times (via spring) and reduced need for full app restarts.

Step 1: Remove any previous configuration

Things to note down before starting:

  • Take a note of your current version of FactoryBot / FactoryBotRails, use bundle info factory_bot or bundle info factory_bot_rails
  • Take a note of the factories you depend on, usually Solidus, occasionally extensions, most of the time those come in the form of requires in your spec_helper, e.g. require 'spree/testing_support/factories'
  • Make sure you're not cherry-picking factories from Solidus, i.e. you don't have anything like require 'spree/testing_support/factories/user_factory', check both your spec helper files and your factories

A laundry list of places that could include FactoryBot related code:

  • Remove any dependency on factory_bot or factory_bot_rails from the Gemfile
  • Remove any spec_helper.rb, rails_helper.rb, or spec/support code related to FactoryBot, including requires coming
  • Remove any initialization code in config/application.rb or inside initializers, e.g. config/initializers/factory_bot.rb

Step 2: Add back the updated configuration

1. Add the factory_bot_rails gem

Add gem 'factory_bot_rails', '~> 4.8' to your Gemfile, we advise to use the same version used by Solidus to ensure you don't get errors from differences in how FactoryBot builds/creates associations, but if your app was based on a more recent version you might prefer to stick to that one instead.

Gemfile

group :development, :test do
  gem 'factory_bot_rails', '~> 4.8'
end

2. Configure paths in config/initializers/factory_bot.rb

The initializer should look like this:

config/initializers/factory_bot.rb

Rails.configure do

  # Don't initialize this if factory_bot_rails is not part of the
  # current bundle group.
  if defined?(FactoryBotRails)
    initializer after: "factory_bot.set_factory_paths" do
      require 'spree/testing_support
      
      # The paths for solidus factories.
      solidus_paths = Spree::TestingSupport::FactoryBot.definition_file_paths
      
      # The paths for extensions may be in different places, please refer to each extension
      # and add the factories you require without the ".rb" extension, avoid the directory
      # as it's supported by FactoryBot but can generate ambigous paths when a file with the 
      # same name exists.
      extension_paths = [
        MySolidusExtension::Engine.root.join("lib/solidus_content/factories.rb"),
        # alternative 1: MySolidusExtension::Engine.root.join("lib/solidus_content/factories/product.rb"),
        # alternative 2: MySolidusExtension::Engine.root.join("lib/solidus_content/factories/product_factory.rb"),
        # etc.
      ].map { |path| path.chomp(".rb") }
      
      # The application factories, according to the place in which they're stored.
      app_paths = [
        Rails.root.join('lib', 'factories'),
        Rails.root.join('spec', 'factories'),
      ]
      
      FactoryBot.definition_file_paths = solidus_paths + extension_paths + app_paths
    end
  end
end

3. Setup the RSpec helpers

As advised by the FactoryBot readme:

spec/support/factory_bot.rb

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

🎉 That's all!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment