The Practical Usage of factory_bot Transient Attributes

kinopyo avatar
kinopyo

One interesting feature of factory_bot to me is this Transient Attributes. It's quite powerful - not only DRY your factory setup, but also make it intention-revealing.

How Transient Attributes works

Let's check out the example provided in the GETTING_STARTED official doc. (Breaking it into 2 parts to better demonstrate how it works.)

factory :user do
  transient do
    rockstar { true }
  end

  name { "John Doe#{" - Rockstar" if rockstar}" }
end


create(:user).name
#=> "John Doe - ROCKSTAR"
create(:user, rockstar: false).name
#=> "John Doe"

rockstar is not an attribute on the User model. You can think of it as a virtual attribute that only exists during the test. You can use the passed in value in the factory definition to your need.
Note that you do need to provide a default value for the Transient Attributes, and nil is also a valid value.

The next example is combining it with the callback:

factory :user do
  transient do
    upcased { false }
  end

  name { "John Doe" }

  after(:create) do |user, evaluator|
    user.name.upcase! if evaluator.upcased
  end
end


create(:user, upcased: true).name
#=> "JOHN DOE"

The attributes defined in the transient block can be accessed in the value block or via evaluator parameter.

That's the functionality. Let's look at some practical usage.

Set the attributes of the associated records

Here we assume a User always has one Authenticaiton record, which has a uuid attribute. With Transient Attributes, we can set the authentication.uuid through the user factory directly.

factory :user do
  transient do
    uuid { SecureRandom.uuid }
  end

  before(:create) do |user, evaluator|
    user.authentications.build(uuid: evaluator.uuid)
  end
end


user = create(:user, uuid: "uuid-1234-xxxx")
user.authentications.first.uuid
# => "uuid-1234-xxxx"

user = create(:user)
user.authentications.first.uuid
#=> The one generated by SecureRandom.uuid

Works great with Trait Attributes

It's convenient to use Transient inside the Trait Attributes.

FactoryBot.define do
  factory :user do
    trait :with_post do
      transient do
        post_title "user post title"
      end
      
      after(:create) do |user, evaluator|
        create(:post, user: user, title: evaluator.post_title)
      end
    end
  end
end

user = create(:user, :with_post, post_title: "My Post")
user.posts.first.title
# => "My Post"

That's it. 🙂

References