617 words, 4 min read

Running Laravel's test suite in parallel speeds things up considerably, but it also makes it easy to miss PHPUnit notices. The parallel worker output gets interleaved and buffered, and notices about deprecated API usage or risky tests tend to scroll past unnoticed — or disappear entirely. This post shows the command I use to surface them reliably.

The command

LARAVEL_PARALLEL_TESTING=1 \
LARAVEL_PARALLEL_TESTING_RECREATE_DATABASES=1 \
vendor/brianium/paratest/bin/paratest \
--colors=always \
--configuration=/path/to/phpunit.xml \
--runner=\\Illuminate\\Testing\\ParallelRunner \
--display-phpunit-notices \
--fail-on-all-issues \
tests/Unit

What each part does

Environment variables

LARAVEL_PARALLEL_TESTING=1 activates Laravel's parallel testing support. It causes the framework to spin up separate database connections per worker (suffixed _1, _2, etc.) and seed each one independently.

LARAVEL_PARALLEL_TESTING_RECREATE_DATABASES=1 forces those databases to be dropped and recreated from scratch on every run. Without it, a previous run's leftover state can cause tests to pass or fail for the wrong reasons — particularly relevant when you change a migration between runs.

Invoking paratest directly

Laravel's php artisan test --parallel is a thin wrapper around brianium/paratest. Calling the binary directly gives you access to flags that the Artisan wrapper doesn't expose, particularly the notice-related ones below.

--runner=\\Illuminate\\Testing\\ParallelRunner tells paratest to use Laravel's own runner class, which handles the database token injection and other framework-specific setup that the default runner skips.

--configuration takes an absolute path to your phpunit.xml. When you invoke paratest from outside the project root — for example, from a CI script or a Makefile — relative paths silently resolve to the wrong location. Using an absolute path avoids that class of silent misconfiguration.

Surfacing notices

--display-phpunit-notices is the key flag. PHPUnit emits notices for things like:

  • calls to deprecated assertion methods (assertContains on a string instead of assertStringContainsString)
  • tests marked @covers that cover no code
  • tests with no assertions when beStrictAboutTestsThatDoNotTestAnything is enabled

In a parallel run these notices are buffered per worker and often never reach the terminal. This flag ensures they are printed to output regardless.

--fail-on-all-issues treats any notice, warning, or deprecation as a test suite failure. This is what makes the command useful for CI: the exit code becomes non-zero the moment any worker emits a notice, so the pipeline fails and forces you to deal with it rather than letting it accumulate.

Scoping to tests/Unit

Passing tests/Unit as the path restricts the run to unit tests, which tends to surface notices faster than a full suite run because unit tests don't need a running server or real database queries. Once you've cleared the unit test notices, you can repeat with tests/Feature or omit the path entirely.

Reading the output

When a notice fires, paratest prints it alongside the failing worker output. The format looks like:

NOTICE tests/Unit/Services/OrderServiceTest.php:42
Method Illuminate\Testing\Assert::assertContains() is deprecated. Use assertStringContainsString() instead.

The file path and line number point directly to the test method that triggered it. If you see the same notice repeating across many tests, the issue is usually in a shared base class or a trait — check the stack trace for the actual call site rather than fixing every test individually.

Making it a habit

The goal is to run with --fail-on-all-issues in CI from the start, before notices accumulate. If you're adding this to an existing codebase, it's usually easier to tackle notices test-file by test-file: run against a single file first, fix what you find, then broaden the path incrementally.

# Start with one file
LARAVEL_PARALLEL_TESTING=1 \
vendor/brianium/paratest/bin/paratest \
--runner=\\Illuminate\\Testing\\ParallelRunner \
--display-phpunit-notices \
--fail-on-all-issues \
tests/Unit/Services/OrderServiceTest.php

Once the file is clean, commit and move on to the next. The --fail-on-all-issues flag acts as a ratchet: once a file is notice-free, CI will catch any regression immediately.