β‘ rspec-turbo
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-turbocounts 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.rbholds 20% of your suite, a file-based splitter leaves one process grinding while the rest idle.rspec-turboslices 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
:developmenttoo?rakeandrailscommands run in the development environment by default, and Bundler only loads gems from the groups for the current environment. Thespec:turbo/coverage:mergetasks are registered by a Railtie, so the gem must be loaded indevelopmentforrake spec:turbo/rails spec:turboto exist. (Thebundle exec rspec-turbobinary works from any group.) If you must keep it in:testonly, run the tasks withRAILS_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
- DbSetup β spawns one
rails db:drop db:create db:schema:load db:seedper worker slot (each with its ownTEST_ENV_NUMBER). Cached by a fingerprint ofdb/schema.rb+db/seeds.rband the worker count, so repeat runs skip it. - FileDiscovery β globs
*_spec.rb, applies--exclude-pattern. - BatchPlanner β one
rspec --dry-run --format jsoncounts 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. - 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. - Report β failures, slowest folders/files, and a one-line summary with wall time, summed CPU time and the resulting speedup.
Requirements
- Rails β
DbSetupusesrails db:*. (Non-Rails projects can still run if the databases already exist; setRSPEC_TURBO_FORCE_SETUP=0, the default, and the setup is skipped on a cache hit.) rspec_junit_formatterβ only whenJUNIT_DIRis set.simplecov+simplecov_json_formatterβ only whenCOVERAGE=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).
- Add the formatters to your Gemfile:
group :test do
gem "simplecov", require: false
gem "simplecov_json_formatter", require: false
end
- 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 ofspec/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
- 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.