- Published on
Injecting into transient FactoryBot attributes
- Authors
- Name
- James Brady
- @james_elicit
I was just writing some code which parses a URL in order to extract a piece of the path.
When working correctly, the code under test takes a URL something like this:
http://site.com/.well-known/pki-validation/4b1706977f59ffe3c1ddf282bbee6f45.txt
... and returns just the hash-like part of the path: 4b1706977f59ffe3c1ddf282bbee6f45
.
Creating factories to effectively test it was surprisingly fiddly! Here are the options I considered and what worked for me – in the hope it's of some use to you.
Static value
The simplest approach would be to just use a static value for the URL in the factory:
factory :response do
ssl_http_url "http://site.com/.well-known/pki-validation/4b1706977f59ffe3c1ddf282bbee6f45.txt"
end
Then, in my tests I could do something like:
expect(my_class.parse_response).to eq('4b1706977f59ffe3c1ddf282bbee6f45')
Pros
- super simple
Cons
- URL never changes: we're not exercising the code as much as we might
- we need to look at the factory definition to know what the correct subpath is
Overall, a weak but workable approach.
Dynamic value: sequence
To address these problems (kind-of), we could use a dynamic value with a sequence, like so:
factory :response do
sequence(:ssl_http_url) { |n| "http://site.com/.well-known/pki-validation/#{n}.txt" }
end
This would generate URLs like so:
http://site.com/.well-known/pki-validation/1.txt
http://site.com/.well-known/pki-validation/2.txt
http://site.com/.well-known/pki-validation/3.txt
- etc.
Pros
- None
Cons
- the generated URLs are unrealistic
- no way to reliably know what the subpath should be
Overall, this is an even weaker approach than a static value. For the test code to know what subpath it should expect your code to produce, we have to rely on the incrementing sequence numbers. But what happens if you add a before
block which creates a new response
? All of a sudden all your expectations have off-by-one errors in their expectations.
Dynamic value: random URL
How about the factory randomly generates a URL of the right form?
factory :response do
ssl_http_url "http://#{Faker::Internet.domain_name}/.well-known/pki-validation/#{ Faker::Crypto.md5 }.txt"
end
This would generate URLs like so:
http://gaylordmraz.io/.well-known/pki-validation/e8bfe6eda36585d2d18bb665096c16da.txt
http://boyle.org/.well-known/pki-validation/16fe4249b256a74bc10144542b35ea5e.txt
http://faheylakin.co/.well-known/pki-validation/7434184363b29d07dac7a11698bf86fe.txt
They are realistic and they change, which means your code is better exercised.
However, the problem now is that the test code doesn't know what value to expect. Because the md5 hash which forms the subpath is random, we don't know if the code under test is doing the right thing without reimplementing the parsing logic in the test itself!
Implementing a shadow version of the production code logic just to test the production code is a nightmarish smell.
Dynamic value: injecting a value
What I really wanted was for the URL to be randomly generated, but for the test code to know what subpath value to expect.
FactoryBot has a handy way of achieving this, but unfortunately it's documented in a confusing manner.
Transient or ignored attributes are properties which you can set on your FactoryBot factories, but which don't need to exist on the underlying class. They aren't available for use on the generated objects either – they exist only in the scope of generating the object.
The confusion comes in because they're called ignored attributes in version 4.4.0 but transient attributes in version 4.5.0, which breaks semver….
However, with a factory defined like so:
factory :response do
ssl_http_url { "http://#{ Faker::Internet.domain_name }/.well-known/pki-validation/#{url_subpath}.txt" }
# if you're using FactoryBot < 4.5.0
ignore do
url_subpath { Faker::Crypto.md5 }
end
end
You can pass in a value for url_subpath
and test to your heart's content, e.g.:
subpath = Faker::Crypto.md5
response = FG.create(:response, url_subpath: subpath)
expect(my_class.parse_response).to eq(subpath)
Pros
- URLs are randomised
- generated URLs are realistic
- test code knows what subpath to expect
Cons
- None