GeezerVison is showing php-testo.github.io — Testo — Modern PHP Testing Framework
Skip to contentThe PHP Testing
Framework with a
Taste for Khinkali
Built for developers, who crave
bread-and-butter tests
with juicy environment control.
Clean CodeNo TestCase inheritance required — or no classes at all. Proudly PHPBecause it is a backend language, not it(). IDE IntegrationPhpStorm/IntelliJ IDEA plugin with test running, navigation, debugging, and all the workflow you expect. #[Attributes]Instead of magic conventions. Minimum boilerplate, maximum expressiveness. Small CoreEverything else is middleware and event dispatchers with unlimited customization. Well-Designed APISeparate Assert (now) and Expect (later) facades with pipe assertions for type-safe checks. Async FriendlyTest leaks, fibers, and async code with built-in tools. PHP >=8.2Works on all actively supported PHP versions. Well-Designed Assertion API Assertion functions are split into semantic groups: - Assert
\Testo\Assert facade — assertions, executed immediately - Expect
\Testo\Expect facade — expectations, deferred until test completion
Pipe syntax with type grouping keeps code concise and type-safe. phpuse Testo\Assert;
// Pipe assertions — grouped by type
Assert::string($email)->contains('@');
Assert::int($age)->greaterThan(18);
Assert::file('config.php')->exists();
Assert::array($order->items)
->allOf(Item::class)
->hasCount(3);
phpuse Testo\Expect;
// ORM should stay in memory
Expect::leaks($orm);
// Test fails if entities are not cleaned up
Expect::notLeaks(...$entities);
// Output validation
Expect::output()->contains('Done');
php// Have you seen this anywhere else?
Expect::exception(ValidationException::class)
->fromMethod(Service::class, 'validateInput')
->withMessage('Invalid input')
->withPrevious(
WrongTypeException::class,
static fn (ExpectedException $e) => $e
->withCode(42)
->withMessage('Field "age" must be integer.'),
);
php/**
* You can use attributes for exception expectations
*/
#[ExpectException(ValidationException::class)]
public function testInvalidInput(): void
{
$input = ['age' => 'twenty'];
$this->service->validateInput($input);
}
Multiple Ways to Declare Tests Write tests the way that fits your style. - Tests can be classes, functions, or even attributes right in production code (Inline Tests).
- Classes don't need to inherit from a base test class. Code stays clean.
- Test discovery by naming conventions or explicit attributes.
php// Explicit test declaration with #[Test] attribute
final class OrderTest
{
#[Test]
public function createsOrderWithItems(): void
{
$order = new Order();
$order->addItem(new Product('Bread'));
Assert::int($order->itemCount())->equals(1);
}
}
php// Explicit test with #[Test] attribute
// or "test" prefix in function name
#[Test]
function validates_email_format(): void
{
$validator = new EmailValidator();
Assert::true($validator->isValid('[email protected]'));
Assert::false($validator->isValid('invalid'));
}
function testEmailValidator(): void { ... }
php// "Test" suffix on class and "test" prefix on methods
final class UserServiceTest
{
public function testCreatesUser(): void
{
$user = $this->service->create('[email protected]');
Assert::string($user->email)->contains('@');
}
public function testDeletesUser(): void { /* ... */ }
}
php// Test the method right in your code
// Convenient for simple cases
final class Calculator
{
#[TestInline([1, 1], 2)]
#[TestInline([40, 2], 42)]
#[TestInline([-5, 5], 0)]
public function sum(int $a, int $b): int
{
return $a + $b;
}
}
First-Class IDE Integration Native plugin for PhpStorm, IntelliJ IDEA, and OpenIDE. Full-featured workflow: run and re-run from gutter icons, navigation between tests and code, debugging with breakpoints, test generation, results tree. Benchmarks with a Single Attribute php// Baseline method with the #[Bench] attribute
#[Bench(
callables: ['in cycle' => [self::class, 'sumCycle']],
arguments: [1, 5_000],
)]
public static function sumArray(int $a, int $b): int
{
return \array_sum(\range($a, $b));
}
php// Alternative implementation
public static function sumCycle(int $a, int $b): int
{
$result = 0;
for ($i = $a; $i <= $b; ++$i) {
$result += $i;
}
return $result;
}
Results for sumArray:
+--------------------------+------------------------------------------------+---------+
| BENCHMARK SETUP | TIME RESULTS | SUMMARY |
| Name | Iters | Calls | Mean | Median | RStDev | Place |
+----------+-------+-------+-------------------+-------------------+--------+---------+
| current | 10 | 1000 | 11.65µs | 11.58µs | ±1.55% | 1st |
| in cycle | 10 | 1000 | 43.80µs (+275.8%) | 44.17µs (+281.5%) | ±1.67% | 2nd |
+----------+-------+-------+-------------------+-------------------+--------+---------+
|