Skip to content

Latest commit

 

History

History
303 lines (228 loc) · 6.97 KB

File metadata and controls

303 lines (228 loc) · 6.97 KB
layout title description image llms hero features
home
Testo — Modern PHP Testing Framework
PHP testing framework without TestCase inheritance. Clean OOP, middleware architecture, PSR-14 events, Assert/Expect facades.
/images/og-image.jpg
false
name text tagline image actions
The PHP Testing Framework with a Taste for Khinkali
Built for developers, who crave bread-and-butter tests with juicy environment control.
src alt
/logo-full.svg
Testo Logo
theme text link
brand
Get Started
/docs/getting-started
theme text link
alt
GitHub
title details
Familiar OOP
Tests are classes without TestCase inheritance or functions. Code stays clean.
title details
IDE Integration
PhpStorm/IntelliJ IDEA plugin with test running, navigation, debugging, and all the workflow you expect.
title details
Small Core
Everything else is middleware and event dispatchers with unlimited customization.
title details
Well-Designed API
Separate Assert (now) and Expect (later) facades with pipe assertions for type-safe checks.
<script setup> import { data as jbPlugin } from './.vitepress/theme/jetbrains-plugin.data' const ideScreenshot = { light: '/images/ide-screenshot-light.jpg', dark: '/images/ide-screenshot-dark.jpg' } const assertTabs = [ { name: 'Assert.php', slot: 'assert', icon: 'testo' }, { name: 'Expect.php', slot: 'expect', icon: 'testo' }, { name: 'Exception.php', slot: 'expectException', icon: 'testo' }, { name: 'Attributes.php', slot: 'expectAttr', icon: 'testo-class' }, ] const declareTabs = [ { name: 'Class', slot: 'declare-class', icon: 'testo-class' }, { name: 'Function', slot: 'declare-function', icon: 'testo-function' }, { name: 'Convention', slot: 'declare-convention', icon: 'testo-class' }, { name: 'Inline', slot: 'declare-inline', icon: 'class' }, ] </script>

::: warning 🚧 Work in Progress Testo is still under active development and not ready for production use. Feel free to explore and experiment, but don't rely on it for real projects yet.

Want to support the project? Star the repo or become a sponsor. :::

Well-Designed Assertion API

Assertion functions are split into semantic groups:

  • Assert:: facade — assertions, executed immediately
  • Expect:: facade — expectations, deferred until test completion

Pipe syntax with type grouping keeps code concise and type-safe.

<template #assert>

use 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);

<template #expect>

use 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');

<template #expectException>

// 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.'),
    );

<template #expectAttr>

/**
 * 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.

<template #declare-class>

// 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);
    }
}

<template #declare-function>

// Explicit test with #[Test] attribute
// or "test" prefix in function name

#[Test]
function validates_email_format(): void
{
    $validator = new EmailValidator();

    Assert::true($validator->isValid('user@example.com'));
    Assert::false($validator->isValid('invalid'));
}

function testEmailValidator(): void { ... }

<template #declare-convention>

// "Test" suffix on class and "test" prefix on methods

final class UserServiceTest
{
    public function testCreatesUser(): void
    {
        $user = $this->service->create('john@example.com');

        Assert::string($user->email)->contains('@');
    }

    public function testDeletesUser(): void { /* ... */ }
}

<template #declare-inline>

// 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 and IntelliJ IDEA.

Full-featured workflow: run and re-run from gutter icons, navigation between tests and code, debugging with breakpoints, test generation, results tree.