Slow test suites are a major pain point in projects, often due to RSpec
and FactoryBot
. Although minitest
and fixtures are sometimes viewed as outdated, they can greatly improve test speed.
We adopted a project using minitest and fixtures, and while it required some initial refactoring and establishing good practices, the faster test suite was well worth it! Stick with me to explore how these tools might actually be a good practice.
So, why is this setup faster? Partially, it's because minitest is more lightweight than RSpec
, which as a testing DSL relies on Ruby's metaprogramming. The main speed boost, however, comes from using fixtures over FactoryBot
, as fixtures load essential test data upfront at the beginning of the testsuite.
Finding peace with fixtures
While setting up the database upfront challenges the idea of maintaining a perfectly clean database state at the beginning of each test, with some disciplined practices, handling fixtures becomes quite manageable. Let’s dive into how this works.
Only few fixture per model (1-2 is enough in most cases)
You should only have a minimal amount of fixtures for each model! These instances should reflect the most common use cases of the model (which you will need for ~90%) of the tests.
But what to do if the fixtures are not quite what you need for one particular test? Remember that any change will be rollbacked at the end of any test, so you are free to:
- Update any fixture to reflect a required state.
- Create additional records.
- Delete all record from specific tables in the beginning of a test.
If there are rather common modifications/creation patterns just create a helper for that.
Isolate your tests
Something to keep in mind is that you never want your testsuite to break because you add additional fixtures or change fixtures, e.g. do not test against a count of the database, but rather the change that should happen.
So try to be as explicit as possible in every test and observe changes rather than the end result.
Bad example:
# will break if a new user fixture is created
test "user can be created" do
post users_path, params: { first_name: "Foo", last_name: "Bar" }
assert User.count == 3
end
Good example:
test "user can be created" do
assert_difference "User.count", 1 do
post users_path, params: { first_name: "Foo", last_name: "Bar" }
end
end
Conclusion
Benefits
- You tests will become more explicit by default (less tools such as
subject
,let
,describe
or factories to hide something!) - Smart parallel runs are possible by default with no additional tooling
- It is harder to mock with minitest
- Test speed is a lot faster
- Fixtures are handy for development seed data, they can be loaded in development with:
rails db:fixtures:load
Downsides
- Less matchers & library support
- It is harder to mock with minitest
- RSpec contains more tooling for stubbing/mocking
- In general people tend to have less experience with Minitest/Fixtures than FactoryBot & RSpec
- Less tools to keep tests DRY (e.g. no shared examples)