Testo: A Standalone PHP Testing and Benchmarking Framework
The new Testo framework is now open for beta testing. It’s a dependency-free implementation — no reliance on PHPUnit — supporting PHP 8.2+, plugins, inline tests, and benchmarks. Designed for mid-to-senior PHP developers, Testo prioritizes flexibility, strict typing, and minimal boilerplate.
Key Differences from PHPUnit and Alternatives
Testo was built to address core limitations in PHPUnit: inflexibility, frequent breaking changes, and stagnation beyond basic PHP version support. The author contributed to PHPUnit’s issue tracker and PRs — but proposed improvements were not merged. PEST and Codeception inherit PHPUnit’s constraints while adding awkward syntax.
Testo is fully independent:
- Avoids
nikic/php-parser, eliminating parser-related conflicts. - Requires PHP 8.2+.
- AI agents can auto-generate tests using
llms.txt— a structured spec derived from official documentation.
A modular plugin system enables per-project customization. Every feature ships as an optional plugin; each test suite selects its own set. For example, the #[Retry] plugin adds flaky-test resilience — implemented via just two lightweight files.
Extend behavior through pipelines, middleware, and events. Write inline tests directly in your src/ code with #[TestInline], and use attributes to embed test logic cleanly.
Installation and Configuration
Install via Composer:
composer require --dev testo/testo
Create testo.php in your project root:
<?php
declare(strict_types=1);
use Testo\Application\Config\ApplicationConfig;
use Testo\Application\Config\SuiteConfig;
return new ApplicationConfig(
suites: [
new SuiteConfig(
name: 'Sources',
location: ['src'],
),
new SuiteConfig(
name: 'Tests',
location: ['tests'],
),
],
);
This file returns an ApplicationConfig instance. Without it, Testo falls back to defaults (e.g., scanning tests/). Two default suites are defined: Sources for inline tests and benchmarks embedded in production code, and Tests for traditional unit tests.
Run tests with ./vendor/bin/testo — or use the official PHPStorm plugin for seamless IDE integration.
Unit Test Examples
No inheritance required. Annotate methods with #[Test]:
final class OrderTest
{
#[Test]
public function calculatesTotal(): void
{
$order = new Order();
$order->addItem('Book', price: 15.0, quantity: 2);
$order->addItem('Pen', price: 3.0, quantity: 5);
Assert::same($order->total(), 45.0);
}
#[Test]
#[DataSet([100.0, 10, 90.0], '10% off')]
#[DataSet([100.0, 0, 100.0], 'no discount')]
#[DataSet([0.0, 50, 0.0], 'zero price')]
public function appliesDiscount(float $price, int $percent, float $expected): void
{
$result = Order::applyDiscount($price, $percent);
Assert::same($result, $expected);
}
#[Test]
#[ExpectException(InsufficientFundsException::class)]
public function cannotOverdraw(): never
{
new Account(balance: 100)->withdraw(200);
}
}
Apply #[Test] to a class to treat all void or never methods as tests. Standalone functions (outside classes) run without lifecycle attributes.
Assertions follow an intuitive order: actual first, expected second. Chainable, type-safe assertions:
Assert::string($email)->contains('@');
Assert::int($age)->greaterThan(0)->lessThan(150);
Assert::array($items)
->hasKeys('id', 'name')
->isList()
->notEmpty();
Assert::json($response->body())
->isObject()
->hasKeys('data', 'meta');
Inline Tests and Benchmarks
Use #[TestInline] inside src/ to verify private or internal methods:
// src/Money.php
final class Money
{
#[TestInline(['price' => 100.0, 'discount' => 0.1, 'tax' => 0.2], 108.0)]
#[TestInline(['price' => 50.0, 'discount' => 0.0, 'tax' => 0.1], 55.0)]
private static function calculateFinalPrice(
float $price,
float $discount,
float $tax,
): float {
return $price * (1 - $discount) * (1 + $tax);
}
}
Benchmark performance with #[Bench]:
#[Bench(
callables: [
'multiply' => 'viaMultiply',
'shift' => 'viaShift',
],
arguments: [1, 5_000],
calls: 2_000_000,
)]
function viaDivision(int $a, int $b): int
{
$d = $b - $a + 1;
return (int) (($d - 1) * $d / 2) + $a * $d;
}
function viaMultiply(int $a, int $b): int
{
$d = $b - $a + 1;
return (int) (($d - 1) * $d * 0.5) + $a * $d;
}
function viaShift(int $a, int $b): int
{
$d = $b - $a + 1;
return ((($d - 1) * $d) >> 1) + $a * $d;
}
Sample output:
+---+----------+-------+---------+------------------+--------+
| # | Name | Iters | Calls | Avg Time | RStDev |
+---+----------+-------+---------+------------------+--------+
| 2 | current | 10 | 2000000 | 75.890µs | ±0.79% |
| 3 | multiply | 10 | 2000000 | 78.821µs (+3.9%) | ±0.47% |
| 1 | shift | 10 | 2000000 | 70.559µs (-7.0%) | ±0.70% |
+---+----------+-------+---------+------------------+--------+
Why It Matters
- A truly standalone framework — zero PHPUnit dependencies, extensible via composable plugins.
- Inline tests (
#[TestInline]) and benchmarks (#[Bench]) live directly alongside production code insrc/. - Type-safe, fluent assertion chains — no boilerplate, no inheritance, no magic globals.
- Configuration via plain PHP files, with full support for multi-suite projects.
- First-class PHPStorm plugin and CLI tooling — ready for daily development.
— Editorial Team
No comments yet.