J'ai besoin d'étendre une classe de tiers que je ne peux pas modifier. Les dépendances de la classe sont pour la plupart injectées dans le constructeur, ce qui les rend faciles à moquer. Cependant, la classe utilise également des méthodes d'un trait qui appellent directement des services globaux, détruisant l'isolement des tests. Voici un échantillon de code qui illustre le problème:
trait SomeTrait {
public function irrelevantTraitMethod() {
throw new \Exception('Some undesirable behavior.');
}
}
class SomeSystemUnderTest {
use SomeTrait;
public function methodToTest($input) {
$this->irrelevantTraitMethod();
return $input;
}
}
class MockWithTraitTest extends \PHPUnit_Framework_TestCase {
public function testMethodToTest() {
$sut = new SomeSystemUnderTest();
$this->assertSame(123, $sut->methodToTest(123)); // Unexpected exception!
}
}
Comment cela traite-t-il d'une situation comme celle-ci à PHPUNIT? Les traits peuvent-ils être moqués et injectés d'une manière ou d'une autre? La seule solution que j'ai trouvée est de se moquer de la SUT elle-même et de nullifier simplement les méthodes gênantes du trait, laissant les méthodes de SUT réelles intactes. Mais cela ne se sent pas bien.
J'espère que quelqu'un fournira une meilleure réponse à un moment donné, mais entre-temps - au cas où quelqu'un d'autre traite du même problème - voici la solution de contournement que j'ai mentionnée dans la question. Compte tenu du même trait et de la même SUT, le test ci-dessous passera. Il crée une "simulation partielle" de la SUT, remplaçant uniquement la méthode du trait et laissant le reste intact pour exercer et affirmer.
class MockWithTraitTest extends \PHPUnit_Framework_TestCase {
public function testMethodToTest() {
/** @var \SomeSystemUnderTest|\PHPUnit_Framework_MockObject_MockObject $sut */
$sut = $this->getMockBuilder('\SomeSystemUnderTest')
->setMethods(array('irrelevantTraitMethod'))
->getMock();
$this->assertSame(123, $sut->methodToTest(123)); // OK.
}
}
Cette approche fonctionne, mais je n'aime pas le faire parce que je crains que cela nécessite un couplage trop serré à la mise en œuvre et pourrait conduire à des tests fragiles. (Et si j'ajoute une autre méthode au trait, disons pour la journalisation et utilisez-la dans la SUT? Cela causera des échecs de test non pertinents jusqu'à ce que je tiens à mettre à jour toutes mes moqueurs.) Je préférerais de loin être capable de se moquer du trait entier.
Il n'y a aucun moyen de faire ce que vous voulez. Les traits sont un moyen de définir une API publique classes, de la même manière que les interfaces et l'héritage. Vous ne pouvez pas "simuler" que loin comme si vous vous moquiez d'une dépendance.
Ce qui pourrait fonctionner (pas essayé): utiliser la réflexion pour "désactiver" de manière dynamique toutes les méthodes que le trait définit.
[.____] Toutefois, il y a un autre problème avec ceci: la classe d'implémentation peut écraser les méthodes du trait ou utiliser une méthode d'un autre trait avec le même nom. Il suffit simplement de "désactiver" toutes les méthodes que le trait "cible" définit ne fera pas tout à fait l'affaire.
[.____] Si vous avez de la chance, la réflexion vous aidera également à travailler autour de cela.