A slow test suite is a hidden tax on development. It’s not just the minutes spent waiting for a green light. It’s the psychological friction. When running tests is painful you do it less often. You push code hoping CI will catch something. Your feedback loop stretches from seconds to minutes or even hours. This friction kills momentum.
Most developers assume slow tests are an unavoidable consequence of a growing application. They are not. Slow tests are usually the result of a few common mistakes that are easy to fix once you see them. And the biggest mistake of all is talking to the database when you do not need to.
Your application code is fast. Modern Ruby is remarkably quick. Instantiating objects is cheap. What is not cheap is I/O. Writing to disk is slow. And the main source of I/O in a Rails test suite is the database.
Every call to User.create
or post.save
involves a round trip to your test database. It writes records checks constraints and updates indexes. This takes milliseconds. Milliseconds add up when you have thousands of tests each creating half a dozen records. Your two second test becomes a ten minute test suite. The cause is almost always an over-reliance on database persistence.
Factories are a powerful tool. Tools like FactoryBot make it trivial to generate test data. The problem is that they make it too easy to create more than you need. The default way to use a factory is to create a fully persisted record.
Consider a simple model method.
def user_can_publish_post?(post)
user.admin? || user.id == post.user_id
end
To test this you might create a user and a post. let(:user) { create(:user) }
and let(:post) { create(:post, user: user) }
. The create
call hits the database. Twice. But look at the method. It does not need the database at all. It just needs two objects that respond to a few methods like id
and admin?
. It needs no proof that these objects can be saved or retrieved.
This is the root of the problem. We use factories to create real database records to test logic that never touches the database.
FactoryBot gives you a way out. It is called build_stubbed
. Unlike build
which creates an in-memory object with no ID build_stubbed
creates an in-memory object and gives it a fake ID. It behaves like a persisted record without ever touching the database.
Changing create(:user)
to build_stubbed(:user)
can make a single test file orders of magnitude faster. It is the single most effective change you can make to speed up your unit tests.
Your rule of thumb should be to always start with build_stubbed
. If the test fails then try build
. Only if that also fails should you reach for create
. You will be surprised how rarely you need it. Most of your model tests and service object tests can run without the database entirely.
If you have already started pulling complex logic out of your models you will find this even easier. Simple Ruby objects are much simpler to test because their dependencies are explicit. You can read more about that pattern here: https://journalofme.com/blog/when_rails_models_get_too_big.
Of course some tests do need a real database connection. Controller tests that check a full request cycle need it. Integration tests that verify user flows need it. Tests for complex ActiveRecord scopes definitely need it. You cannot test Post.published
without a database containing published and unpublished posts.
The key is to be deliberate. For these slower tests separate them. Many teams run their fast unit tests first and only run the full suite of integration tests before merging or in CI. This gives you the best of both worlds a rapid feedback loop for most of your work and full coverage when it counts.
Also check your test setup. Ensure you are using the transaction strategy for cleaning the database between tests not truncation. Transactions are much faster because they wrap each test in a database transaction and roll it back at the end. Truncation deletes all data from the tables which is much slower.
The goal is not just speed. The goal is a test suite that feels like a helpful tool not a chore. A suite that runs in seconds encourages you to run it constantly. You write a method and run the test. You refactor and run the tests. This tight feedback loop is where good work happens.
Stop accepting a slow test suite as normal. Look at your tests especially your model tests. See how many create
calls you have. Try changing one to build_stubbed
and watch the time drop. It feels like magic but it is just being intentional about what you are asking your test to do.
Take a moment to think about your slowest test file and what it’s really doing.
— Rishi Banerjee
September 2025