Skip to main content

Speeding Up Test Suites in Rails Applications with parallel_tests

In the world of Ruby on Rails development, writing tests is a cornerstone of building reliable, maintainable applications. But as your test suite grows, so does the time it takes to run it. In a recent instance, my team adhered to a philosophy of maximum test coverage—a noble goal that resulted in a robust suite of specs. The downside? It took around 10 minutes to complete the full run. While that might not sound like much, those minutes add up fast during active development, breaking focus and slowing down iteration cycles.

Traditionally, Rails test suites run as a single-core process. This means that even if your machine boasts multiple CPU cores, most of that power sits idle while your specs chug along sequentially. Eager to reclaim some of that lost time and make better use of my system’s resources, I went hunting for a solution. That’s when I discovered the parallel_tests Ruby gem—a game-changer for speeding up test suites in Rails applications.

What is parallel_tests?

The parallel_tests gem is a powerful tool designed to accelerate test execution by distributing your tests across multiple CPU cores—specifically, across all available logical cores. It supports popular testing frameworks like RSpec, Minitest, Cucumber, and more, making it a versatile addition to almost any Rails instance. The magic lies in its ability to split your test files into balanced groups—either by line count or runtime—and run each group in a separate process, complete with its own isolated database. The result? A test suite that finishes faster by tapping into your machine’s full potential.

To understand the performance implications, it’s important to know the difference between physical cores and logical cores. Physical cores are the actual hardware processors on your CPU chip. However, many modern CPUs use a technology called simultaneous multithreading (SMT)—known as Hyper-Threading in Intel processors—which allows each physical core to handle two threads simultaneously. These virtual or logical cores appear to the operating system as additional CPUs. For example, a processor with 6 physical cores might show up as having 12 logical cores due to SMT.

The parallel_tests gem takes advantage of all available logical cores. So if you’re running on a CPU with 6 physical cores and SMT enabled (resulting in 12 logical cores), the gem will distribute your tests across all 12 logical processors. This maximizes CPU utilization and can lead to significant performance improvements, though the actual speedup might not be exactly linear due to factors like I/O operations and the overhead of process management.

In my case, with a test suite that took 10 minutes on a single core, I saw an opportunity to slash that time significantly. Modern laptops and CI servers often come equipped with 4, 8, or even 16 logical cores—why not put them to work?

Setting It Up

Getting started with parallel_tests is straightforward. Here’s how I integrated it into my Rails project:

1. Install the Gem

Add it to your Gemfile under the test group:

group :test do
  gem 'parallel_tests'
end

Then run:

bundle install

2. Configure Your Database

Each parallel process needs its own database to avoid conflicts. Update your config/database.yml:

test:
  adapter: postgresql # or your preferred adapter
  database: myapp_test<%= ENV['TEST_ENV_NUMBER'] %>
  username: myuser
  password: mypass

3. Database Management

The gem provides several rake tasks for managing your test databases. WARNING⚠️: To ensure these commands only affect your test databases, always run them with RAILS_ENV=test. Without specifying the environment, the commands will run in your current environment and could potentially affect development or production databases.

Why multiple databases? When running tests in parallel, each process needs its own isolated database to prevent interference. Without separate databases, concurrent processes might modify the same data simultaneously, leading to race conditions and unpredictable test failures. For example, if two processes try to create a user with the same email address at the same time, one would fail if they shared a database. By giving each process its own database, we ensure complete isolation and reliable test results.

Creating Databases
# Create all test databases
bundle exec rake parallel:create

# For multi-database setups, create specific database
bundle exec rake parallel:create:<database>
bundle exec rake parallel:create:secondary
Schema and Migrations
# Copy development schema (to be also run each time after migrations)
bundle exec rake parallel:prepare

# Run migrations in all test databases (to be also run each time after migrations)
bundle exec rake parallel:migrate

# For multi-database setups, migrate specific database
bundle exec rake parallel:migrate:<database>
Setup and Cleanup

The parallel:setup task is a convenience command that combines multiple steps into one. It executes parallel:create, parallel:prepare, and parallel:migrate in sequence, making it perfect for CI environments or after running migrations. Instead of running these commands individually, you can simply use:

# Setup complete test environment (creates db and loads schema - ideal for CI)
bundle exec rake parallel:setup

When you need to clean up your test environment or free up database space, you can remove the parallel test databases:

# Drop all test databases if required
bundle exec rake parallel:drop

# For multi-database setups, drop specific test database if required
bundle exec rake parallel:drop:<database>

4. Run Your Tests

For RSpec (which we were using), the command is:

bundle exec rake parallel:spec

For Minitest, use parallel:test, or for Cucumber, parallel:features. The gem automatically detects your CPU core count and splits the workload accordingly.

The Payoff

On my first run, the difference was striking. What once took 10 minutes dropped to under 2 minutes on my machine with 12 logical cores (6 physical cores with SMT)—a speedup of over 4x! The gem intelligently balanced the specs across processes, ensuring no single core was overwhelmed. For larger suites, the gains can be even more dramatic; I’ve read reports of teams cutting hour-long runs down to 15-20 minutes by leveraging hardware with 16 or more logical cores.

It’s important to note that your mileage may vary. The actual speed improvements from parallel testing heavily depend on your system’s hardware resources—primarily the number of processor cores and available RAM. Performance can also fluctuate between runs based on your current system load and other running processes. For instance, running the same test suite during heavy development work (with multiple applications and services running) versus on a clean system can show noticeable differences in execution time.

Fine-Tuning for Maximum Impact

To squeeze out every ounce of performance, I experimented with a few tweaks:

1. Adjusting Process Count

By default, parallel_tests uses your machine’s core count, but you can override it. This flexibility is particularly valuable in several scenarios:

  • When you need to run tests while continuing development work
  • During CI/CD pipeline execution where resources are shared
  • On production-like environments where other critical processes need CPU time
  • When running memory-intensive test suites that might cause system strain

Control the number of parallel processes with:

# Use 2 cores
bundle exec rake parallel:spec[2]

This granular control ensures you can balance test execution speed with system resource availability.

2. Runtime-Based Splitting

For uneven test suites, add runtime logging to optimize the split. Add this to your .rspec file:

--format ParallelTests::RSpec::RuntimeLogger
--out tmp/parallel_runtime_rspec.log

3. Spring Integration

If you use Spring to preload your Rails environment, add the spring-commands-parallel-tests gem to speed up process boot times further.

Caveats to Watch For

No solution is without its quirks. Parallelizing tests can expose race conditions or shared resource issues that didn’t surface in a single-threaded run. In our instance, we hit a snag with a spec that relied on a shared file—two processes tried to access it simultaneously, and chaos ensued. The fix was simple: use isolated resources (like Tempfile) or namespace them with ENV['TEST_ENV_NUMBER'].

For example, when using Active Storage in your tests, you might need to configure separate storage paths in your config/storage.yml:

test:
  service: Disk
  root: <%= Rails.root.join("tmp", "storage", ENV['TEST_ENV_NUMBER'].presence || '1') %>

This ensures each test process uses its own storage directory, preventing file conflicts.

Important: Small test suites might actually run slower due to the overhead of spinning up multiple databases. Rails itself sets a threshold (50 tests) before parallelizing, but parallel_tests doesn’t—so for tiny suites, stick to the single-core approach.

Why It Matters

In an instance prioritizing maximum test coverage, a fast test suite isn’t just a luxury—it’s a necessity. Ten minutes per run might be tolerable once, but multiply that by dozens of runs a day, and you’re hemorrhaging productivity. parallel_tests turned that bottleneck into a strength, letting us maintain rigorous testing without sacrificing speed.

Conclusion

If your Rails test suite feels like it’s dragging, give parallel_tests a shot. It’s a low-effort, high-reward way to harness your machine’s full power and keep your development cycle humming. For my team, it was the difference between a slog and a sprint—and I suspect it could be for you too.

Explore Other Resources

February 14, 2024 in Case Studies, Product Engineering

CLAS – A system that integrates Hubspot, Stripe, Canvas

About This Project Ziplines is a series A-funded ed-tech startup with one goal—helping students attain the real-world skills they need to thrive in careers they love by partnering with universities.…
Read More
September 9, 2024 in blog

Gentle Introduction to Elasticsearch

Elasticsearch is a search engine based on the Lucene library. It provides a distributed, multitenant-capable full-text search engine with an HTTP web interface and schema-free JSON documents. Elasticsearch is developed…
Read More
May 17, 2024 in blog

How to fix “OAuth out-of-band (OOB) flow will be deprecated” error for Google apps API access.

Migrate your OAuth out-of-band flow to an alternative method. Google has announced that they will block the usage of OOB based OAuth starting from January 31, 2023. This has forced…
Read More

Leave a Reply