En effet, aujourd'hui, dans le cadre des tests unitaires d'ogo, j'ai eu besoin de tester non pas la valeur d'une variable, mais ce que PHP était censé afficher à l'aide de la commande echo
.
Mon premier réflexe a été d'utiliser un adapter
, mais echo
est une construction du langage et non une fonction, et à ce titre, il n'est pas possible d'utiliser cette technique.
Pourtant, il fallait bien que je puisse tester ce que echo
recevait comme argument...
Pour résoudre ce problème, ma première approche, très classique, a été d'utiliser les fonctions de bufferisation de PHP, qui permettent de stocker en mémoire l'intégralité de ce qui est envoyé à la commande echo
et de ne l'afficher qu'à la demande ou à la fin du script, voir même de récupérer dans une variable la totalité des données.
Le code était alors le suivant :
<?php
...
protected function testMakeReport()
{
$score = new \ogo\test\cases\score($this);
$reporter = new \ogo\test\cases\reporter\cli();
$reporter->addScore($score);
ob_start();
$reporter->makeReport(); // méthode utilisant la commande echo
$echo = ob_get_clean();
$this->assert->string($echo)->isEmpty();
}
...
?>
A première vue, ce n'est pas mal puisque ma problèmatique est résolue, mais en réalité, c'est de la merde !
En effet, à chaque fois que j'aurais besoin de tester le comportement de \ogo\test\cases\reporter\cli::makeReport()
ou d'une méthode faisant appel à une commande du type d'echo
, je serais obligé de dupliquer la séquence de code suivante :
<?php
...
ob_start();
// des trucs faisant appel à echo
$echo = ob_get_clean();
$this->assert->string($echo)->......
?>
Et je sais pas ce qu'il en a été pour vous, mais mes professeurs m'ont appris qu'un développeur qui fait de la duplication de code méritait le bûcher, et personnellement, je suis plutôt allergique aux flammes.
De plus, je suis un fainéant n'aime pas avoir le bout des doigts carrés à force de taper sur mon clavier.
Et c'est à ce moment que j'ai pensé aux fameuses fermetures de PHP 5.3.
En effet, pourquoi ne pas créer une méthode dans ma classe de test qui accepterait une fonction anonyme comme argument, qui démarrerait la bufferisation, éxécuterait la fonction anonyme, arrêterait la bufferisation et renverrait son contenu ?
C'est donc ce que j'ai fais, de la manière suivante.
J'ai ajouté à ma classe de test \ogo\test\cases\base
la méthode getOutput()
, dont le code est le suivant :
<?php
...
protected function getOutput($function)
{
ob_start();
$function();
$output = ob_get_clean();
return $output;
}
...
?>
Une fois cela fait, il ne me restait plus qu'à la mettre en oeuvre dans mon test à l'aide d'une fermeture :
<?php
...
protected function testMakeReport()
{
$score = new \ogo\test\cases\score($this);
$reporter = new \ogo\test\cases\reporter\cli();
$reporter->addScore($score);
$this->assert->string($this->getOutput(function() use ($reporter) { $reporter->makeReport(); }))->isEmpty();
}
...
?>
Et en poussant le vice un peu plus loin, il serait même possible de déporter ce traitement dans un asserter
spécifique puisque finalement, la méthode getOutput()
n'a rien à faire dans la classe de test.
De plus cela permettrait de réduire encore le code :
<?php
...
protected function testMakeReport()
{
$score = new \ogo\test\cases\score($this);
$reporter = new \ogo\test\cases\reporter\cli();
$reporter->addScore($score);
// nous passons maintenant directement par l'asserter
$this->assert->output(function() use ($reporter) { $reporter->makeReport(); })->isEmpty();
}
...
?>
Ainsi, la duplication de code est supprimée, je ne brulerais pas sur un bûcher, mes doigts ne deviendront pas carrés, et j'ai un moyen standard pour tester tout ce qui est envoyé à echo
et aux autres commandes PHP du même type.
Pour ceux que cela intéresse, le code final est disponible dans le dépôt de ogo
.
Et finalement, les fermetures, assez paradoxalement, m'ouvre tout un tas de perspectives intéressantes dans le cadre de futurs développements...
4 réactions
1 De metagoto - 22/02/2010, 13:51
Les closures tel qu'implémentés actuellement dans php sont utiles mais souffrent de limitations qui fort heureusement devraient être levées un de ces jours. Le problème concerne la manière dont les closures sont liés au model objet: comment $this est référencé, quelle est la visibilité (accès aux membres private etc) et comment on procède à des "rebinds" (penser à javascript).
Aux dernières nouvelles de la mailing list "internals", un consensus se dégage et tend vers la dernière proposition de ce document (Modified proposal A) :
http://wiki.php.net/rfc/closures/ob...
Perso, je pense que c'est la meilleur proposition à défaut d'être la moins verbeuse, mais le prix à payer est légitimé par les possibilités offertes.
La première utilisation à laquelle je pense consisterait à fournir des "helpers" en tant que fonctions anonymes (possiblement closures) pour un système de templates. Ca serait beaucoup plus élégant à l'utilisation que les implémentations actuelles (enfin, tout dépend de ce qu'on appelle élégant
2 De tight - 25/02/2010, 13:01
Intéressant. Je n'ai toujours vu les closures (php) comme une solution plus élégante aux callbacks pour les array_*
Petite question : pourquoi utilises-tu echo, plutôt que de retourner une chaine (méthode plus facilement testable d'ailleurs) ? Est-ce que ça n'est pas un peu un "effet de bord" ?
3 De mageekguy - 25/02/2010, 16:14
@tight:
J'utilise
echo
car le but de l'objet est d'afficher des informations en mode CLI.Il est donc difficile de le faire autrement que via cette commande ou sa cousine
printf()
;).4 De tight - 26/02/2010, 09:17
Yep, je viens d'avoir un flash, je passais justement pour dire de laisser tomber la question