boundwize / pyrameter
PHPUnit extension that measures the shape of your test pyramid.
Fund package maintenance!
Requires
- php: ^8.2
- nikic/php-parser: ^5.0
- phpunit/phpunit: ^11.0 || ^12.0
Requires (Dev)
- boundwize/structarmed: ^0.13.4
- laminas/laminas-coding-standard: ^3.1
- phpstan/phpstan: ^2.2
- rector/rector: ^2.4
This package is auto-updated.
Last update: 2026-06-15 00:28:31 UTC
README
Keep your PHPUnit test suite shaped like a pyramid.
Pyrameter is a PHPUnit extension that shows what your test suite is becoming. It classifies each test by the classes and namespaces the test file consumes, then prints a shape report after PHPUnit runs.
Use it to spot a suite that is getting heavier, agree on what "healthy" means for your project, and optionally fail CI when the pyramid drifts too far.
vendor/bin/phpunit ........................ Pyrameter ========= Shape: Integration Mountain Result: Violated ⚠ Kind Tests Actual Target Status Unit 39 65.0% >= 70.0% ✗ Functional 10 16.7% <= 20.0% ✓ Integration 9 15.0% <= 8.0% ✗ E2E 1 1.7% <= 2.0% ✓ Unknown 1 1.7% <= 2.0% ✓ Total: 60 tests Your suite is getting heavier.
How it works
Pyrameter does not trust test directories, and it does not scan production classes. Instead, it classifies by configured class or namespace usage in test files:
- no configured heavy usage => unit
- framework test runtime => functional
- real resource boundary, such as database, cache, queue, filesystem, or external service => integration
- browser driver usage => e2e
When multiple usages match, the heaviest kind wins. Mocked heavy dependencies stay unit.
Your pyramid, your rules: decide which class usage means functional or integration in your project, then configure Pyrameter to match your team's belief.
For example, if a test consumes an analyser that reads real paths, configure that analyser class or namespace as integration.
Quick start
Pyrameter supports PHP 8.2+ and PHPUnit 11 or 12.
Install it as a dev dependency:
composer require --dev boundwize/pyrameter
Register the PHPUnit extension:
<extensions> <bootstrap class="Pyrameter\Extension"> <parameter name="config" value="pyrameter.php"/> </bootstrap> </extensions>
Run PHPUnit as usual:
vendor/bin/phpunit
If the config parameter is omitted, Pyrameter looks for pyrameter.php in the current working directory. If the file does not exist, it uses the default rules and target shape.
Configure
Create pyrameter.php when you want to tune the rules or targets.
Start with defaults() to keep Pyrameter's built-in rules for PDO, mysqli, Doctrine, Redis, Symfony functional tests, Panther, and WebDriver, then add your project-specific beliefs:
<?php declare(strict_types=1); use Pyrameter\Config\PyrameterConfig; use Pyrameter\TestKind; return PyrameterConfig::defaults() ->usesClass(App\Analyser\Analyser::class, TestKind::Integration) ->usesNamespace('App\Tests\Browser\\', TestKind::E2E) ->targetShape( unit: ['min' => 75], functional: ['max' => 15], integration: ['max' => 7], e2e: ['max' => 2], unknown: ['max' => 1], );
Use create() when you want full control. It starts with no usage rules and no target shape:
<?php declare(strict_types=1); use Pyrameter\Config\PyrameterConfig; use Pyrameter\TestKind; return PyrameterConfig::create() ->usesClass(PDO::class, TestKind::Integration) ->usesNamespace('Doctrine\DBAL\\', TestKind::Integration) ->usesNamespace('Symfony\Bundle\FrameworkBundle\Test\\', TestKind::Functional) ->usesNamespace('Symfony\Component\Panther\\', TestKind::E2E) ->usesNamespace('Facebook\WebDriver\\', TestKind::E2E) ->targetShape( unit: ['min' => 70], functional: ['max' => 18], integration: ['max' => 8], e2e: ['max' => 2], unknown: ['max' => 2], );
Targets are percentage ranges. Missing min means 0; missing max means 100. When targetShape() is called, missing kinds default to ['min' => 0, 'max' => 100], which Pyrameter reports as no target.
Fail CI
By default, Pyrameter is report-only. It prints target violations without changing PHPUnit's exit code.
Turn violations into a failing PHPUnit process when you are ready to enforce the shape:
return PyrameterConfig::defaults() ->failOnViolation();
A note on taxonomy
Pyrameter measures suite shape from static usage rules in test files. It is a useful pressure gauge, not a perfect taxonomy judge.
