⚑ rspec-turbo

Gem Version Ruby Style License: MIT

Run your whole RSpec suite in parallel β€” with zero config.

rspec-turbo spreads your specs across every core, balancing the load by the actual number of examples (not file size, not a stale timing log) and even splitting a single oversized file across workers. One command, a live progress dashboard, and a report that tells you exactly which folders are slowing you down.

bundle add rspec-turbo --group "development, test"
bundle exec rspec-turbo

That's it. No runtime logs to maintain, no grouping flags to tune.


🏎️ See it run

====================================================================
  RSpec Turbo - Parallel
====================================================================

  βœ“ 8 DB(s) ready (0s)
  βœ“ 4210 examples Β· 312 files Β· 8 batches (~526 each) (3s)

  βœ“ worker/01  1m02s  PASS   requests/v1
  βœ“ worker/02  58s    PASS   models Β· services
  β Ή worker/03  ~520 ex  46s   jobs Β· mailers
  ...

  β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–‘β–‘β–‘β–‘β–‘β–‘β–‘  2731/4210  65%

====================================================================
  RSpec Turbo Report
====================================================================

  Slowest folders  ↳ optimize these first

  requests/v1                                     1m12s  β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“
  models                                            48s  β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–‘β–‘β–‘β–‘β–‘β–‘β–‘
  services                                          31s  β–“β–“β–“β–“β–“β–“β–“β–“β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘

  βœ“ All passed  Β·  4210 examples  Β·  8 workers  Β·  wall 1m04s  sum 7m58s  7.4x

(Illustrative output β€” your speedup scales with your cores.)


Why not just use parallel_tests?

parallel_tests is a great, battle-tested tool β€” and if you run Minitest, Cucumber, Test::Unit and RSpec, its multi-framework reach is exactly what you want.

But if your project is RSpec-only, that generality costs you. rspec-turbo does one thing and tunes hard for it:

parallel_tests rspec-turbo
Scope Multi-framework (RSpec, Minitest, Cucumber…) RSpec only β€” focused and lean
Default balancing By file size (bytes) β€” a rough proxy for time By actual example count from one rspec --dry-run
Best-case balancing --group-by runtime, needs a runtime log you record and keep fresh Recomputed every run from the dry-run β€” always current, nothing to maintain
Unit of work A whole file β€” one giant *_spec.rb stalls a process A file or example-ID slices β€” splits big files across workers
Config to balance well Generate + commit tmp/parallel_runtime_rspec.log None β€” good distribution out of the box
Live output Per-process stdout, interleaved Live TTY dashboard (spinner per worker + global bar) / clean CI progress
Final report Concatenated process outputs One consolidated report: failures per worker, speedup, slowest folders/files
Slow-test insight DIY (--profile per process, aggregate yourself) Built in, on by default β€” per-file time + SQL query counts, aggregated
Test-DB setup rake parallel:prepare (you decide when to re-run) Automatic, schema-fingerprint cached β€” skipped when the schema hasn't changed
JUnit / coverage merge Extra wiring Built in (JUNIT_DIR, COVERAGE=1)

What that means in practice

  • Better balance, no homework. parallel_tests' file-size grouping puts a 500-line file with 3 slow examples in the same weight class as a 500-line file with 80 fast ones. rspec-turbo counts the examples (via a fast dry-run) and packs them with a longest-processing-time-first heuristic β€” and it does this every run, so it never goes stale and there's no runtime log to commit.
  • No single-file bottleneck. When one mega *_spec.rb holds 20% of your suite, a file-based splitter leaves one process grinding while the rest idle. rspec-turbo slices that file by example ID across workers.
  • Answers, not just speed. Every run ends with a ranked "slowest folders / files" report (and SQL query counts under Rails), so you know what to optimize next β€” not just that the suite is slow.

Install

Add it to the :development, :test group of your Gemfile:

group :development, :test do
  gem "rspec-turbo"
end
bundle install

Why :development too? rake and rails commands run in the development environment by default, and Bundler only loads gems from the groups for the current environment. The spec:turbo / coverage:merge tasks are registered by a Railtie, so the gem must be loaded in development for rake spec:turbo / rails spec:turbo to exist. (The bundle exec rspec-turbo binary works from any group.) If you must keep it in :test only, run the tasks with RAILS_ENV=test rails spec:turbo.

Usage

bundle exec rspec-turbo                              # all of spec/
bundle exec rspec-turbo spec/models lib              # specific folders
bundle exec rspec-turbo spec/models/project_spec.rb  # a single file
bundle exec rspec-turbo --exclude-pattern "spec/requests/**/*"
bundle exec rspec-turbo --fail-fast spec/models

RSPEC_TURBO_MAX=6 bundle exec rspec-turbo            # cap workers
RSPEC_TURBO_FORCE_SETUP=1 bundle exec rspec-turbo    # recreate test DBs

Any RSpec flag you pass through (--tag, --seed, --order, …) is forwarded to every worker.

Three ways to launch it

bundle exec rspec-turbo       # the binary β€” full control (paths, flags)
bundle exec rails spec:turbo  # the same task, via the Rails CLI
bundle exec rake spec:turbo   # Rake task β€” runs the whole suite

In a Rails app the spec:turbo task is registered automatically (via a Railtie), and rails spec:turbo works because Rails routes unknown commands to Rake. The rake/rails forms run the entire suite β€” ideal for CI; for specific folders or RSpec flags, reach for the rspec-turbo binary.

How it works

parse argv β†’ DbSetup β†’ FileDiscovery β†’ BatchPlanner β†’ Executor (pool) β†’ Report
  1. DbSetup β€” spawns one rails db:drop db:create db:schema:load db:seed per worker slot (each with its own TEST_ENV_NUMBER). Cached by a fingerprint of db/schema.rb + db/seeds.rb and the worker count, so repeat runs skip it.
  2. FileDiscovery β€” globs *_spec.rb, applies --exclude-pattern.
  3. BatchPlanner β€” one rspec --dry-run --format json counts examples per file, then bin-packs files into balanced batches (Longest-Processing-Time first). Files heavier than a batch's fair share are split into example-ID slices so no single file bottlenecks a worker.
  4. Executor β€” a fixed pool of slots; each finished slot is recycled until the queue drains. Live dashboard on a TTY, periodic [progress] lines on CI.
  5. Report β€” failures, slowest folders/files, and a one-line summary with wall time, summed CPU time and the resulting speedup.

Requirements

  • Rails β€” DbSetup uses rails db:*. (Non-Rails projects can still run if the databases already exist; set RSPEC_TURBO_FORCE_SETUP=0, the default, and the setup is skipped on a cache hit.)
  • rspec_junit_formatter β€” only when JUNIT_DIR is set.
  • simplecov + simplecov_json_formatter β€” only when COVERAGE=1 (see Coverage below).

Parallel test databases (required)

Each worker runs with its own TEST_ENV_NUMBER (1, 2, … up to the worker count), so every worker needs its own database β€” otherwise they trample each other. Make the test database name include TEST_ENV_NUMBER in config/database.yml:

default: &default
  adapter: postgresql
  username: postgres
  password: postgres

test:
  <<: *default
  database: myapp_test<%= ENV["TEST_ENV_NUMBER"] %>

That resolves to myapp_test1, myapp_test2, … β€” one per worker. (Plain rails commands, with no TEST_ENV_NUMBER, still use myapp_test.)

On the first run β€” or after a schema change β€” rspec-turbo creates and loads all of them for you (db:drop db:create db:schema:load db:seed, one process per slot, each with its own TEST_ENV_NUMBER). Later runs reuse them via the schema-fingerprint cache; force a rebuild with RSPEC_TURBO_FORCE_SETUP=1.

Environment variables

Variable Default Purpose
RSPEC_TURBO_MAX nproc Number of parallel workers
RSPEC_TURBO_LOG_DIR tmp/rspec-turbo Where per-worker logs live
RSPEC_TURBO_FORCE_SETUP off 1 recreates the test DBs even if cached
RSPEC_TURBO_PROGRESS_INTERVAL 30 Seconds between CI progress lines
COVERAGE 0 1 merges SimpleCov results after the run
JUNIT_DIR β€” Emit one JUnit XML per worker into this dir
CI β€” Forces the plain (non-TTY) progress mode

Slowest-files report (on by default)

The "Slowest folders / Slowest files" section is fed by the bundled slow_profile hook, loaded into every worker. It is on by default: each worker times every example, and under Rails it also counts SQL queries via ActiveSupport::Notifications. Outside Rails it degrades gracefully β€” it just times examples and reports zero queries.

Turn it off with the master kill switch:

RSPEC_TURBO_NO_PROFILE=1 bundle exec rspec-turbo
Variable Default Purpose
RSPEC_TURBO_NO_PROFILE off 1 disables profiling entirely (master kill switch)
RSPEC_PROFILE_THRESHOLD_TIME 0.2 Seconds an example must exceed to make the "slow examples" list
RSPEC_PROFILE_THRESHOLD_QUERIES 30 Query count an example must exceed to make that list
RSPEC_PROFILE_GROUP_BY β€” 1/auto, a base path, or a comma list of folders to bucket by

Coverage (optional)

With COVERAGE=1, each worker records coverage under its own TEST_ENV_NUMBER and rspec-turbo merges the results into a single report when the run ends, via the bundled coverage:merge task β€” JSON on CI (SimpleCov::Formatter::JSONFormatter), HTML locally (SimpleCov::Formatter::HTMLFormatter).

  1. Add the formatters to your Gemfile:
   group :test do
     gem "simplecov", require: false
     gem "simplecov_json_formatter", require: false
   end
  1. Have each worker write its own result file (so parallel workers don't clobber each other), keyed by TEST_ENV_NUMBER β€” at the very top of spec/spec_helper.rb, before your app is required:
   if ENV["COVERAGE"] == "1"
     require "simplecov"
     SimpleCov.command_name "worker_#{ENV["TEST_ENV_NUMBER"]}"
     SimpleCov.coverage_dir "coverage/#{ENV["TEST_ENV_NUMBER"]}"
     SimpleCov.start "rails"
   end
  1. Run it:
   COVERAGE=1 bundle exec rspec-turbo

The merge collates coverage/**/.resultset.json (override with RSPEC_TURBO_COVERAGE_GLOB) and writes the combined report to coverage/. You can also run it on its own: bundle exec rake coverage:merge.

Architecture

lib/rspec_turbo/
β”œβ”€β”€ config.rb            # env-driven settings + derived log paths
β”œβ”€β”€ terminal.rb          # colour, duration formatting, spinner, separators
β”œβ”€β”€ options.rb           # split ARGV into rspec flags vs folders
β”œβ”€β”€ db_setup.rb          # cached parallel test-DB creation (Rails)
β”œβ”€β”€ file_discovery.rb    # find + filter *_spec.rb files
β”œβ”€β”€ batch_planner.rb     # dry-run counting + LPT bin-packing
β”œβ”€β”€ display.rb           # live spinner + final report + log parsing
β”œβ”€β”€ worker.rb            # spawn one rspec process per batch
β”œβ”€β”€ executor.rb          # the slot pool + TTY/CI run loops
β”œβ”€β”€ runner.rb            # top-level orchestration
β”œβ”€β”€ progress_reporter.rb # formatter injected into workers (progress bar)
β”œβ”€β”€ slow_profile.rb      # profiler injected into workers (slow report)
β”œβ”€β”€ railtie.rb           # registers the spec:turbo task in Rails apps
└── tasks.rake           # the rake spec:turbo / rails spec:turbo task

Development

bundle install
bundle exec rake          # runs the specs + Standard
bundle exec rspec         # specs only
bundle exec standardrb    # lint
bundle exec standardrb --fix

Style is enforced by Standard Ruby. The .rubocop.yml simply loads Standard's ruleset so editors and tooling that speak RuboCop pick up the same rules; the canonical runner is standardrb. VS Code is pre-wired (.vscode/settings.json) to format on save with Standard via the Ruby LSP extension.

Troubleshooting

Unrecognized command "spec:turbo" (Rails::Command::UnrecognizedCommandError) β€” the gem isn't loaded in the environment your command runs in. rake/rails default to development, so move the gem to group :development, :test (see Install), or run RAILS_ENV=test rails spec:turbo. The bundle exec rspec-turbo binary works regardless.

Workers fail with "database … already exists" / connection clashes β€” your config/database.yml test database name isn't keyed by TEST_ENV_NUMBER. See Parallel test databases.

Contributing

Issues and pull requests are welcome. Run bundle exec rake before opening a PR β€” it must be green (specs + Standard).

If rspec-turbo shaves minutes off your CI, drop a ⭐ on the repo β€” it helps other RSpec teams find it.

License

MIT. See LICENSE.txt.