robiningelbrecht/phpunit-pretty-print
A PHPUnit result printer that transforms default test output into clean, colorful, readable reports. See progress, failures, and timing at a glance, with less noise and more useful detail during every test run. Easy to install and configure via phpunit.xml.
Install as a dev dependency:
composer require --dev robiningelbrecht/phpunit-pretty-print
Register the listener/printer in your phpunit.xml (or phpunit.xml.dist) so every developer and CI run gets it automatically:
<phpunit ...>
<!-- PHPUnit 9 style -->
<listeners>
<listener class="RobinIngelbrecht\PHPUnitPrettyPrint\PrettyPrintTestListener" />
</listeners>
</phpunit>
Then run tests as usual:
vendor/bin/phpunit
Where to look first:
phpunit.xml snippet — that is the entire integration surface. There is no service provider, no facade, no config file.composer.json of the package — check the phpunit/phpunit version constraint, because the TestListener API was removed in PHPUnit 10 and replaced by the event system. The version of this package you install must match your PHPUnit major version.phpunit.xml — confirm whether you have a committed config or rely on PHPUnit defaults (if there is no config file, the listener is never loaded and you will see stock output with no error).First use case: you have a plain PHP package or a non-Laravel project using raw PHPUnit, you are jealous of Laravel/Pest's Collision output, and you want the same colored failure panels, diffs, and stack-trace rendering without adopting Laravel or Pest. Drop in the listener, run a failing test, and you should see Collision-style output instead of PHPUnit's terse Failed asserting that....
Sanity check: temporarily break an assertion and confirm you get a styled failure panel. If you get the normal PHPUnit output, the listener did not load — the config file is not being picked up, or the class name/version is wrong.
Treat the config as the deliverable. The package is a one-line integration. Commit the listener block in phpunit.xml.dist so new clones and CI inherit it. If individual developers want to opt out locally, they can use a personal phpunit.xml that overrides the dist file rather than adding conditionals.
Pair it with an explicit colors flag in CI. Collision decides whether to emit ANSI escapes based on TTY detection. GitHub Actions, GitLab, and most CI runners are not TTYs, so you will get either plain output or raw escape codes depending on the environment variable you set:
vendor/bin/phpunit --colors=always # or FORCE_COLOR=1
Keep this in the CI script, not in phpunit.xml, so local and CI behavior can diverge intentionally.
Use it as the replacement for --testdox-style readability, not alongside it. Collision already renders a structured summary at the end of the run. If you also pass --testdox or --printer, you will often get overlapping summaries. Pick one presentation mode: either Collision output for full-suite runs, or testdox for a quick human-readable list when debugging a single file.
Ordering with other listeners. If you already have listeners in phpunit.xml (coverage reporters, custom loggers, JUnit writers), the pretty printer generally belongs last so its summary is the final thing flushed to the terminal. Listeners registered earlier that write to stdout can interleave with Collision's panels — put file-writing reporters first and stdout-writing ones last.
Parallel testing (Paratest / PHPUnit's built-in --parallel). Each worker process generates its own listener output. This is the main pattern where the package earns its keep and where you should validate behavior: run a small suite in parallel once and confirm the aggregated output is readable. If worker output interleaves badly, fall back to single-process output in CI and keep parallel for local speed runs, or use Paratest's own output aggregation and accept less pretty formatting there.
Scope it per project, not globally. Because there is no global config, this is naturally per-repo. That is good: monorepo packages that already use Pest (which bundles Collision) should not add this listener — Pest runs its own pretty printer and you will double-print summaries.
Pair with a static-analysis/pre-commit flow. A typical day-to-day workflow looks like: vendor/bin/phpunit for the fast local loop (pretty output shines here on failures), then --coverage-text or a JUnit logger in CI where machine-readable output matters. The listener does not interfere with exit codes or coverage, so you can layer it freely.
The version matrix is the real pitfall. PHPUnit 9 used TestListener; PHPUnit 10+ uses the event/subscriber system and removed the listener interface entirely. If you install this package into a PHPUnit 10/11/12 project and get "class not found" or "does not implement TestListener," you have the wrong major version of the package. Pin the package version to match your PHPUnit major in composer.json, and let Renovate/Dependabot bump both together — not independently.
Silent no-op when the config is not loaded. If vendor/bin/phpunit is run from a directory where phpunit.xml is not found, or you pass --no-configuration, the listener is never registered and PHPUnit prints normally with no warning. Verify with vendor/bin/phpunit --debug | head that your config is being used before blaming the package.
Duplicate output. PHPUnit still runs its built-in printer alongside listeners in some versions, which can yield both the default ./F progress and Collision's summary. If you see doubled output, that is a PHPUnit/package version interaction, not a misconfiguration — check the README for the version-specific setup (some versions want a printer override instead of a listener, or an environment variable to suppress the default).
Raw escape codes in logs. Redirecting output to a file (> test.log) or piping through a CI log collector can capture ANSI sequences literally. Either force --colors=never when writing logs, or strip with sed 's/\x1b\[[0-9;]*m//g'. Collision usually detects non-TTY and disables colors, but FORCE_COLOR and CI env vars override that — do not set FORCE_COLOR=1 in a job that also archives output.
Editor links. Collision can make stack-trace file paths clickable in supported terminals if it can resolve an editor (--editor=vscode and similar). This is a genuine quality-of-life win on failures — worth configuring locally in a shell alias: alias pu='vendor/bin/phpunit --editor=phpstorm'.
Windows / older terminals. ANSI rendering depends on the terminal. Windows Terminal and modern VS Code terminal are fine; legacy cmd.exe will show escape garbage. Collision falls back, but if a Windows developer reports unreadable CI logs, check the terminal before suspecting the package.
Do not wrap it in a Laravel service provider. Laravel already ships Collision via nunomaduro/collision (the dependency this package wraps). Adding this to a Laravel app is redundant and can conflict with the framework's own exception rendering. This package's niche is non-Laravel PHPUnit projects and standalone packages.
Exit codes and CI gating are untouched. The listener only changes formatting; 0/1/2 semantics and failure counts still come from PHPUnit. If a build passes when it should fail, the problem is in your test suite or CI script, not here — do not debug it through the formatter.
Debugging a broken listener. Run vendor/bin/phpunit --debug to see PHPUnit's internal event log; if the listener class is missing or throws, it typically surfaces there. Also check composer dump-autoload after install — a stale autoloader will make the class unresolvable in the config with a confusing error.
Extension points are essentially zero. There is no config file, no hooks, no plugin API. If you need custom formatting (JUnit XML, JSON, TAP), that belongs in a separate listener or --log-junit; do not try to bend this package into a reporting tool.
How can I help you explore Laravel packages today?