boundwize/pyrameter

PHPUnit extension that measures the shape of your test pyramid.

Maintainers

Package info

github.com/boundwize/pyrameter

pkg:composer/boundwize/pyrameter

Fund package maintenance!

samsonasik

Statistics

Installs: 49

Dependents: 1

Suggesters: 0

Stars: 1

Open Issues: 0

dev-main 2026-06-15 00:28 UTC

This package is auto-updated.

Last update: 2026-06-15 00:28:31 UTC


README

Pyrameter

Keep your PHPUnit test suite shaped like a pyramid.

Latest Version ci build Code Coverage PHPStan Downloads

Windows macOS Linux

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.