Testo : un framework PHP autonome pour les tests et les benchmarks
Le nouveau framework Testo est désormais ouvert aux tests bêta. Il s’agit d’une implémentation indépendante — sans dépendance à PHPUnit — compatible avec PHP 8.2+, dotée de plugins, de tests intégrés (inline tests) et de fonctionnalités de benchmarking. Conçu pour les développeurs PHP confirmés, Testo privilégie la flexibilité, le typage strict et la suppression du code répétitif.
Principales différences avec PHPUnit et ses alternatives
Testo a été conçu pour pallier les limites fondamentales de PHPUnit : manque de souplesse, ruptures fréquentes entre versions et stagnation au-delà du simple support des versions récentes de PHP. Son auteur a contribué au suivi des bogues et aux pull requests de PHPUnit — mais les améliorations proposées n’ont pas été intégrées. PEST et Codeception héritent des contraintes de PHPUnit tout en ajoutant une syntaxe parfois maladroite.
Testo est entièrement autonome :
- Il évite
nikic/php-parser, éliminant ainsi les conflits liés à l’analyse syntaxique. - Il requiert PHP 8.2 ou ultérieur.
- Des agents IA peuvent générer automatiquement des tests à partir de
llms.txt— une spécification structurée issue de la documentation officielle.
Un système de plugins modulaires permet une personnalisation par projet. Chaque fonctionnalité est livrée sous forme de plugin optionnel ; chaque suite de tests choisit ses propres extensions. Par exemple, le plugin #[Retry] ajoute une résilience face aux tests instables (flaky tests) — implémenté en seulement deux fichiers légers.
Étendez le comportement via des pipelines, des middleware et des événements. Écrivez vos tests directement dans votre code source (src/), avec l’attribut #[TestInline], et utilisez les attributs pour intégrer proprement la logique de test.
Installation et configuration
Installez Testo via Composer :
composer require --dev testo/testo
Créez un fichier testo.php à la racine de votre projet :
<?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'],
),
],
);
Ce fichier renvoie une instance de ApplicationConfig. À défaut, Testo applique des valeurs par défaut (par exemple, analyse du dossier tests/). Deux suites sont préconfigurées : Sources, destinée aux tests intégrés et aux benchmarks insérés directement dans le code métier, et Tests, réservée aux tests unitaires classiques.
Exécutez les tests avec ./vendor/bin/testo — ou utilisez le plugin officiel PHPStorm pour une intégration transparente dans votre environnement de développement.
Exemples de tests unitaires
Aucune héritage requis. Annotez vos méthodes avec #[Test] :
final class OrderTest
{
#[Test]
public function calculatesTotal(): void
{
$order = new Order();
$order->addItem('Livre', price: 15.0, quantity: 2);
$order->addItem('Stylo', price: 3.0, quantity: 5);
Assert::same($order->total(), 45.0);
}
#[Test]
#[DataSet([100.0, 10, 90.0], '10 % de réduction')]
#[DataSet([100.0, 0, 100.0], 'aucune réduction')]
#[DataSet([0.0, 50, 0.0], 'prix nul')]
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);
}
}
Appliquez #[Test] à une classe pour traiter toutes ses méthodes void ou never comme des tests. Les fonctions autonomes (hors classes) s’exécutent sans attributs de cycle de vie.
Les assertions suivent un ordre intuitif : valeur réelle en premier, valeur attendue en second. Elles sont chaînables et pleinement typées :
Assert::string($email)->contains('@');
Assert::int($age)->greaterThan(0)->lessThan(150);
Assert::array($items)
->hasKeys('id', 'nom')
->isList()
->notEmpty();
Assert::json($response->body())
->isObject()
->hasKeys('données', 'métadonnées');
Tests intégrés et benchmarks
Utilisez #[TestInline] dans src/ pour valider des méthodes privées ou internes :
// src/Money.php
final class Money
{
#[TestInline(['prix' => 100.0, 'remise' => 0.1, 'taxe' => 0.2], 108.0)]
#[TestInline(['prix' => 50.0, 'remise' => 0.0, 'taxe' => 0.1], 55.0)]
private static function calculateFinalPrice(
float $price,
float $discount,
float $tax,
): float {
return $price * (1 - $discount) * (1 + $tax);
}
}
Mesurez les performances avec #[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;
}
Résultat type :
+---+----------+------------+---------+------------------+--------+
| # | Nom | Itérations | Appels | Temps moyen | Écart |
+---+----------+------------+---------+------------------+--------+
| 2 | actuel | 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% |
+---+----------+------------+---------+------------------+--------+
Pourquoi cela compte
- Un framework véritablement autonome — zéro dépendance à PHPUnit, extensible via des plugins composables.
- Tests intégrés (
#[TestInline]) et benchmarks (#[Bench]) directement intégrés au code métier danssrc/. - Chaînes d’assertions typées et fluides — zéro code répétitif, zéro héritage, zéro variable globale magique.
- Configuration via des fichiers PHP simples, avec prise en charge native des projets multi-suites.
- Plugin PHPStorm et outils CLI de première qualité — prêts pour une utilisation quotidienne.
— Editorial Team
Aucun commentaire pour le moment.