A different testing approach with Minitest and Fixtures

Updated . Posted . Visible to the public.

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)
Felix Eschey
Last edit
Henning Koch
License
Source code in this card is licensed under the MIT License.
Posted by Felix Eschey to makandra dev (2024-10-28 06:38)