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.6% <= 2.0% β
Total: 60 tests
Your suite is getting heavier.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.
Pyrameter supports PHP 8.2+ and PHPUnit 11 or 12.
Install it as a dev dependency:
composer require --dev boundwize/pyrameterRegister the PHPUnit extension:
<extensions>
<bootstrap class="Boundwize\Pyrameter\Extension"/>
</extensions>Run PHPUnit as usual:
vendor/bin/phpunitIf 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.
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 Boundwize\Pyrameter\Config\PyrameterConfig;
use Boundwize\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 Boundwize\Pyrameter\Config\PyrameterConfig;
use Boundwize\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.
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();Pyrameter measures suite shape from static usage rules in test files. It is a useful pressure gauge, not a perfect taxonomy judge.
