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
Great simple and to the point explanation! Thank you!