thoughtbot / factory_bot

A library for setting up Ruby objects as test data.

Home Page:https://thoughtbot.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Make FactoryBot::Evaluator#overrides public attribute

ngouy opened this issue · comments

commented

Problem this feature will solve

let's have a model MyModel with:

  • a data_type and unit attributes that must coerce.
  • Both are an enum
  • unit is a optional attribute that must/must not be nil depending on the data_type

Here is how I deal with that in my factory

FactoryBot.define do
  factory :my_model do

    data_type { MyModel.data_types.keys.sample }
    
    after(:build) do |new_instance|
      case new_instance.data_type
      when "integer", "float"
        new_instance.unit ||= MyModel.units.keys.sample
      when "string"
        new_instance.unit = nil
      end
    end
  end
end

So far so good

But when I want to test that my model has the right behaviour (let's say I want to check if data_type is integer, a nil unit would result to invalid object), I have an issue

Indeed, I have no wau in my after block to know if unit is nil because it has not been defined by the factory "caller", or because it has been defined to nil
In another word, I have no way to know which value is overriden or not
I have no way to differenciate these 2 calls
build(:my_model, data_type: :integer, unit: nil)
build(:my_model, data_type: :integer)

Desired solution

Digging a little bit, I saw the Evaluator has this information, but that is a private information (@overrides variable carries that)
Thanks to this overrides, we know exacly if unit is nil because it has been set like that, or because it has not been given by the factory caller

Would it be possible to make it publicly available

Alternatives considered

A working but ugly alternative is of course

FactoryBot.define do
  factory :my_model do

    data_type { MyModel.data_types.keys.sample }
    
    after(:build) do |new_instance, evaluator|
     # skip if unit is already set by the caller
      next if evaluator.instance_variable_get(:@overrides).key?(:unit)

      case new_instance.data_type
      when "integer", "float"
        new_instance.unit = MyModel.units.keys.sample
      when "string"
        new_instance.unit = nil
      end
    end
  end
end
commented

welp

Hi @ngouy thanks for your proposal, while reading the use case, a couple of things come to mind. This seems to be a use case pretty specific to the way this factory is being written. You could achieve a similar result by changing a bit on how you are defining it. By making use of transient attributes, the default value for unit could stay within the boundaries of the factory and if a user changes it then you can react to it in the after block, similar to:

DEFAULT = "default"
FactoryBot.define do
  factory :my_model do
    transient do
      override_unit { DEFAULT }
    end
    data_type { MyModel.data_types.keys.sample }
    
    after(:build) do |new_instance, evaluator|
     # skip if unit is already set by the caller
     if evaluator.override_unit != DEFAULT
        new_instance.unit = evaluator.override_unit
        next
     end

      case new_instance.data_type
      when "integer", "float"
        new_instance.unit = MyModel.units.keys.sample
      when "string"
        new_instance.unit = nil
      end
    end
  end
end

The overrides attribute is an internal property that is subject to change and it is unstable per each run, so it shouldn't be relied upon. Whenever a public API is added, it needs to be documented and maintained. Therefore is not a small decision to make. If you still think this grants a change in that API, would you provide a couple of more use cases to demonstrate what other scenarios it could power. Please use the reproduction script attached on the repo. Thanks

commented

. Whenever a public API is added, it needs to be documented and maintained. Therefore is not a small decision to make.

💯

Your solution is probably a better option anyway. Haven't thought about it. Thanks a lot for this alternative