Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make FactoryBot return the right STI sub class?

I'm making a big change in my system, so I changed one of my main tables into a STI, and create subclasses to implement the specific behavior.

class MainProcess < ApplicationRecord
end

class ProcessA < MainProcess
end

class ProcessB < MainProcess
end

In the application code, if I run MainProcess.new(type: 'ProcessA') it will return a ProcessA as I want. But in the Rspec tests when I run FactoryBot::create(:main_process, type: 'ProcessA') it is returning a MainProcess and breaking my tests.

My factor is something like this

FactoryBot.define do
  factory :main_process do
    foo { 'bar' }
  end

  factory :process_a, parent: :main_process, class: 'ProcessA' do
  end

  factory :process_b, parent: :main_process, class: 'ProcessB' do
  end
end

Is there some way to make FactoryBot have the same behavior of normal program?

like image 211
Alan Alves de Oliveira Avatar asked Aug 15 '19 03:08

Alan Alves de Oliveira


2 Answers

I found the solution

FactoryBot.define do
  factory :main_process do
    initialize_with do
      klass = type.constantize
      klass.new(attributes)  
    end
  end
  ...
end

The answer was founded here http://indigolain.hatenablog.com/entry/defining-factory-for-sti-defined-model (in japanese)

Edit #1:

⚠⚠⚠ Important ⚠⚠⚠

As mentioned here initialize_with is part of a private FactoryBot API.

According to the documentation:

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

So avoid to use if you can. (although I didn't find any other way to achieve this result without use it)

Edit #2

Besides the warning in the gem documentation (described above), the GETTING_STARTED.md actually suggest you use it

If you want to use factory_bot to construct an object where some attributes are passed to initialize or if you want to do something other than simply calling new on your build class, you can override the default behavior by defining initialize_with on your factory

like image 165
Alan Alves de Oliveira Avatar answered Nov 01 '22 11:11

Alan Alves de Oliveira


If you just modify your original code to specify the class as the class type instead of a string, it works:

FactoryBot.define do
  factory :main_process do
    foo { 'bar' }
  end

  factory :process_a, parent: :main_process, class: ProcessA do
  end

  factory :process_b, parent: :main_process, class: ProcessB do
  end
end

Here's the relevant section of the FactoryBot documentation.

like image 42
Jared Avatar answered Nov 01 '22 11:11

Jared