I have many models which can be authorable (have an author field) and/or tenancyable (have a tenant field). So, I wrote concerns for both of them.
The problem is in tests. I had used the shared_examples_for
block to write tests for the concerns and include them into my model tests. Anyway, to do this, I have several traits and after blocks, for example:
after(:build) do |authorable|
authorable.author = build(:user, tenant: authorable.tenant)
end
trait :no_author do
after(:build) do |authorable|
authorable.author = nil
end
end
trait :no_tenant do
tenant nil
end
This piece of code should be equal in the factories of all the models that are tenancyable and authorable.
I didn't found any way to do this. Is it possible?
FactoryBot's traits are an outstanding way to DRY up your tests and factories by naming groups of attributes, callbacks, and associations in one concise area. Imagine defining factories but without the attributes backed by a specific object.
Rspec has great feature and that is trait. In rspec we create factory for each class which provides the simplest set of attributes necessary to create an instance of that class. Many times we need some attributes which we do not want to add in original factory and at the same time we do not want to repeat it.
Traits can be registered globally, so that they can be used in any other factory without using FactoryGirl's inheritance:
FactoryGirl.define do
trait :no_author do
after(:build) { |authorable| authorable.author = nil }
end
trait :no_tenant do
tenant nil
end
factory :model do
tenant { build(:tenant) }
end
end
You can then simply build your objects like this:
FactoryGirl.build(:model, :no_tenant)
FactoryGirl.build(:model, :no_author)
after
callbacks can also be registered globally, but that would mean they are triggered for any object FactoryGirl creates, which may cause undesired side effects:
FactoryGirl.define do
after(:build) do |authorable|
authorable.author = build(:user, tenant: authorable.tenant)
end
factory :model do
tenant { build(:tenant) }
end
factory :other_model
end
FactoryGirl.build(:model) # Happiness!
FactoryGirl.build(:other_model) # undefined method `tenant'
To avoid this, you can either wrap the callback in a trait, like you did in the :no_author
trait, or you can use factory inheritance:
FactoryGirl.define do
factory :tenancyable do
trait :no_tenant do
tenant nil
end
factory :authorable do
after(:build) do |authorable|
authorable.author = build(:user, tenant: authorable.tenant)
end
trait :no_author do
after(:build) do |authorable|
authorable.author = nil
end
end
end
end
factory :model, parent: :authorable, class: 'Model' do
tenant { build(:tenant) }
end
factory :other_model
end
Note how the class for the model
factory needs to be explicitly specified here to make this work. You can now build objects:
FactoryGirl.build(:model, :no_author) # Happiness!
FactoryGirl.build(:other_model) # More Happiness!
With the second approach, the traits and callbacks are more contained. This may actually cause less unwanted surprises when you have a large codebase with many factories.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With