Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Share traits across different factories

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?

like image 750
caarlos0 Avatar asked Apr 21 '14 19:04

caarlos0


People also ask

What is trait in factory bot?

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.

What is trait in Rspec?

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.


1 Answers

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.

like image 126
fivedigit Avatar answered Oct 21 '22 08:10

fivedigit