Every test needs data. Instead of Rails' global fixtures we use factories that create only the records a test needs, which keeps tests isolated and predictable.
Important
Work on this lesson in
teachermode.
Learning goals
- You can explain why we create test data with factories instead of fixtures: each test starts empty and creates only what it needs.
- You can define factories with sensible defaults, using gems like FactoryBot, and use them in specs with overrides.
- You can define factories for associated records, and avoid creating duplicates where a record should be reused.
- You only set the attributes a test is about and let the factory supply the rest.
- You understand that excessive factory calls make test suites slow, so you create only the records a test needs.
- You can explain why factories must not contain random data.
- You can explain how the database is reset between tests and why test order must never change a result.
Learn
Factories, not fixtures
By default Rails uses global fixtures Show archive.org snapshot for its tests. This is a giant world of example data that grows with every test.
In our experience the use of fixtures can make a test suite hard to work with. In any non-trivial test suite there will be thousands of invisible dependencies between fixtures and test examples. E.g. to test a new edge case you make a small change to an existing fixture. Then a dozen tests break because they relied on the data you just changed.
Getting to know FactoryBot
Our favorite gem for test factories is FactoryBot Show archive.org snapshot . We used other gems in the past, but they all work in the same way.
Watch Railscasts PRO #158 Factories not Fixtures (revised) Show archive.org snapshot for an introduction to factories in general and FactoryBot in particular.
Note
This is an older video. FactoryGirl has since been renamed to FactoryBot.
Read the FactoryBot documentation Show archive.org snapshot — start with best practices Show archive.org snapshot and build strategies Show archive.org snapshot ; the pages on associations, traits, transient attributes, callbacks and custom construction come in handy for the exercises.
Factories should not be random
You may encounter libraries like Faker Show archive.org snapshot that creates realistic names, addresses, etc. for your sample data. This looks awesome, but at the risk of adding random strings to your screen.
Read don't build randomness into your factories for more background.
Tests should never influence each other
On a similar note, we don't want tests to behave differently based on their execution order or parallelism. One core mechanism in this regard is a gem called Database Cleaner Show archive.org snapshot . It ensures that every test starts with an empty database by erasing the test database before every unit and integration test.
Have a look at its documentation. The gem is already part of your MovieDB. Note that Rails itself can wrap each test in a database transaction that is rolled back afterwards (see the Rails Guides link below); MovieDB deliberately uses Database Cleaner instead, so tests can opt out of transactions where they need to.
Resources
Read what's new to you, skim what's familiar, skip what you already master. Stop when you can meet the learning goals.
Your agent can also generate an overview, a tutorial or an explanation for anything here, tailored to what you already know. Just ask.
- 📄 FactoryBot documentation Show archive.org snapshot — best practices Show archive.org snapshot , build strategies Show archive.org snapshot , associations Show archive.org snapshot , traits Show archive.org snapshot , sequences Show archive.org snapshot , transient attributes Show archive.org snapshot , callbacks Show archive.org snapshot , custom construction Show archive.org snapshot
- 📄
factory_bot_railsShow archive.org snapshot — the Rails integration you install in the first exercise - 📄 Rails fixtures Show archive.org snapshot — what we are replacing
- 📄 Don't build randomness into your factories — and why Faker Show archive.org snapshot stays out of our factories
- 📄 Rails Guides: Testing — Transactions Show archive.org snapshot and Capybara: Transactions and database setup Show archive.org snapshot — how the database is reset between tests
- 📄 Database Cleaner Show archive.org snapshot — what MovieDB and older projects use; background in Why RSpec users should care about Rails 5.1 Show archive.org snapshot
- 📄 RSpec: Where to put custom matchers and other support code
- ▶️ Drifting Ruby #103: Sample Data with Factory Bot and Faker Show archive.org snapshot — 8 minutes
- ▶️ Railscasts #158: Factories not Fixtures (revised) Show archive.org snapshot — 9 minutes; from 2012 and still says "FactoryGirl" (the gem was renamed to FactoryBot), but the concepts hold up
- 📘 Everyday Rails Testing with RSpec, chapter 5 "Creating meaningful test data" (in our library, see the previous card)
Exercises
Setup FactoryBot
If you haven't done so already:
- Install FactoryBot via
factory_bot_railsShow archive.org snapshot .
Tip
You will be calling your factories a lot. Therefore it is preferrable to just say
create(:movie)instead ofFactoryBot.create(:movie). You can configure this like so:RSpec.configure do |config| config.include FactoryBot::Syntax::Methods endAlso see RSpec: Where to put custom matchers and other support code.
Use factories everywhere
- Write a factory for each of your MovieDB models.
- In your model specs and E2E features, only use factories to create your test data.
- Leverage that factories will set defaults for attributes that you don't explicitly mention. You should remove any attribute values that are not relevant to a test. E.g. if a test needs a
Moviebut does not care about its#yearattribute, the test should let the factory fill in a default year. However, if the test checks for one particular year, it should be explicitly set in the test (even though there is a factory default). - Watch
log/test.logwhile a feature spec runs and count theINSERTs. Remove factories and records the test doesn't actually need — needless records are how test suites get slow.
Advanced factory
Create a factory that works like this:
create(:movie, :biography, person: 'Al Gore')
This creates up to three records (class names may differ in your MovieDB implementation):
- A
Moviewith with title"Al Gore: Biography" - An
Actorwith the name"Al Gore". If an actor with that name already exists, the record is re-used and no newActoris created. - A
Rolethat links the movie and actor above. The role's#character_nameshould also be"Al Gore", since he would be playing himself.
Tip
Don't actually give your
Moviemodel a#personattribute. Instead, use FactoryBot's transient attributes andbefore/afterhooks to implement the factory above.
Hint
Look at transient attributes Show archive.org snapshot in the factory_bot docs. A transient attribute is available in the factory's association blocks, where you can decide whether to create a record or reuse an existing one.