Le fournisseur de données doit donc retourner un tableau ou un intégrateur contenant les arguments qui seront automatiquement transmis par atoum à toutes les méthodes de test auxquelles est rattaché le fournisseur de données lors de l’exécution des tests.
<?php class calculator extends atoum { /** * @dataProvider sumDataProvider */ public function testSum($a, $b) { $this ->if($calculator = new project\calculator()) ->then ->integer($calculator->sum($a, $b))->isEqualTo($a + $b) ; } public function sumDataProvider() { return array( array( 1, 1), array( 1, 2), array(-1, 1), array(-1, 2), ); } }
Et si j’ai toujours trouvé le concept intéressant dans le fond, j’ai toujours trouvé que la forme nuisait à la lisibilité des tests.
De plus, j’ai rarement rencontré un cas de figure ou leur utilisation était réellement pertinente.
L’un dans l’autre, cela explique pourquoi je n’ai jamais mis en œuvre les fournisseurs de données jusqu’à aujourd’hui alors que j’en ai réalisé l’implémentation.
Cependant, aujourd’hui, j’ai rencontré un cas de figure dans lequel leur utilisation s’imposait.
J’avais en effet défini deux méthodes de tests très similaires puisque la seule différence de comportement entre les deux méthodes que je testais était le fait que l’une devait retourner $this, tandis que la seconde devait renvoyer un clone de $this.
De plus, les deux méthodes étaient testées avec les mêmes arguments.
<?php class path extends atoum { … public function testRelativizeFrom() { $this ->if($path = new testedClass('/a/b', '/')) ->then ->object($path->relativizeFrom($path)) ->isIdenticalTo($path) ->toString->isEqualTo('.') ->if($path = new testedClass('/a/b', '/')) ->then ->object($path->relativizeFrom(new testedClass('/a', '/'))) ->isIdenticalTo($path) ->toString->isEqualTo('./b') ->if($path = new testedClass('/a/b', '/')) ->then ->object($path->relativizeFrom(new testedClass('/a/', '/'))) ->isIdenticalTo($path) ->toString->isEqualTo('./b') ->if($path = new testedClass('/a/b', '/')) ->then ->object($path->relativizeFrom(new testedClass('/c', '/'))) ->isIdenticalTo($path) ->toString->isEqualTo('../a/b') ->if($path = new testedClass('/a/b', '/')) ->then ->object($path->relativizeFrom(new testedClass('/c/', '/'))) ->isIdenticalTo($path) ->toString->isEqualTo('../a/b') ->if($path = new testedClass('/a/b', '/')) ->then ->object($path->relativizeFrom(new testedClass('/c/d', '/'))) ->isIdenticalTo($path) ->toString->isEqualTo('../../a/b') ->if($path = new testedClass('/a/b', '/')) ->then ->object($path->relativizeFrom(new testedClass('/c/d/', '/'))) ->isIdenticalTo($path) ->toString->isEqualTo('../../a/b') ->if($path = new testedClass('/a/b', '/')) ->then ->object($path->relativizeFrom(new testedClass('/', '/'))) ->isIdenticalTo($path) ->toString->isEqualTo('./a/b') ; } public function testGetRelativizedPathFrom() { $this ->if($path = new testedClass('/a/b', '/')) ->then ->object($path->getRelativizedPathFrom($path)) ->isNotIdenticalTo($path) ->toString->isEqualTo('.') ->if($path = new testedClass('/a/b', '/')) ->then ->object($path->getRelativizedPathFrom(new testedClass('/a', '/'))) ->isNotIdenticalTo($path) ->toString->isEqualTo('./b') ->if($path = new testedClass('/a/b', '/')) ->then ->object($path->getRelativizedPathFrom(new testedClass('/a/', '/'))) ->isNotIdenticalTo($path) ->toString->isEqualTo('./b') ->if($path = new testedClass('/a/b', '/')) ->then ->object($path->getRelativizedPathFrom(new testedClass('/c', '/'))) ->isNotIdenticalTo($path) ->toString->isEqualTo('../a/b') ->if($path = new testedClass('/a/b', '/')) ->then ->object($path->getRelativizedPathFrom(new testedClass('/c/', '/'))) ->isNotIdenticalTo($path) ->toString->isEqualTo('../a/b') ->if($path = new testedClass('/a/b', '/')) ->then ->object($path->getRelativizedPathFrom(new testedClass('/c/d', '/'))) ->isNotIdenticalTo($path) ->toString->isEqualTo('../../a/b') ->if($path = new testedClass('/a/b', '/')) ->then ->object($path->getRelativizedPathFrom(new testedClass('/c/d/', '/'))) ->isNotIdenticalTo($path) ->toString->isEqualTo('../../a/b') ->if($path = new testedClass('/a/b', '/')) ->then ->object($path->getRelativizedPathFrom(new testedClass('/', '/'))) ->isNotIdenticalTo($path) ->toString->isEqualTo('./a/b') ; } … }
L’utilisation d’un fournisseur de données s’imposait donc afin de mutualiser la gestion des arguments afin de supprimer la duplication de code induite par le fait que j’utilisais dans les deux méthodes de test exactement les mêmes arguments.
De plus, grâce au fournisseur de données, je pourrais au besoin ajouter un cas de test supplémentaire pour chacune des deux méthodes de test en modifiant uniquement le fournisseur de données.
J’ai donc défini les annotations @dataProvider
nécessaires pour chacune des deux méthodes de test, ainsi que la méthode nécessaire à la génération de leurs arguments.
Et je me suis alors rendu compte de la lourdeur du processus.
En effet, pour mettre en place mon fournisseur de données, j’ai été obligé d’ajouter l’annotation mentionnée précédemment pour chacune des deux méthodes afin de définir la méthode devant être utilisée comme telle.
De plus, j’ai été obligé d’ajouter à chacune de mes deux méthodes de test les arguments nécessaires.
Or, il est tout à fait possible de se passer totalement de la définition de l’annotation.
En effet, si une méthode de test accepte des arguments, elle fait nécessairement appel à un fournisseur de données.
Et si une convention existe permettant de déduire automatiquement à partir du nom d'une méthode de test le fournisseur de données correspondant, il devient alors possible pour le développeur des tests de ne pas annoter sa méthode de test pour indiquer à atoum le fournisseur de données qu’elle devra utiliser lors de l’exécution des tests.
La rédaction des tests deviendrait alors plus simple et intuitive…
J’ai donc modifié le code d’atoum afin que si une méthode de test accepte des arguments et qu’elle ne dispose pas de l’annotation @dataProvider
, elle utilise alors automatiquement et sans aucune intervention de l’utilisateur le fournisseur de données modélisé par la méthode protégée ou privée portant le nom résultat de la concaténation du nom de la méthode de test et de la chaîne de caractères 'DataProvider'
.
<?php class path extends atoum { … public function testRelativizeFrom($path, $directorySeparator, $fromPath, $fromDirectorySeparator, $relativePath) { $this ->if($path = new testedClass($path, $directorySeparator)) ->then ->object($path->relativizeFrom(new testedClass($fromPath, $fromDirectorySeparator))) ->isIdenticalTo($path) ->toString->isEqualTo($relativePath) ; } public function testGetRelativizedPathFrom($path, $directorySeparator, $fromPath, $fromDirectorySeparator, $relativePath) { $this ->if($path = new testedClass($path, $directorySeparator)) ->then ->object($path->getRelativizedPathFrom(new testedClass($fromPath, $fromDirectorySeparator))) ->isNotIdenticalTo($path) ->toString->isEqualTo($relativePath) ; } protected function testRelativizeFromDataProvider() { return array( array('/a/b', '/', '/a/b', '/', '.'), array('/a/b', '/', '/a', '/', './b'), array('/a/b', '/', '/a/', '/', './b'), array('/a/b', '/', '/c', '/', '../a/b'), array('/a/b', '/', '/c/', '/', '../a/b'), array('/a/b', '/', '/c/d', '/', '../../a/b'), array('/a/b', '/', '/c/d/', '/', '../../a/b'), array('/a/b', '/', '/', '/', './a/b') ); } protected function testGetRelativizedPathFromDataProvider() { return $this->testRelativizeFromDataProvider(); } … }
Quelques lignes de code plus tard et quelques itérations successives puisque je n’ai pas compris immédiatement que l’annotation pouvait devenir totalement facultative, atoum facilitait donc encore un peu plus la vie du développeur des tests en lui permettant de ne pas avoir à se soucier de certains détails d’implémentation durant leur rédaction.
J’ai donc eu la preuve que suivre l’adage Eat your own dog food
est le meilleur moyen d’améliorer son code pour un développeur !
8 réactions
1 De Swop - 19/06/2013, 22:59
Bonjour,
Je ne suis pas utilisateur d'Atoum, mais je trouve votre approche des TU très intéressante dans la mesure où le but est ici de faciliter la vie du développeur.
Venant du monde de PHPUnit, je me pose une question avec ce système d'utilisation automatique de data providers.
Je suppose que, de la même façon que PHPUnit, les méthode de tests sont reconnaissables (et exécutées) car leur nom est préfixé par "test". Cela ne pose-t-il pas un problème étant donné que ces data providers sont préfixés également de la même façon ?
Excusez-moi d'avance si la question vous semble triviale, mais ça m'intéresserait de comprendre comment Atoum définie les tests à executer parmis l'ensemble des fonctions existantes dans la classe de test.
Cordialement.
2 De Cyrano - 19/06/2013, 23:47
Je vais peut-être dire une énormité, mais le truc qui m'est venu à l'esprit est la chose suivante : est-ce que ce n'est pas typiquement un problème qui pourrait être traité avec hoa\praspel ? De mémoire (je n'ai jamais testé), il s'agit d'annoter le code avec des commentaires particuliers propres à praspel pour générer automatiquement les tests. En l'occurrence et dans l'idée générale, ça dispenserait purement et simplement d'avoir à écrire les fournisseurs de données... ?
Oublier tout ça si j'ai dit une ânerie, merci :S
3 De mageekguy - 20/06/2013, 10:44
@Swop : Les méthodes de test doivent effectivement être préfixé par "test" dans atoum, mais elles doivent également être publique.
Il suffit donc de définir les fournisseurs de données en tant que méthode protégée pour ne pas qu'ils soient considérés comme des méthodes de test.
4 De mageekguy - 20/06/2013, 10:47
@Cyrano : Praspel n'a pas du tout le même objectif que ce que je viens de développer et qui surtout ne s'utilise pas de la même façon puisque quelque part, il remplace le développeur des tests.
C'est un outil à part entière qui a un fort potentiel et des gens sont en train de travailler pour effectivement l'utiliser avec atoum mais chut…
5 De mnapoli - 20/06/2013, 10:59
Hello,
Pareil, je n'avais jamais eu l'occasion d'utiliser les data providers et je n'en voyait pas l'intérêt jusqu'à dernièrement.
Pour faire des tests d'intégration de PHP-DI (mon container d'injection de dépendances), je voulais tester toutes les configurations possibles et équivalentes (annotations, fichiers de config, config à la volée à l'aide de code PHP, ...).
Du coup il m'a suffit d'écrire les tests 1 fois (1 test par fonctionnalité), et d'écrire un data provider qui retournerait X containers, tous configurés pareils mais à partir de sources différentes (donc annotations, fichiers, etc.).
Du coup, les tests sont très lisibles (je teste chaque fonctionnalité 1 par 1), il est extrèmement facile d'ajouter une nouvelle source de configuration, et il est tout aussi facile d'ajouter un test pour une nouvelle fonctionnalité.
6 De Cyrano - 20/06/2013, 13:07
« des gens sont en train de travailler pour effectivement l'utiliser avec atoum mais chut… »
Ok, promis je dirai rien, ça sortira pas d'internet :D
7 De gdelamarre - 16/07/2013, 16:59
Salut Fred,
quelques remarques en vrac (et en retard) :
- j'avoue être surpris que tu aies mis tout ce temps à rencontrer une situation dans laquelle un DataProvider était pertinent... c'est pour moi un outil essentiel, et trop souvent méconnu des auteurs de tests
- j'ai rencontré des soucis avec cette mise à jour qui a rompu la compatibilité avec le fonctionnement précédent (setDataProvider())
- je trouve que l'idée de se baser sur une convention pour déterminer automatiquement le DataProvider est à ranger dans la catégorie "fausses bonnes idées" (sauf ton respect ;)) -> en terme de lisibilité je trouve ça moyen, au contraire, et en terme d'apprentissage également (quand tu relis un code de ce genre la première fois il te faut deviner le fonctionnement - l'annotation est nettement plus explicite)
- sans compter que j'ai aussi eu des problèmes sur certaines méthodes de tests qui attendaient des paramètres facultatifs et inutilisés (analyse statique de code) et qui m'ont donc obligé à implémenter des providers inutiles - lesquels soit dit en passant ne sont pas très à l'aise avec les paramètres facultatifs justement
8 De mageekguy - 16/07/2013, 20:54
@gdelamarre : si tu as des précisions sur l'aspect rétro-compatibilité, je suis preneur.
Et oui, en l'état, les arguments facultatifs ne sont pas gérés.