Double est une nouvelle bibliothèque de test doubles pour PHP signée Jason McCreary, le créateur de Laravel Shift. Elle exige PHP 8.3 et en est à la v0.4.0. L'idée centrale : au lieu de choisir d'emblée entre un mock, un spy et un partial, on crée un seul type d'objet avec Double::for() et ce sont les verbes appelés ensuite qui définissent son comportement. L'article parcourt l'API, la compare à Mockery et justifie chaque décision de conception.

Double::for() accepte des classes, des interfaces, plusieurs interfaces à la fois (tout ce qui suit le premier doit être une interface, comme la règle PHP des types d'intersection) ou une instance réelle. L'objet retourné satisfait instanceof vis-à-vis de sa cible et donc n'importe quel type hint. Chaque double fonctionne dans exactement un des trois modes : loose (par défaut) retourne des valeurs typées sûres pour les appels non configurés — false pour bool, 0 pour int, [] pour array, le premier cas d'un enum, le double lui-même pour self, et un double fraîchement généré pour les types de retour classe ou interface non nullables (la génération ne descend que d'un niveau, ensuite il faut configurer explicitement). Strict lève une exception au premier appel non configuré. Passthru délègue les appels non configurés à une instance réelle tout en enregistrant chaque appel.

La configuration vit sur le double lui-même, donc sept noms de méthodes sont réservés : expects, allows, strict, passthru, received, unused et verify. Doubler une classe qui en déclare un (pertinent sous Laravel, où allows() fait partie du contrat Gate) lève immédiatement une erreur en nommant la collision.

Les deux verbes de configuration sont expects() — l'appel doit avoir lieu exactement une fois par défaut — et allows(), qui autorise un nombre quelconque d'appels, y compris zéro. Les modificateurs se lisent de gauche à droite : with() pour les arguments, returns() (plusieurs valeurs forment une file, la dernière étant conservée), throws() et resolves() pour les closures. Les comptes d'appels passent tous par times() : times(2), times(5), times(1, 3) pour les plages, et times(minimum: 2) / times(maximum: 5) via les arguments nommés ; never() reste une méthode à part. Les mêmes comptes fonctionnent a posteriori sur received(). Quand deux attentes peuvent correspondre au même appel, la plus récemment enregistrée gagne — on écrit donc les défauts larges en premier.

Par rapport à un test Mockery, quatre choses changent : plus de décision mock()/spy(), puisque received() fonctionne sur tout double ; once() disparaît car expects() signifie déjà exactement un appel ; shouldReceive()/andReturn() deviennent expects()/returns() sans alias ; Mockery::close() est remplacé par $double->verify() ou le trait automatique VerifiesDoubles. La sortie d'échec s'améliore aussi — Double nomme la classe doublée au lieu d'un identifiant Mockery_0_ et affiche le journal des appels réels lors d'une attente non satisfaite. Le shouldNotHaveBeenCalled() de Mockery ne teste que si le mock a été invoqué comme callable, tandis que unused() de Double affirme zéro appel à toute méthode et liste ceux observés.

La correspondance d'arguments dans with() compare scalaires et tableaux avec === et les objets avec ==. Tout ce qui est plus souple passe par la façade Argument : Argument::any(), type(), same() (identité), matches() (regex), contains(), remaining() (reste variadique), capture($var) pour récupérer l'argument réel dans une variable, et not(), qui retourne un objet exposant ses propres verbes pour des négations fluides. Les attentes peuvent être marquées ordered() pour vérifier l'ordre des appels, appliqué par double ; un appel prématuré lève immédiatement une erreur nommant les deux méthodes. Les méthodes statiques ne peuvent pas être configurées — la bibliothèque les rejette d'emblée avec une explication.

Pour la vérification, verify() contrôle chaque expects() enregistré. Les chaînes received() s'évaluent à la fin de l'instruction, la bibliothèque ne pouvant savoir si vous allez enchaîner ->with() ou ->never(). Les messages d'échec nomment le double, l'appel et une piste de correction : une faute de frappe dans un nom de méthode est détectée à la configuration avec une suggestion « vouliez-vous dire », les échecs en mode strict montrent la ligne allows() qui résoudrait le problème (ou le journal des appels si la méthode a été invoquée ailleurs), et les doubles générés automatiquement en mode loose s'identifient dans leurs propres messages.

Avec PHPUnit installé (versions 11 et 12, détecté à l'exécution), les échecs étendent AssertionFailedError, donc une attente non satisfaite est rapportée comme un failure plutôt qu'une erreur, les vérifications réussies comptent comme de vraies assertions, et le trait VerifiesDoubles vérifie automatiquement chaque double créé pendant un test. PHPUnit n'est toutefois pas requis.

Pour la migration, la documentation inclut une correspondance méthode par méthode avec Mockery : mock() devient Double::for(), les spies deviennent received(), shouldIgnoreMissing() est le comportement loose par défaut, shouldDeferMissing() devient passthru($realInstance), shouldReceive('foo')->once()->andReturn($x) devient expects('foo')->returns($x), et andThrow/andReturnUsing deviennent throws()/resolves(). Les matchers se transposent un à un : Mockery::on() vers Argument::satisfies(), isSame() vers same(), andAnyOtherArgs() vers remaining(). Quelques fonctionnalités de Mockery sont volontairement absentes — alias, ducktype(), mock des méthodes statiques, ordre globally() et byDefault() — chacune avec une alternative documentée. Un convertisseur gratuit, Double Converter, sur laravelshift.com automatise la conversion.

L'installation se fait via composer require --dev jasonmccreary/double, sans service provider ni fichier de configuration — la bibliothèque fonctionne dans les suites PHPUnit ou Pest, avec ou sans Laravel. Pour doubler une classe finale, on appelle Double::bypassFinals() en première ligne du bootstrap PHPUnit, avant l'autoloader — le mot-clé final est réécrit avant la compilation, mais une classe déjà chargée ailleurs reste rejetée. La documentation complète est sur testdoublephp.com ; le code est sur GitHub sous jasonmccreary/double.