When you install RSpec in your Rails application, two of the files it generates are
spec/rails_helper.rb. The generated source contains a lot of helpful comments, but it might not be immediately clear why there are two helpers instead of one.
When you write specs in your Rails app, at the top of each file you require a helper that loads RSpec and other dependencies:
require "spec_helper" require "word_count" RSpec.describe WordCount do # your specs here end
require "rails_helper" RSpec.describe User do # your specs here for the User model end
Which file you choose to require has implications when it comes to running unit tests for a given file or directory. Let's dig into what those implications are and the differences between them.
spec_helper is intended to be the most lightweight, fast RSpec config in your app. It's great for testing plain old Ruby objects (POROs) that don't require Rails. Even if those POROs require a gem or other classes, if they don't need Rails, you can use
spec_helper is what you'd use if you are testing a Ruby gem with RSpec or anything non-Rails. It configures RSpec and that's just about it.
spec_helper at the very top of the file, so it pulls in all of the config from that. So
spec_helper is our base and
rails_helper stacks on top of it.
You might be thinking, why not just have one helper and keep it easy?
I've certainly worked in codebases that do that, but that was before people understood that keeping them separate is important, especially when it comes to fast TDD cycles.
Running individual tests that require
spec_helper is much faster than requiring
rails_helper because it's not loading all of the Rails app and gems. It's just loading RSpec, the spec_helper, and the files required for the test.
rails_helper exists to be used with specs that test functionality that can't exist without Rails—controllers, routes, models, views. When testing a model's validations, you'll want to make sure those are happening in the context of Rails and working correctly, so you need to require Rails.
rails_helper is also useful for configuring gems and support files needed by those tests, like setting up FactoryBot. It's also where you might want to reset and clean up the database after every test run (an timely operation in the context of fast tests).
With a fresh Rails 7 codebase (source), here's the difference in speed for testing one plain Ruby class's method that lives in
spec_helper: Finished in 0.00182 seconds (files took 0.04228 seconds to load)
rails_helper(cold run): Finished in 0.01671 seconds (files took 1.07 seconds to load)
rails_helper(warm run): Finished in 0.01058 seconds (files took 0.45144 seconds to load)
There are two values to be aware of. The time it takes to run the specs (the first number) and the time it takes to load the files from disk. They are separate values and their aggregate is the total time it takes to run a given spec (or specs).
The spec_helper loads the files for testing 25x faster on cold runs! Even on a warm run with the files already loaded, spec_helper is still 10x faster at loading the files!
On top of that, running the actual code in the specs is 10x faster than both.
That's a huge difference when it comes to the time it takes and you'll notice it as you're going through the Red -> Green -> Refactor TDD cycle.
This happens because the
rails_helper is loading hundreds, potentially thousands of Ruby files and configuring the Rails app. Rails does a lot! Look at your
Gemfile.lock and see the dependency tree. Even if you have only ~15 gems in in your
Gemfile, it's likely there are far more than that because each gem has its own dependencies.
There's a cost to pulling in dependencies and working with an application framework as large as Rails—it slows things down.
This means that if you want to have faster tests when you're actively writing your code, you'll want to require
I get it. You're writing view, controller, and model code that all needs Rails to test them properly. That's true. And those tests are valuable. But there are still things you can do and should be aware of.
There comes a point when writing code you actually aren't doing anything related to Rails. Sure, maybe the objects being acted upon are models, but you could use POROs and then in your tests pass in
instance_double and require
spec_helper. When you build complex applications beyond CRUD, you'll begin to write more Ruby code that's not dependent on Rails.
You'll also be writing more unit tests, which, in general, won't need Rails. So you want to really leverage
spec_helper when writing unit tests for POROs.
Your POROs can live in
lib or in
app, wherever you want to put them. That's up to you ultimately.
It is important to note that the speed of your tests will ultimately come down to the slowest required helper. If you have three spec files that get run and one of them requires
rails_helper, that'll cause all of them to run slower because the file loading time is as slow as the slowest helper.
Since you're most likely running all of your tests on CI or occassionally on your machine, that's not a big deal. But it's something to be aware of. What you require won't impact the speed of your entire test suite running. For that, you'd need parallelization and a deeper dive into fixing your slowest specs.
What we're optimizing for is the tests you run while you're actively writing your code. Those individual file test runs need to be fast. Any friction and slowdown breaks focus.
Just like how rspec-rails creates two helpers from the get-go, you can do the same! Do you use Capybara for acceptance tests? Create
spec/acceptance_helper.rb that requires
rails_helper (thus also requiring
spec_helper) that configures Capybara and then in those specs:
There's no reason to slow down all of your Rails unit tests with the loading and configuring of even more code.
You can create whatever helpers you want. If you've got a directory of POROs in
lib that all require a common set up, create a helper for them that requires
- Optimize for fast single file test runs, which is where speed matters most with TDD
spec_helperas minimal as possible, basically only configure the core of RSpec in it
- Be mindful of what you
- Use POROs when possible for their clarity, their single responsibility, and faster unit tests
- Create separate helpers for different needs in your app, don't make single file test runs slower just because you need something loaded and configured in other specs
Managing your spec helpers and being intentional about them and understanding the difference is a major part in having fast, maintainable tests with Rails and RSpec.