---
title: Things you might not need in your tests
teaser: Your test suite is slow and flaky because it does too much.
tags: testing,performance
author: Matheus Richard
published_on: 2025-01-29
---

No one likes slow and brittle tests, in particular if you're working a TDD
workflow. Often, tests get that way because they're doing too much. Consider
avoiding the following:

## Using a database

Database access imposes many costs on your tests. You need to setup a database, connect to it, clean
it between test runs, etc. And then each call to the db is _slow_.

In a typical Rails app, it's unlikely that you'll be able to avoid the database
entirely, but you can reduce its usage. For instance, let's consider this model:

```rb
class User < ApplicationRecord
  def first_name = name.split.first
end
```

If we would test this method, there's no need to hit the database since we're
only dealing with in-memory values.

```rb
test "should return the first name" do
  # Instantiate the model, but don't save it to the database
  user = User.new(name: "John Doe")

  assert_equal "John", user.first_name
end
```

If you're not fetching, saving, updating, deleting or counting records,
you probably don't need the database and can just instantiate the model.

A single test might not make a big difference, but doing it consistently across
the suite will make it run faster and more reliably.

## Associations

Factories [are great], but be careful with your associations. It is easy to
cause a long nested chain of records created that aren't strictly necessary for
the test running. To avoid this, prefer creating associations via traits, so
you'll always be explicit when requiring an association. It will help you not
to create unnecessary records.

[are great]: https://thoughtbot.com/blog/why-factories

```rb
FactoryBot.define do
  factory :author do
    name { "John Doe" }

    trait :with_book do
      after(:create) do |author|
        create(:book, author: author)
      end
    end
  end
end
```

<aside class="info">
  <p><strong>Pro tip:</strong> use the gem <a href="https://test-prof.evilmartians.io/profilers/factory_doctor">factory_doctor</a> to catch (possible) unnecessary record creations in your tests.
  </p>
</aside>

If you can, [use `build_stubbed` instead of
`create`/`build`](https://thoughtbot.com/blog/use-factory-bots-build-stubbed-for-a-faster-test)
to avoid hitting the database in your factories. But remember that not using
the FactoryBot at all [will be even faster].

[factory_doctor]: https://test-prof.evilmartians.io/profilers/factory_doctor
[will be even faster]: https://thoughtbot.com/blog/speed-up-tests-by-selectively-avoiding-factory-bot

## Tracking database changes

Another tip is to disable gems that track database changes, like `paper_trail` in tests:

```rb
# config/environments/test.rb
PaperTrail.enabled = false
```

You can always re-enable it in a specific test if you need to test that behavior:

```rb
# spec/support/paper_trail.rb
def with_paper_trail
  was_enabled = PaperTrail.enabled?
  was_enabled_for_request = PaperTrail.request.enabled?
  PaperTrail.enabled = true
  PaperTrail.request.enabled = true
  yield
ensure
  PaperTrail.enabled = was_enabled
  PaperTrail.request.enabled = was_enabled_for_request
end
```

This avoids creating several unnecessary records in the database, which slow
down your tests. On that note...

## Creating several records

If you can't avoid hitting the database, you should try to reduce the
number of records you create. As a rule of thumb, you should create as few
records as possible. **In tests, 2 records == many records**. If you need to
create tens of records to test something like pagination, it is worth making
that threshold configurable so you can cause pagination with just a couple of
records.

```rb
# config/environments/test.rb
Rails.application.configure do
  config.to_prepare do
    Pagy::DEFAULT[:limit] = 2 # instead of, say, 20
  end
end
```

## Security

Unless you're testing security-related code, disable high-security features, like strong
password hashing algorithms in your tests. These are usually slow,
so it's a quick win to disable/lower them in tests.

If you're using [Devise], for instance, you should get this automatically in the config file:

```rb
Devise.setup do |config|
  config.stretches = Rails.env.test? ? 1 : 10
end
```

But if you're using something like the Rails 8 authentication generator, add
this to your `config/environments/test.rb`:

```rb
BCrypt::Engine.cost = BCrypt::Engine::MIN_COST
```

<aside class="info">
  <p>One of our readers,<a href="https://github.com/Uaitt">Lorenzo Zabot</a>, pointed out that
    <a href="https://github.com/thoughtbot/clearance">Clearance</a> does that
    <a href="https://github.com/thoughtbot/clearance/blob/10cc025bdbd2117ddfdc28208d3133a9ae7183e2/lib/clearance/password_strategies/bcrypt.rb#L35">automatically</a> for you.
  </p>
</aside>

That will make the password hashing algorithm faster -- but less secure -- in
your tests.

[devise]: https://github.com/heartcombo/devise

## Driving with a real browser

If you can avoid using a browser (by using [request tests] or [RackTest] instead
of Selenium), go for it! That alone can make things 100x faster -- and much more
reliable.

But if you _have_ to use a browser, consider diminishing its usage. For
instance, instead of filling out and submitting a form to sign in users in
tests, use [a backdoor] to assign cookies and create a session directly.

```rb
 def login_as(user)
  Current.session = user.sessions.create!
  cookies = ActionDispatch::Request.new(Rails.application.env_config).cookie_jar
  cookies.signed[:session_id] = { value: Current.session.id, httponly: true, same_site: :lax }
end
```

[a backdoor]: http://robots.thoughtbot.com/faster-tests-sign-in-through-the-back-door

<aside class="info">
  <p>
  The Rails 8 authentication generator <a href="https://github.com/rails/rails/pull/53708">should have this feature soon</a>.
  </p>
</aside>

[RackTest]: https://github.com/teamcapybara/capybara?tab=readme-ov-file#racktest

Also check out the Chrome DevTools Protocol for interacting with the browser in
tests -- in Ruby you can access that via [Cuprite] --. That can lead to [up to
30% faster tests].

[Cuprite]: https://github.com/rubycdp/cuprite
[up to 30% faster tests]: https://janko.io/upgrading-from-selenium-to-cuprite/

## Logging

Logging is generally unnecessary on tests, so disable it. Less code running (and
less IO being performed) means faster. You can control that with an env var, so
you can enable it back when you need to debug something:

```rb
# in config/environments/test.rb
Rails.application.configure do
  unless ENV['TEST_LOGS']
    config.logger = Logger.new(nil)
    config.log_level = :fatal
  end
end
```

Plus, consider using the block syntax [to avoid unnecessary allocations]:

```rb
# bad
Rails.logger.info "Something"

# good
Rails.logger.info { "Something" }
```

[to avoid unnecessary allocations]: https://willj.net/posts/you-should-use-the-rails-logger-block-syntax/

## Run fewer tests!

The less code you run, the faster it will finish and the faster you'll
discover failures. In development, you can use `rails test && rails test:system`
instead of `rails test:all`. If integration and unit tests run first and fail,
you get feedback faster. Only then run system specs. If you're using RSpec, you
can do something similar by using [the `--fail-fast` option].

[the `--fail-fast` option]: https://rspec.info/features/3-12/rspec-core/command-line/fail-fast/

Before deploying, you might want to run a suite of smoke tests that run first,
and then the full test suite. That way you can catch issues sooner.

## Test pyramid

In general, keep [the test pyramid] in mind:

1. Don't lean too much on system tests, as they're slow and brittle. Use them as smoke tests for the most important paths in your application.
1. Use other test types to exercise more specific scenarios. [View specs] can be useful to test all variants of a particular component, for example.
1. Write more [request tests], which are a good middle ground between unit and system tests.

These guidelines help keep our test suites fast and reliable, which means
that we'll run and trust them that much more.

---

Got another tip? [Let me know] and I'll add it to the list.

[the test pyramid]: https://martinfowler.com/bliki/TestPyramid.html
[View specs]: https://thoughtbot.com/blog/how-we-test-rails-applications#view-specs
[request tests]: https://thoughtbot.com/blog/faster-tests-with-capybara-and-request-specs
[Let me know]: https://twitter.com/matheusrich
