ParaTest: Running PHPUnit Suites in Parallel Without Rewriting Your Tests
:computer: Parallel testing for PHPUnit
At a glance
- What is it?
- ParaTest wraps PHPUnit and spreads test cases across worker processes. It targets teams whose sequential suite has become the slowest part of CI, and it trades full PHPUnit feature parity for speed.
- Who is it for?
- Adopt ParaTest if your PHPUnit suite is CPU-bound, your tests avoid shared static state, and you can pin the PHPUnit version it supports. Do not adopt it if your tests assert on static properties, constants, or reflection data exposed by other test classes, because the README lists those as unsupported.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem ParaTest solves, and who actually needs it
PHPUnit runs one test after another in a single PHP process. On a large suite, wall-clock time grows roughly with test count, and CI pipelines pay for it on every push. ParaTest's stated objective is to support parallel testing in PHPUnit: the README says that provided you have well-written PHPUnit tests, you can drop paratest in and start using it with no additional bootstrap or configurations. That is the whole pitch, and it is a narrow one. ParaTest is for teams that already have a working PHPUnit suite and a suite that has become slow enough to matter. It is not a test framework, it does not replace PHPUnit, and it does not make a badly structured suite safe to parallelize. The README is explicit that the benefit assumes well-written tests. If your tests share a database, a cache, or a static singleton, parallel execution will surface that as flakiness rather than speed.
How the parallel runner splits work: TestCase mode and functional mode
ParaTest ships a binary that sits in front of PHPUnit. The README describes two granularities. The default parallelizes by TestCase, and `--functional` parallelizes by Test. Those are different units of work, and the choice changes both speedup and failure isolation. Splitting by TestCase means each worker process gets whole test classes, so class-level setup and any static state inside a class stay in one process. Splitting by Test (functional mode) distributes individual test methods, which balances load better when class sizes are uneven but gives you less process locality. The README does not publish a scheduling algorithm, so how work is dispatched to workers is not documented in the available sources. What is documented is the process model: multiple PHP interpreter instances run at once, each with its own static variable space. That single fact drives most of the caveats below. The README also notes that the coverage cache is always warmed up by ParaTest before executing the test suite, which is a deliberate upfront cost paid to avoid every worker warming it independently.
Installing ParaTest and running a first parallel pass
Installation is a single Composer command, and the README shows it as a dev dependency. Run it from your project root.
composer require --dev brianium/paratestAfter that, the binary lands at `vendor/bin/paratest`. The README states you can run it with no extra bootstrap or configuration to parallelize by TestCase. Run this and you should see PHPUnit's normal output, but with test cases distributed across worker processes.
vendor/bin/paratestTo parallelize by Test instead of TestCase, pass the flag the README names.
vendor/bin/paratest --functionalFor the full option list, the README points at the help flag.
vendor/bin/paratest --helpOne thing to plan for before your first real run: if your tests need a separate database per process, the README documents a `TEST_TOKEN` environment variable, guaranteed to differ from every other currently running test. The README gives this example of using it.
if (getenv('TEST_TOKEN') !== false) { // Using ParaTest
$dbname = 'testdb_' . getenv('TEST_TOKEN');
} else {
$dbname = 'testdb';
}There is also a `UNIQUE_TEST_TOKEN` variable, which the README says is unique both per run and per process.
The setUp problem: initialization runs once per process, not once per suite
This is the first thing that breaks when a sequential suite goes parallel, and the README calls it out directly. A guard flag checked in `setUp()` looks like it runs initialization once, but it does not. Static variables persist for the life of a single process, and each worker is a separate process with its own copy of that flag. The README states the pattern runs the initialization once per process instead of once for the whole invocation. The documented fix uses the filesystem as shared mutable state: touch a lock file, then take an exclusive non-blocking lock. The first process to win the lock runs the initialization; the others block on a shared lock until it finishes. That is a real coordination primitive, and it is the kind of code most suites never needed before. If your suite has migrations, fixture seeding, or index creation in setup, budget time for this rewrite. The README's own example writes the lock file to `/tmp`, which is fine on a single machine and a problem the moment your workers are not on the same filesystem.
Code coverage under parallel execution, and the caveats that limit adoption
Coverage is where ParaTest does something genuinely useful: the README says you can run tests in N parallel processes and all the code coverage output will be combined into one report. It documents two engines. For PCOV, if the extension is installed but only needs to be enabled during tests, the README passes the PHP option through the binary.
php -d pcov.enabled=1 vendor/bin/paratest --passthru-php="'-d' 'pcov.enabled=1'"For Xdebug, the README says setting the environment variable is enough to have it active in subprocesses.
XDEBUG_MODE=coverage vendor/bin/paratestThe caveats section is the part that should decide adoption for many teams. The README states that constants, static methods, static variables, and everything exposed by test classes consumed by other test classes, including Reflection, are not supported. It attributes this to the current implementation of `WrapperRunner` and how PHPUnit searches for classes. The suggested fix is to move shared code into classes that are not tests themselves. That is a structural refactor, not a config change, and suites that use test classes as fixtures or base classes for shared assertions will feel it. A second constraint sits in the versions section: only the latest PHPUnit is supported, and only the latest ParaTest is maintained, because ParaTest relies heavily on PHPUnit `@internal` classes from version 5 onward. Upgrading PHPUnit means upgrading ParaTest in step. If you are pinned to an older PHPUnit for other reasons, ParaTest is the wrong tool.
Debugging a failing worker, and the PHPStorm helper binary
Parallel failures are harder to read than sequential ones, and ParaTest addresses that in two ways. The README says to enable debug output via `--verbose` for more information. More useful is the failure output itself: when a sub-process fails, the originating command is printed and can be copy-pasted into a terminal and run directly. The README also notes that internal commands run with `--printer [...]\NullPhpunitPrinter`, which silences PHPUnit's own output, and that removing that option during a debugging run restores the output. So the debugging path is: read the printed command, drop the null printer, run it alone. That is a workable loop, though it means the parallel run is not the thing you debug in. For IDE users, the repository ships a separate binary, `paratest_for_phpstorm`. The README's steps are to configure PHPUnit in PHPStorm first, add a PHPUnit-type run configuration named `ParaTest`, and put `./vendor/bin/paratest_for_phpstorm` in the interpreter options. Additional ParaTest options go in the test runner options section. The README claims it works with the Rerun failed tests and Toggle auto-test buttons, and notes that coverage must already work sequentially in PHPStorm before the helper binary handles it correctly.
How ParaTest differs from running PHPUnit with its own parallelism
The obvious comparison is plain PHPUnit. PHPUnit is the framework that defines, discovers and asserts your tests; ParaTest is a runner layered on top of it, and the README's framing is drop-in: install it, run `vendor/bin/paratest`, and your existing tests execute in parallel. The practical difference is who owns process management and result aggregation. With sequential PHPUnit you get one process and one output stream. With ParaTest you get N processes, a combined coverage report, and the `TEST_TOKEN` mechanism for per-process isolation. The cost is the compatibility surface described above: reliance on PHPUnit internals means version lockstep, and the unsupported static and reflection cases mean some suites cannot simply be dropped in. A second comparison worth making is against the approach of splitting your suite into shards and running several PHPUnit processes yourself in CI. ParaTest does that internally and merges coverage, which saves you writing the shard logic. It does not remove the underlying requirement that tests be isolated from each other. No amount of runner tooling fixes a suite that shares mutable global state.
Maintenance, licence, and what upgrading costs
The repository is not archived, and the last push was on 2026-09-28. Releases are frequent: v7.25.0 on 2026-09-24, v7.24.1 on 2026-08-17, and v7.24.0 on 2026-08-07. That cadence matches the README's own explanation that the fast pace of PHP and PHPUnit creates a maintenance burden the project can only absorb for the latest versions. Read that as a commitment with a boundary: you get support while you stay current, and the project makes no promise about older PHPUnit lines. The repository is licensed MIT, which permits commercial and proprietary use and modification, with the usual requirement to preserve the copyright and licence notice. That is a permissive licence, but it says nothing about the support obligations of the maintainers, and nothing here should be read as legal advice. The upgrade cost is the real one. Because ParaTest depends on PHPUnit internal classes, a PHPUnit major upgrade is not a background chore. The Makefile in the repository shows the project's own toolchain, including `vendor/bin/phpstan`, `vendor/bin/phpcbf`, and an Infection mutation step, which tells you the maintainers hold themselves to a static analysis and mutation testing bar. Expect your own upgrade to need a full test run, not just a Composer update.
Editorial conclusion
Adopt ParaTest if your PHPUnit suite is CPU-bound, your tests avoid shared static state, and you can pin the PHPUnit version it supports. Do not adopt it if your tests assert on static properties, constants, or reflection data exposed by other test classes, because the README lists those as unsupported. Before rolling it into CI, verify two things on your own suite: that a run with --functional produces the same pass and fail set as a sequential PHPUnit run, and that your coverage engine still emits a combined report under parallel execution.
Frequently asked questions
What is a parallel test?
In ParaTest's context, a parallel test run means multiple PHP processes execute parts of the same PHPUnit suite at the same time instead of one process running everything in sequence. ParaTest splits the suite by TestCase by default, or by Test when you pass --functional.
How do I install ParaTest?
The README gives one Composer command, run as a dev dependency: composer require --dev brianium/paratest. After that the binary is available at vendor/bin/paratest.
How do I give each ParaTest process its own database?
The README documents a TEST_TOKEN environment variable that is guaranteed to differ from every other currently running test, and shows appending it to a database name so each process uses its own. A UNIQUE_TEST_TOKEN variable is also available and is unique per run and per process.
Why does my setUp initialization run more than once under ParaTest?
Each worker is a separate PHP process with its own static variables, so a static initialized flag in setUp() is per process, not per suite. The README's fix uses a lock file with flock() so the first process runs the initialization and the others wait.
Which PHPUnit versions does ParaTest support?
The README states that only the latest version of PHPUnit is supported, and only the latest version of ParaTest is actively maintained. This is because ParaTest relies heavily on PHPUnit @internal classes from version 5 onward.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/paratestphp-paratest)