Paraunit is a tool for faster executions of PHPUnit test suites. It makes this possible by launching multiple test in parallel with single PHPUnit processes.
Paraunit is developed using Symfony components.
Requirements
Paraunit is used in conjunction with PHPUnit. It reads PHPUnit’s .xml configuration file, so it’s needed to know which test to load.
If you are testing a Symfony+Doctrine application, it’s suggested to use also dama/doctrine-test-bundle, to avoid database concurrency problems during functional testing; also, if your want to run functional tests, remember to warm up the cache before, in order to avoid a mass cache miss (and relative cache stampede) with concurrency problems, and subsequent random failures.
Installation
To use this package, use Composer:
- from CLI:
composer require --dev facile-it/paraunit
- or, directly in your
composer.json
:
{
"require-dev": {
"facile-it/paraunit": "^2.0"
}
}
Usage
run
command
The run
command is the main functionality of Paraunit; it launches all the tests in all your configured test suites in parallel; you can run it like this: (assuming your composer’s bin dir is vendor/bin
)
vendor/bin/paraunit run
This is possible because Paraunit starts as a Symfony console command, and it’s provided through a bin launcher.
coverage
command
The coverage
command is used to generate the test coverage in parallel. It supports all the same options of the run
command (documented below) but it requires at least one of those options to choose the coverage output format:
Option | Description |
---|---|
--html=dir |
Coverage in HTML format, inside the specified directory |
--clover=filename.xml |
Coverage in XML-clover format, with the specified filename |
--xml=dir |
Coverage in PHPUnit XML format, inside the specified directory |
--text=filename.txt |
Coverage in text format, into the specified filename |
--text |
Coverage in text format, printed directly in the console, at the end of the process |
--text-summary=filename.txt |
Coverage summary in text format, into the specified filename |
--text-summary |
Coverage in text format, printed directly in the console, at the end of the process |
--php=filename.php |
Coverage in PHP format, into the specified filename (since 0.8.0) |
--crap4j=filename.xml |
Coverage in Crap4J XML format, into the specified filename (since 0.8.0) |
--cobertura=filename.xml |
Coverage in Cobertura XML format, into the specified filename (since 2.2.0) |
Example:
vendor/bin/paraunit coverage --html=./coverage
Paraunit detects automatically which coverage driver can use to fetch test coverage data; supported drivers are:
Paraunit checks the presence of those drivers, and uses them with that order of preference. Both Xdebug 3 and Pcov can be installed but stay disabled when launching Paraunit, since it will take care on its own to enable them when launching PHPUnit processes. As a last resource, it will use PHPDbg, which should be always available since it’s built into PHP core since 5.6.
Please note that if you want to use Pcov, you’ll need at least PHPUnit 8, or add krakjoe/pcov-clobber
on older PHPUnit versions.
If you have issues or random failures when using the coverage
command, you can try to use the --parallel 1
option: this executes just one test at a time, but you will still benefit from the process splitting, that will avoid any memory issue.
The pipelines
Since version 0.9, Paraunit executes the tests using a pipeline logic: this means that if we ask to run 10 tests in parallel at the same time, Paraunit will instantiate 10 pipeline to do it, and each pipeline will be numbered, from 1 to 10.
The only perceivable difference to the user is the environment variable, called PARAUNIT_PIPELINE_NUMBER
, which is injected in every test process; this variable contains the number of the pipeline. This number can be easily retrieved in your tests, and it can be used to access without concurrency issues to a diverse copy of a resource, i.e. a database, like in this little example:
<?php
use Paraunit\Configuration\EnvVariables;
use PHPUnit\Framework\TestCase;
class SomeTest extends TestCase
{
protected function setup(): void
{
$pipelineNumber = getenv(EnvVariables::PARAUNIT_PIPELINE_NUMBER);
$this->databaseName = 'db_test_' . $pipelineNumber;
// ...
}
}
This little piece of code will obtain db_test_1
, db_test_2
etc. as a value for the databaseName
property, achieving actual separation when accessing the test fixtures in the database. The setup and cleanup of the fixtures after each test is still up to the developer, obviously.
As the snippet shows, the name of the environment variables are available as constants in the Paraunit\Configuration\EnvVariables
class.
Optional arguments and parameters
String filter
Like with PHPUnit, you can run a subset of your tests passing a path as the first argument of the command:
vendor/bin/paraunit run path/to/my/tests
In Paraunit this functionality is more powerful, since:
- it’s case insensitive
- it works in combination with
--testsuite
(PHPUnit ignores that if the argument is provided) - it searches a match everywhere in the filename, so it doesn’t have to be a full or relative path
Let’s use an example to show how powerful this feature is. You are working on the MyApp\SpecialPanel\SomeClass
class, and you want to run all the tests of the MyApp\SpecialPanel
namespace. Those tests are in the tests/Unit/SpecialPanel/
and tests/Functional/SpecialPanel/
directories. You can run both dir at the same time with
vendor/bin/paraunit run specialpanel
You don’t have to bother about the fact that the tests are splitted into different subdirectories, and about the uppercase letters too.
Configuration
If your phpunit.xml.dist
file is not in the default base dir, you can specify it by:
vendor/bin/paraunit run --configuration=relPath/to/phpunit.xml.dist
or with the short version:
vendor/bin/paraunit run -c=relPath/to/phpunit.xml.dist
It’s also possible to provide only a directory, in such case Paraunit will look a file with the default name, phpunit.xml.dist
:
vendor/bin/paraunit run -c=relPath/to/xml/file/
Parallel
You can choose how many concurrent processes (pipelines) you want to spawn at the same time, using the --parallel
option. The default value is 10
:
vendor/bin/paraunit run --parallel=5
Chunk size
NEW: introduced in 1.3.0.
You can choose how many test classes should be executed inside a single concurrent process (pipeline). The default value is 1
, so each pipeline runs a single test class; higher values could benefit in terms of total execution time if your tests have a complex and slow class setup.
vendor/bin/paraunit run --chunk-size=3
Testsuite
You can run a single test suite (as defined in your configuration file) using:
vendor/bin/paraunit run --testsuite=testSuiteName
Pass Through
NEW: introduced in 2.0
If you want to use one of the many native PHPUnit options, you can pass them directly to it using the --pass-through
option. That option can be appended multiple times to pass multiple options; keep in mind that some native options are not usable in this way and will trigger a failure, because they will otherwise interefere with how Paraunit works, or either will not produce any meaningful change.
vendor/bin/paraunit run \
--pass-through="--group=foo" \
--pass-through="--process-isolation"
PHPUnit inherited options
DEPRECATED: no longer present in Paraunit 2.0, use --pass-through
instead.
A large number of PHPUnit options (apart from the aforementioned --testsuite
) are compatible with Paraunit, and they will be passed along to each single PHPUnit spawned process. For a more complete documentation of those options’ behavior, see the PHPUnit CLI documentation.
This is the complete list of supported options:
filter
group
exclude-group
test-suffix
dont-report-useless-tests
strict-coverage
strict-global-state
disallow-test-output
disallow-resource-usage
enforce-time-limit
disallow-todo-tests
process-isolation
globals-backup
static-backup
loader
repeat
printer
bootstrap
no-configuration
no-coverage
no-extensions
include-path
Debug mode
If you have problem running the tests, or the execution stops before the results are printed out, you can launch Paraunit in debug mode, with:
vendor/bin/paraunit run --debug
It will show a verbose output with the full running test queue.
Parsing results
Paraunit prints a parsed result from the single PHPUnit processes. This parsing is done hooking into PHPUnit, so it’s a resilient and reliable process; it allows to be also resilient to fatal errors and other abnormal process termination.
Anyhow, Paraunit doesn’t rely on the parsed results to provide the final exit code; instead, it looks only to the processes’ exit codes: it will return a clean zero exit code only if all the PHPUnit processes gave it a zero exit code. So you can safely use it in your CI build ;)
Side note: if you are using Symfony’s PHPUnit bridge to spot deprecations (or any other plugin that outputs something), you will be able to detect test failures due to deprecations since version 0.11.
Troubleshooting
If you are experiencing any problems, you can try the --debug
option to identify the problematic test, and try running it alone; if failures seems to appear at randoms during Paraunit runs, check for concurrency problem, like database access; otherwise, please open an issue here on GitHub.