To speed up a 20-minute Minitest suite with parallelize_me!, what does it actually parallelise, how does MT_CPU size it, and which tests break?
answer
- threads in one process, not forks
- MT_CPU, else Etc.nprocessors
- ENV["N"] removed in 6.0
- parallel classes run after serial ones
- global stubs, ENV, Dir.chdir break
basics
~20 sparallelize_me! runs that class's tests on a pool of threads in the same process, sized by MT_CPU or the CPU count. Under CRuby's GVL it mainly speeds up tests that wait on I/O, and it breaks tests that share process-wide state.
solid answer
~40 s`parallelize_me!` marks a class (and its subclasses) with `run_order :parallel`; its tests are pushed onto a queue served by a pool of **threads** in the same process. The pool size is `MT_CPU`, else `Etc.nprocessors`; with a size of 1 no executor exists and `parallelize_me!` does nothing. Minitest 6 removed the old `N` variable for this. Parallel classes run after all serial ones, and reporting is synchronised. On CRuby only one thread runs Ruby code at a time, so a CPU-bound suite gains little; tests that wait on a database, HTTP or `sleep` gain most. What breaks is anything process-wide: class-method stubs such as `Time.stub(:now)`, `ENV` edits, `Dir.chdir`, globals and class-level caches, shared database rows, and `Minitest::Mock` across threads. Measure first (`rake test:slow`), then parallelise classes proven isolated.
code
bash · 4 linesrake test:slow # the 25 slowest tests
MT_CPU=8 rake test # size the thread pool
MT_CPU=1 rake test # disable parallelism to compare
MT_HELL=1 rake test # parallelise every class to probe isolationgo deeper
Recall that parallelize_me! goes inside a test class and that MT_CPU sets how many threads run its tests.
Explain the thread pool and queue, serial-before-parallel scheduling, MT_CPU versus the removed N, and why the GVL limits CPU-bound gains.
Lead the speed-up: measure with test:slow, remove global stubs and shared state, parallelise isolated classes, and use MT_CPU=1 to bisect regressions.
Choose between threads, process-level splitting and fixing slow tests for the team's suite, weighing CI cost, flakiness risk and maintenance.
## Start with measurement A 20-minute suite is rarely slow evenly. Before parallelising, find where the time goes: - `rake test:slow` (from `Minitest::TestTask`) runs the suite verbosely and prints the 25 slowest tests. - `-v` on any run prints each test with its duration. - `rake test:isolated` runs each file alone and shows which ones fail in isolation, a hint about hidden coupling that parallelism will also expose. Often a few classes that sleep, call real HTTP or rebuild big fixtures account for most of the time, and fixing them beats any runner setting. ## What `parallelize_me!` does ```ruby class RemoteFineLookupTest < Minitest::Test parallelize_me! # ... end ``` 1. When Minitest loads it reads `MT_CPU`, falling back to `Etc.nprocessors`, and creates a `Minitest::Parallel::Executor` with that many worker threads if the number is greater than 1. 2. `parallelize_me!` returns immediately if there is no executor. Otherwise it makes the class's `run_order` `:parallel` and replaces how its tests are scheduled: each test is pushed onto a `Thread::Queue`. 3. The workers pop jobs and run each test on a fresh instance, recording results under a lock. 4. `run_all_suites` runs all **serial** classes first and the **parallel** ones afterwards, so serial tests never overlap with threaded ones. Calling it on `Minitest::Test` itself, or requiring `minitest/hell` (or setting `MT_HELL`), parallelises every class, which is a quick way to find out how isolated a suite really is. Minitest 6.0 removed the deprecated `ENV["N"]` for the thread count; `MT_CPU` is the only knob. `MT_CPU=1` switches parallelism off without editing code. ## Why threads help less than expected The executor uses threads, not processes. On CRuby a thread must hold the Global VM Lock to run Ruby code, and it releases it while blocked on I/O. So: | test workload | expected gain | |---|---| | waits on a database, a socket or `sleep` | large: other threads run meanwhile | | pure Ruby computation | little or none: threads take turns | | mixed | somewhere in between; measure it | Minitest itself has no process-based parallel mode. Spreading a CPU-bound suite across cores means several processes, each running a slice of the files. ## What breaks under parallelism Everything that is shared by the whole process is now shared by concurrently running tests: - **Class-method stubs.** `Time.stub(:now, t)` replaces `now` on `Time` for every thread during the block. - **`Minitest::Mock`** is documented by minitest-mock as not supporting multi-threading. - **`ENV` changes, `Dir.chdir`**, globals and class-level caches or configuration. - **Output capture** with `capture_io`, which swaps the process-wide `$stdout` and can pick up another thread's output. - **Shared external state**: the same database rows, files at fixed paths, fixed ports. - **Output assertions** that expect only their own test's output. The classic symptom is a suite that passes with `MT_CPU=1` and fails intermittently with more threads. ## A rollout plan 1. Measure and fix the slowest tests first. 2. Make isolation the default: temp dirs per test, injected clocks instead of `Time.stub`, unique records per test. 3. Add `parallelize_me!` class by class, starting with I/O-heavy, well-isolated classes. 4. Run several seeds in CI after each step; a failure that vanishes with `MT_CPU=1` points at shared state. 5. Keep a way out: `MT_CPU=1` in the environment turns parallelism off while you investigate.
- With Minitest, a suite passes with MT_CPU=1 but fails randomly with MT_CPU=8. Where do you look?At process-wide state touched by the parallel classes: class-method stubs such as `Time.stub(:now)`, `ENV` edits, `Dir.chdir`, globals and class-level caches, fixed file paths or ports, shared database rows, and mocks called across threads. Run the failing seed again, then check which parallel tests overlapped with the failing one.
- With Minitest on CRuby, why may parallelize_me! barely speed up a CPU-heavy test class?The executor runs tests on threads within one process, and on CRuby only the thread holding the Global VM Lock executes Ruby code. Pure computation therefore takes turns; the gain comes from tests that block on I/O and release the lock. CPU-bound suites need several processes instead.
saying these in an interview costs you the question
- parallelize_me! forks a process per CPU core.
- N=8 rake test sets the Minitest 6 thread count.
- Parallel tests on CRuby run Ruby code on all cores at once.
- Time.stub(:now) inside one parallel test only affects that test.
- parallelize_me! still runs tests in parallel when MT_CPU=1.