Creating better FactoryBot Factories
Updated August 27th, 2018: Reflected the much-needed name change for the gem.
Factories always seem like that thing everyone uses, but isn’t spoken about in-depth enough to help people use them easily, let alone to their fullest power. Staying DRY, avoiding over-specificity, using realistically random data, and still being approachable enough to not scare junior developers into just reusing the base versions of factories are just a few of the concerns that go into designing smart factories.
The best factories are not only the most used — but the most remixed. They should evolve as much as your project itself evolves.
TL;DR
- Base factories should use most/all of the attributes on the model.
- Use sub-factories to create natural variations.
- Traits should only be used for tying to multiple factories — think archived/expired.
- Never be afraid to create factories that use other factories.
- Use random values for everything you can.
- Utilize FactoryBot’s hooks.
- Any test-specific attributes should always be defined in the test.
Base Factories
Usually when I take a look at projects that utilize FactoryBot from either senior or junior developers, the biggest problem I find is overused base factories. Often, the factory was generated sometime long ago, and hasn’t been updated since, aside from for fields that require presence. What this inevitably ends up leading to, is a case of forgetting you can be DRY.
There are some core things to keep in mind with base factories:
- They should always represent a typical version of the model, with as many fields as would reasonably be filled out.
- This doesn’t mean to always include fields that normally wouldn’t be filled out — if for example, you have a marketplace, and only sellers give their bank information, your base User model shouldn’t have mock bank information.
- In general, you want as few base factories as possible. This makes them easier to maintain going forward, because if any fields need to change their typical information, you’re changing it in fewer places. Most objects can get away with just one.
- Anytime you add a field to an object, you should be taking another look at your base factory.
Sub-Factories
The rarity with which I see these at all has always made me a little sad. Subfactories are easily my most-used feature of FactoryBot.
factory :user do
username { Faker::Internet.user_name }
email { Faker::Internet.email }
password { Faker::Internet.password(10, 20) }
admin false factory :admin do
email { "#{Faker::Name.first_name }@examplemarket.io" }
admin true
end
end
No need to make new passwords, or utilize traits to create the admin — this is a perfect use-case for creating an alternative version of the same user factory.
The best use of the subfactory system, is to facilitate collections of mutually exclusive kinds of attributes like above, where an admin user would both have admin set to true, and an e-mail at the company domain.
Traits
Traits are best used for mixing and matching. You might have old user accounts that have since been archived, since you didn’t want to lose all of your records related to them. This is a fantastic place to utilize a trait.
Generally, if you might ever want to use one or collections of specific attributes on an object, that’s a trait’s job. Often traits are overused, but it’s precisely because they’re so powerful — as much as often using subfactories is much more economical, that doesn’t mean traits shouldn’t be used- but generally, it is a codesmell that if you’re using a trait a lot, it might be more suitable to be a subfactory, due to it being enough of a typical use case of the object that you’re going out of your way to account for it.
Associations
Factories can create other factories with ease. Creating the same several factories in many tests, when you’re only referencing one or two, is generally a codesmell of a situation that you should be associatively creating factories.
factory :user_with_purchases do
transient do
purchase_count 3
end
after(:create) do |user, evaluator|
create_list(:purchase, evaluator.purchase_count, customer: user)
end
end
Note that any of this can utilize the usual FactoryBot methods — you could just as easily use build_list, or run through manually creating the other factories with highly-specific data. I’m not as much a fan of the latter, but once again, if you’re finding yourself building that data for many tests — better to use the factory to make life easier than to have a horrible bush of object creation at the start of your tests.
Randomized Data
This often gets overlooked, as many users don’t see the benefit of using properly randomized data. At the very least, one benefit is making life a little bit easier in the debugging of attributes — often, using FactoryBot’s sequence method can produce objects that altogether look too similar and can lead to mistakes.
But more important than that, properly-formatted random data can help spot small mistakes over time. For example, say you’re sending e-mails to users as text, but you have a user whose name doesn’t fit ASCII standards. Showing their name to them mangled certainly would feel disrespectful to that user — after all, you didn’t even care to properly use their name.
In the process, I try to use the Faker gem. Faker has a huge collection of data that’s randomized enough to generally spot you any potential bugs you overlooked in the case of unexpected data. I won’t go through all the kinds of data they can spoof, but it’s dozens, nearly everything I’ve ever wanted spoofed.
However, what about other values? No need to just use a hardcoded value here — just create an array and call .sample on it. This is especially useful in the case of enum-type attributes, since you even probably have an array of the values sitting around somewhere.
Do note however — to properly randomize the data, it must be executed in a block.
# Good!
name { Faker::Name.name }# Bad
name Faker::Name.name
If you were to use the bad version above, then every single user you created would have the same name. It would still be random each run of the test suite, but not as random as what we’re looking for.
Utilize FactoryBot Hooks
There’s a ton of hooks, some obvious, others not-so-obvious. I’m just going to list some here, but do remember to read FactoryBot’s GETTING_STARTED.md file.
initialize_with { do_thing } # How to initialize the object.
after(:build) { do_thing } # After Initialization, do this.
to_create { |i| i.do_thing } # How to save the object.
after(:save) { do_thing } # After the save, doobeedoo.
Some useful circumstances for some of these, is if you want to pass options to skip callbacks during initialization or creation, such as in Rails,
to_create { |r| r.save(validate: false) }
Expose test-relevant creation logic in your tests, not factories.
There’s a reason with all of this, I’ve avoided setting specific attributes — you want to be as expressive as possible when writing tests, to provide a better window into code intent to those reading the test(Your tests are your documentation!).
If you’re reusing particular factories or traits enough, it’s fine to have that logic where it belongs — in the factory file. But be careful to make sure that your subfactories and traits are not hiding attribute-based logic. Forcing a developer to have to constantly refer back to your factories to understand any of the tests they’re reading is a quick path to making testing more difficult.
Go make stuff.
Of course, none of my guidelines here should be treated as gospel. FactoryBot is an incredibly powerful library that’s become a standard in the Ruby community for a reason — it’s fast, it’s rock-solid, and it’s easy to use. Hopefully this gives a better baseline understanding to the capabilities and far-reaching limits of FactoryBot. Also, if you ever do anything super cool using FactoryBot that you think others should know about, I’d love to hear it.
Hell, if you read this at all, you oughta come say hi. Thanks.
Picture Credits
Google Images to the rescue.
- http://uflix.racing/industrial-revolution-textile-factories.html
- https://mexicoinstitute.wordpress.com/2013/12/16/north-america-to-drown-in-oil-as-mexico-ends-monopoly/
- https://www.maven.co/wp-content/uploads/2014/03/Plastic-factory.jpg
- http://bethrodden.com/2012/05/bluewater-rope-factory-tour/
- http://mnprairieroots.com/tag/farm/