Make FactoryBot::Evaluator#overrides public attribute
ngouy opened this issue · comments
Problem this feature will solve
let's have a model MyModel
with:
- a
data_type
andunit
attributes that must coerce. - Both are an enum
unit
is a optional attribute that must/must not be nil depending on thedata_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
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
. 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