Search by

lesterkaufungen / suggestedtests

lesterkaufungen

PHPUnit helpers for PHP 7.4+: performance and memory budgets backed by a native OS bridge (ext-ffi), eventually, given/when/then steps, spies, snapshots and seeded test data.

Package info

github.com/lesterkaufungen/suggestedtests-php

Homepage

Issues

pkg:composer/lesterkaufungen/suggestedtests

Statistics

Installs: 8

Dependents: 0

Suggesters: 0

Stars: 0

v0.1.2 2026-10-21 13:50 UTC

This package is auto-updated.

Last update: 2026-10-21 13:51:46 UTC


README

suggestedtests logo

suggestedtests for PHP

PHPUnit helpers that make tests faster to write, and keep your code fast.

Tokens an AI coding agent spends per test, hand-written vs with suggestedtests, projected to larger test suites (estimate)

Packagist PHP 7.4+ PHPUnit 9.6 to 12 no dependencies ~56% fewer tokens for AI agents (estimate) MIT license

The same helpers in Flutter & Dart · Python · JS & TS · PHP · Java · Go · Swift · one repo, one native bridge

Helper What it's for
assertPerformance, Benchmark Timing statistics and performance budgets, on wall-clock or CPU time.
assertMemory, Memory PHP heap budgets, and process RSS via a native bridge (ext-ffi) for Windows, Linux and macOS.
SystemProbe Process CPU time, thread CPU time, current and peak RSS, CPU count.
assertEventually, Eventually Polling assertions. Use them instead of sleep().
scenario, Steps Given/when/then tests whose failure names the failing step.
Spy + assertSpyCalled… Spies for closures and callables. Arguments can be PHPUnit constraints.
assertMatchesSnapshot Snapshot files next to the test, updated with one environment variable.
testData(), TestData Seeded, reproducible fake data. A failure prints the seed.
temporaryDirectory(), Sandbox Self-cleaning temp dirs, and temporary env/ini settings and output capture that are always restored.
assertUniqueElements Duplicate check that names the keys.

Built for AI coding agents

An agent that knows these helpers writes one call where it would otherwise write the loop, the timer, the statistics and the failure message, and then read all of it again on every later change. For the tests in the benchmark, that is 676 → 296 tokens (56% fewer):

Test Hand-written With suggestedtests Saved
Performance budget (without, with) 219 76 65%
Wait for a condition (without, with) 119 69 42%
Peak-RSS budget (without, with) 130 89 32%
Temporary env var (without, with) 208 62 70%

Estimated with the public o200k_base tokenizer: other models count differently, so compare the ratios, not the numbers. Reasoning and retries aren't counted, and the hand-written versions are often weaker (Linux-only memory reads, no seed in the failure).

Tell your agent the helpers exist. Paste this into CLAUDE.md, AGENTS.md or your editor's rules (212 tokens of context, paid back after about 3 tests):

## Tests: use `suggestedtests` (`use SuggestedTests\PHPUnit\SuggestedTests;` in the test class)

- Timing budgets: `$this->assertPerformance(fn () => ..., budgetMs: 5)`; statistics: `Benchmark::run('name', fn () => ...)`.
- Never `sleep()`: `$this->assertEventually(fn () => $this->assert...(...))`.
- Memory: `$this->assertMemory(fn () => ..., maxHeapGrowth: 1 * Memory::MiB)` or `maxPeakGrowth:` for RSS.
- Isolation: `Sandbox::withEnv([...], fn)`, `Sandbox::withIni([...], fn)`, `Sandbox::captureOutput(fn)`, `$this->temporaryDirectory()`.
- Given/when/then: `$this->scenario(function (Steps $s) {...})`; spies: `new Spy()` + `assertSpyCalledWith`; snapshots: `assertMatchesSnapshot`.
- Seeded data: `$this->testData()`; duplicates: `assertUniqueElements`.

Install

composer require --dev lesterkaufungen/suggestedtests

Requires PHP 7.4+ (64-bit for TestData) and, for the trait, PHPUnit 9.6 to 12. The native bridge needs ext-ffi, which is enabled for the CLI by default (ffi.enable=preload). Without it, everything except RSS and CPU time keeps working.

The examples use named arguments, which need PHP 8. On PHP 7.4, pass the same arguments by position: $this->assertPerformance($fn, 5).

use PHPUnit\Framework\TestCase;
use SuggestedTests\PHPUnit\SuggestedTests;

final class CartTest extends TestCase
{
    use SuggestedTests;
}

Performance budgets

public function testParsingStaysFast(): void
{
    $this->assertPerformance(fn () => $parser->parse($bigJson), budgetMs: 5);
}

public function testReport(): void
{
    echo Benchmark::run('sort', fn () => sort($data), iterations: 50);
    // sort: n=50 min=1.02ms median=1.10ms mean=1.15ms p95=1.40ms max=2.01ms ±0.12ms
}

The budget is checked against the median by default, which is robust against GC pauses. Pass statistic: Statistic::P95 (or Min, Mean, Max) to check something else. Pass clock: Clock::Cpu to measure CPU time, which ignores sleep, I/O waits and other processes.

Memory budgets

$this->assertMemory(fn () => $importer->run($file), maxHeapGrowth: 1 * Memory::MiB);
$this->assertMemory(fn () => $image->resize(4000, 4000), maxPeakGrowth: 64 * Memory::MiB);

maxHeapGrowth and maxHeapPeakGrowth use PHP's own allocator (memory_get_usage). They are exact and always available. maxHeapPeakGrowth needs PHP 8.2+ (memory_reset_peak_usage). maxRssGrowth and maxPeakGrowth use the native bridge. They see memory that PHP's heap doesn't, such as GD and Imagick images, libxml documents and FFI buffers.

Native bridge (Windows / Linux / macOS)

SystemProbe::processCpuTime();  // CpuTime(user: 1.25s, system: 80.00ms)
SystemProbe::threadCpuTimeNs();
SystemProbe::memoryUsage();     // MemoryUsage(current: 40.1 MiB, peak: 52.3 MiB)
SystemProbe::cpuCount();

You don't need Rust or a compiler. The package ships the bridge prebuilt for Linux (glibc ≥ 2.17 and musl; x64, arm64, x86, ARMv7 and RISC-V 64), macOS (Intel and Apple Silicon) and Windows (x64, arm64, x86), and Android (Termux). It is loaded with FFI::cdef. To use your own build, point $SUGGESTEDTESTS_NATIVE_LIB at it. Call $this->requireNativeBridge() to skip a test, with the reason, where the bridge is unavailable.

Startup report

The bridge can describe the machine the tests run on: OS, architecture, C library, container, VM and machine kind, as three suggestedtests: ... lines on stderr. It is off by default, so nothing is printed. Turn it on for the whole run, which prints it as the bridge loads:

SUGGESTEDTESTS_VERBOSE=1 vendor/bin/phpunit

Composer's autoloader loads the bridge (on the command line), so the report appears in every PHPUnit run, even one that never calls a native helper. Or turn it on from code, which prints it then (at most once per process):

SystemProbe::setVerbose(true);
suggestedtests: native bridge loaded (ABI v1)
suggestedtests: platform: linux x86_64 glibc 2.39, 64-bit, os 6.8.0-45-generic
suggestedtests: environment: server, container docker, vm kvm (KVMKVMKVM), flags: ci

This flag controls nothing else.

assertEventually: no more fixed sleeps

$this->assertEventually(fn () => $this->assertFileExists($exportPath));
$job = $this->assertEventually(function () use ($queue) {
    $this->assertNotNull($job = $queue->finished());
    return $job;
}, timeoutMs: 10_000, intervalMs: 100);

Given / when / then

$this->scenario(function (Steps $s) {
    $cart = null;
    $s->given('an empty cart', function () use (&$cart) { $cart = new Cart(); });
    $s->when('two items are added', function () use (&$cart) { $cart->add(10)->add(20); });
    $s->then('the total is 30', function () use (&$cart) { $this->assertSame(30, $cart->total()); });
});
// Then: the total is 30
// Failed asserting that 20 is identical to 30.

The failure keeps its original class, so PHPUnit still reports a failure (with its diff), not an error. Only the step is prepended to the message.

Spies

$onSaved = new Spy();                     // or new Spy(fn (User $u) => $u->id)
$form->onSaved($onSaved);
$form->submit();

$this->assertSpyCalledTimes($onSaved, 1);
$this->assertSpyCalledWith($onSaved, [$this->isInstanceOf(User::class)]);
$onSaved->lastCall();                     // [User]

Snapshots

$this->assertMatchesSnapshot($renderer->render($invoice));
$this->assertMatchesSnapshot($api->toArray()); // arrays and objects are stored as JSON

The first run writes __snapshots__/<TestClass>/<testName>.txt next to the test. In CI (when $CI is set), a missing snapshot fails instead of being written. To accept intentional changes, run with SUGGESTEDTESTS_UPDATE_SNAPSHOTS=1.

Reproducible test data

$data = $this->testData();
$user = new User(name: $data->word(), email: $data->email(), age: $data->integer(18, 90));

Each test gets a fresh seed. When a test fails, the seed is printed (suggestedtests: CartTest::testCheckout used TestData seed 1804289383; reproduce with SUGGESTEDTESTS_SEED=1804289383). Use new TestData(42) directly anywhere else.

Sandbox

Sandbox::withEnv(['APP_ENV' => 'production'], fn () => $this->assertTrue(isProduction()));
Sandbox::withIni(['precision' => 5], fn () => ...);
$html = Sandbox::captureOutput(fn () => $view->render());
$dir = $this->temporaryDirectory(); // removed after the test

Example

See examples/ExampleTest.php. Run it with vendor/bin/phpunit examples.

License

MIT