Back to Home

Testo framework: PHP tests and benchmarks

Testo — independent framework for unit testing and benchmarks in PHP 8.2+. Supports plugins, inline tests in src, typed Assert. Installation via Composer, code examples for middle/senior developers.

Testo: new testing framework for PHP 8.2+
Advertisement 728x90

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:

Google AdInline article slot
  • 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:

Google AdInline article slot
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.

Google AdInline article slot

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 in src/.
  • 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

Advertisement 728x90

Read Next