Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Phpunit Slow Test Detector Laravel Package

ergebnis/phpunit-slow-test-detector

A PHPUnit extension (Composer package and Phar) that detects slow tests and reports those exceeding a configurable maximum duration after your test run.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the extension as a dev dependency and register it in phpunit.xml. There is nothing to write in your test code — the detector hooks into the runner.

composer require --dev ergebnis/phpunit-slow-test-detector

For PHPUnit 10–13 (bootstrap element, PHPUnit autoloads the class via Composer):

<phpunit bootstrap="vendor/autoload.php">
    <extensions>
        <bootstrap class="Ergebnis\PHPUnit\SlowTestDetector\Extension"/>
    </extensions>
</phpunit>

For PHPUnit 7.5–9 use <extensions><extension class="..."/></extensions>; on PHPUnit 6.5 use <listeners><listener class="..."/></listeners>. If you install the PHAR instead of the Composer package, also set extensionsDirectory on the <phpunit> element.

Then just run the suite:

vendor/bin/phpunit

If any test exceeds the default 500 ms threshold, a table of the slowest tests is printed after the summary — sorted by duration descending, capped at 10 rows, with a "There is N additional slow test that is not listed here." footer when truncated. Data-provider rows appear with a #index suffix, which is often the fastest way to spot that only one dataset is slow.

As of 2.27.0 the extension also emits GitHub Actions annotations for slow tests when the suite runs inside GitHub Actions. You get the same information inline on the PR's checks page, next to the failing-file view, with no configuration and no log scraping. Outside GitHub Actions the console table is unchanged.

Where to look first:

  1. The Configuration/Extension/Printer classes under src/ — they show exactly which XML parameters are read, how they are cast, and how the report is rendered.
  2. The README "Configuring the extension" section for your PHPUnit major version, since the parameter syntax changed between 9 and 10.
  3. Your own suite's baseline: temporarily set maximum-duration="1" to confirm the extension is actually wired up (everything will be reported), then restore the real threshold.

The killer first use case: add it to an existing legacy suite, run once, and you immediately get a ranked list of the 10 tests that dominate CI wall-clock time — usually a handful of tests with sleep(), real HTTP calls, or un-batched database fixtures.

Implementation Patterns

Tune the threshold per suite, not globally. Keep a fast phpunit.xml.dist for unit tests with a low bar (maximum-duration="100") so regressions are caught early, and a separate config for integration/e2e suites with a realistic budget (maximum-duration="2000"). A single shared threshold is either noisy on the fast suite or blind on the slow one.

<extensions>
    <bootstrap class="Ergebnis\PHPUnit\SlowTestDetector\Extension">
        <parameter name="maximum-duration" value="100"/>
        <parameter name="maximum-count" value="25"/>
    </bootstrap>
</extensions>

Raise maximum-count in CI, keep it low locally. Ten rows is fine for a developer's terminal; in CI you want the full picture, so set maximum-count to 50 and diff the list between the base branch and the PR branch.

Let GitHub Actions do the surfacing. On a GitHub runner the extension now writes annotations directly, so you no longer need to tee the log and grep it just to make slow tests visible in a PR. A slow test shows up as an annotation on the workflow run, which is exactly where a reviewer is already looking. Keep the table in mind for local work and let annotations do the CI job.

Treat it as a reporting layer, not a gate. Neither the table nor the annotations fail the run — the exit code of an "OK" run stays 0. If you need enforcement, act on the output yourself, or better, on machine-readable data:

vendor/bin/phpunit --log-junit build/junit.xml
# then read the per-test `time` attributes and compare against a threshold

Parsing the human-readable table still works, but it is more fragile than JUnit XML: 2.27.0 changed the report's formatting (column wrapping, a trailing line break), so a regex tuned against the old output can silently stop matching. Prefer --log-junit for any automated check.

Use it as a refactoring loop. Before touching a slow test, run only that suite/config and note the duration; after converting an HTTP call to a fake or a sleep() to a clock abstraction, re-run and confirm the row disappears from the table and from the annotations. Because the report is sorted descending, the top row is your highest-leverage fix.

Watch the data-provider indices. A row like SomeTest::testX with data set #7 at 1.4 s while #0–#6 are all fast almost always means that dataset hits the network, the filesystem, or an unindexed query. That is a much more precise signal than a bare "test is slow" warning.

Keep the report off stdout if you parse it. On PHPUnit 10+ the extension automatically respects PHPUnit's own stderr configuration attribute, so <phpunit stderr="true"> routes the table to stderr and keeps stdout clean for machine-readable output. On PHPUnit 6.5–9 you must set the extension's stderr parameter explicitly.

Compare across branches, not across machines. Durations are wall-clock and vary with hardware, container CPU quotas, and cold caches. Use the table as a relative ranking ("this test is 10× the median") and leave generous headroom in absolute thresholds; don't chase a 480 ms test against a 500 ms bar on shared CI runners.

Custom reporting. 2.27.0 extracted a Printer abstraction and routes the console report through it, alongside the existing Extension/Facade event subscription. If you want durations in Datadog or in a PR comment of your own, write a small extension subscribing to the same test-finished events, or adapt the printer if it is part of the public surface — both are more robust than regexing console output that changes shape between releases. Check the class for an @internal marker before depending on it directly.

Gotchas and Tips

  • maximum-duration is an integer number of milliseconds, not seconds and not a float. 0.5 will not do what you mean; 500 is half a second.
  • The extension never fails the build. GitHub annotations are warnings, not failures. If you expected a non-zero exit code from "Detected N slow tests", you will not get one.
  • Annotations only appear under GitHub Actions. Locally, and on GitLab/Buildkite/other runners, you get the console table only. Do not build a CI check that assumes annotations exist on every provider.
  • Nothing prints if the run aborts. With --stop-on-failure, a fatal error, or a crash in a bootstrap, you may never see the table (or the annotations), because the report is emitted at the end of the run.
  • Element name differs by PHPUnit major. <bootstrap class> (10–13) vs <extension class> (7.5–9) vs <listener class> (6.5) vs PHAR setups that additionally need extensionsDirectory. Copying a snippet from the wrong PHPUnit version produces a silently ignored extension or an XML validation error against phpunit.xsd.
  • Minimum PHP is now 7.4. Support for PHP 7.0, 7.1, 7.2 and 7.3 was dropped during the 2.27.0 cycle. The PHPUnit 6.5/7.5 configuration forms still parse, but they only make sense on a PHP 7.4 runtime. If you are stuck on an older PHP, stay on the pre-2.27 release line and pin it.
  • Extension parameters live inside the extension element, not as top-level <phpunit> attributes and not as <php> settings. Putting <parameter> in the wrong place is the single most common configuration error.
  • stderr parameter is ignored on PHPUnit 10+. Set PHPUnit's own stderr attribute instead; the extension reads it from the runner configuration.
  • Report formatting is not a stable contract. 2.27.0 wrapped wide tables and added a trailing line break. Anything that scrapes the table — including greps in older CI recipes — should be re-tested or moved to --log-junit.
  • Verify registration before debugging behaviour. Temporarily set maximum-duration="1" and run one test. If you get no table, the extension is not registered (wrong class name, missing <extensions> block, or autoloader not loaded because bootstrap="vendor/autoload.php" is missing).
  • Data-provider rows are counted individually, so a single test with 100 datasets can flood maximum-count. Raise the count deliberately or filter with --filter when investigating one test.
  • PHAR installs need the class to be loadable from extensionsDirectory. If you mix Composer install and PHAR, you can end up with two copies of the extension registered and duplicate output.
  • setUpBeforeClass/static warmup time is not what you're optimising for. The detector reports per-test durations; a suite that is slow overall because of a heavy bootstrap will show few or no slow tests. Profile at the process level for that case.
  • Pin the package in lockstep with your PHPUnit major. The supported range spans PHPUnit 6.5 through 13, but a given release tracks a specific runner API (and now a specific PHP floor); let Composer resolve and avoid --with-all-dependencies jumps that cross PHPUnit majors without re-checking the extension config syntax.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo
bernskioldmedia/laravel-harvest
bernskiold/laravel-data-scrubber
bernskiold/laravel-currency-converter
bernskiold/laravel-campaign-monitor
bernskioldmedia/laravel-autotranslate
siberfx/telegram-bot
roy-sandip/laravel-view-extended
gregsphotography/site-builder-core
roy-sandip/laravel-view
anodyne/blade-fluentui-icons
webx-ui/module-catalog-collections