Transient attributes in Factory Bot

by Jason Swett,

Factory Bot offers a number of features to make test setup more convenient and less repetitive. Among these features is transient attributes.

Transient attributes are values that are passed into the factory but not directly set on the object’s attributes.

In order to illustrate how transient attributes can be useful, let’s look at an example where it’s painful not to use transient attributes.

PDF file attachment example

Let’s say we have an InsuranceDeposit class that has PDF file attachments.

class InsuranceDeposit
  has_many_attached :pdf_files
end

Perhaps we want to write some tests that depend on the content of an insurance deposit’s attached PDF files.

An example without using transient attributes

It would be tedious and repetitive to manually generate a PDF file on the spot every time we had a test case that needed a PDF file. Here’s what such code might look like:

insurance_deposit = create(:insurance_deposit)

# This setup is noisy and hard to understand
file = Tempfile.new
pdf = Prawn::Document.new
pdf.text "my arbitrary PDF content"
pdf.render_file file.path

insurance_deposit.pdf_files.attach(
  io: File.open(file.path),
  filename: "file.pdf"
)

This way is non-ideal because a) the code is hard to understand and b) if we use these steps end more than one place, we’ll have duplication.

We could of course conceivably extract that code into some sort of helper method. This would be an improvement over putting the code directly in our tests repetitively, but the remaining drawback is that then we’d have our test infrastructure code a little bit fragmented and scattered.

It would be nice if we could keep this code in the same place as all our other InsuranceDeposit factory code.

An example with transient attributes

If we take advantage of transient attributes, we can gain both understandability and DRYness. Here’s what that might look like.

FactoryBot.define do
  factory :insurance_deposit do
    transient do
      # pdf_content is an arbitrary transient attribute
      # that I'm defining here
      pdf_content { "" }
    end

    after(:create) do |insurance_deposit, evaluator|
      file = Tempfile.new
      pdf = Prawn::Document.new

      # Because I defined pdf_content as a transient attribute
      # above, I can read evaluator.pdf_content here
      pdf.text evaluator.pdf_content

      pdf.render_file file.path

      insurance_deposit.pdf_files.attach(
        io: File.open(file.path),
        filename: "file.pdf"
      )
    end
  end
end

Now, in our test code, all we have to do is this:

create(:insurance_deposit, pdf_content: "my arbitrary PDF content")

This is much tidier than the original. If we want to see how pdf_content works, we can open up the insurance_deposit factory and have a look, but we aren’t burdened by these details when we just want to understand the high-level steps of the test.

Related articles

How I set up Factory Bot on a fresh Rails project
Nested factories in Factory Bot: what they are and how to use them
When to use Factory Bot’s traits versus nested factories

Leave a Reply

Your email address will not be published.