Test parallèle pour PHPUnit avec ParaTest

PHPUnit a fait allusion au parallélisme depuis 2007, mais dans l’intervalle, nos tests continuent de se dérouler lentement. Le temps, c'est de l'argent, non? ParaTest est un outil qui repose sur PHPUnit et vous permet d'exécuter des tests en parallèle sans utiliser d'extensions. C'est un candidat idéal pour les tests fonctionnels (à savoir le sélénium) et autres processus de longue durée.


ParaTest à votre service

ParaTest est un outil de ligne de commande robuste permettant d'exécuter des tests PHPUnit en parallèle. Inspiré par les gens de Sauce Labs, il a été développé à l'origine pour être une solution plus complète d'amélioration de la vitesse des tests fonctionnels..

Depuis sa création - et grâce à de brillants contributeurs (dont Giorgio Sironi, mainteneur de l'extension PHPUnit Selenium) -, ParaTest est devenu un outil précieux pour accélérer les tests fonctionnels, ainsi que les tests d'intégration avec bases de données, services Web et systèmes de fichiers..

ParaTest a également l’honneur d’être associé au framework de test de Sauce Labs, Sausage, et a été utilisé dans près de 7 000 projets au moment de la rédaction de ce document..

Installer ParaTest

Actuellement, le seul moyen officiel d’installer ParaTest est via Composer. Pour ceux d'entre vous qui débutez dans Composer, nous avons un excellent article sur le sujet. Pour récupérer la dernière version de développement, incluez les éléments suivants dans votre composer.json fichier:

 "require": "brianium / paratest": "dev-master"

Alternativement, pour la dernière version stable:

 "require": "brianium / paratest": "0.4.4"

Ensuite, lancez compositeur installer à partir de la ligne de commande. Le binaire ParaTest sera créé dans le fournisseur / bin annuaire.

L'interface de ligne de commande ParaTest

ParaTest inclut une interface de ligne de commande qui devrait être familière à la plupart des utilisateurs de PHPUnit - avec quelques bonus supplémentaires pour les tests en parallèle.

Votre premier test en parallèle

Utiliser ParaTest est aussi simple que PHPUnit. Pour démontrer rapidement ceci en action, créez un répertoire, échantillon paratest, avec la structure suivante:

Installons ParaTest comme mentionné ci-dessus. En supposant que vous ayez un shell Bash et un binaire Composer installé globalement, vous pouvez le faire en une ligne à partir du échantillon paratest annuaire:

 echo '"require": "brianium / paratest": "0.4.4"'> composer.json && composer installer

Pour chacun des fichiers du répertoire, créez une classe de cas de test portant le même nom, comme suit:

 La classe SlowOneTest étend PHPUnit_Framework_TestCase fonction publique test_long_running_condition () sleep 5); $ this-> assertTrue (true); 

Prendre note de l'utilisation de dormir (5) simuler un test dont l’exécution prend cinq secondes. Nous devrions donc avoir cinq scénarios de test qui durent chacun cinq secondes. En utilisant PHPUnit vanilla, ces tests seront exécutés en série et dureront vingt-cinq secondes au total. ParaTest exécutera ces tests simultanément dans cinq processus distincts et ne devrait prendre que cinq secondes, pas vingt-cinq!

Maintenant que nous avons compris ce qu'est ParaTest, approfondissons un peu les problèmes associés à l'exécution de tests PHPUnit en parallèle..


Le problème à portée de main

Les tests peuvent être un processus lent, en particulier lorsque nous commençons à parler de frapper une base de données ou d'automatiser un navigateur. Pour effectuer des tests plus rapidement et plus efficacement, nous devons pouvoir exécuter nos tests simultanément (au même moment) et non en série (l'un après l'autre)..

La méthode générale pour y parvenir n'est pas une idée nouvelle: exécutez différents groupes de tests dans plusieurs processus PHPUnit. Ceci peut facilement être accompli en utilisant la fonction native PHP proc_open. Ce qui suit serait un exemple de cela en action:

 / ** * $ runningTests - processus actuellement ouverts * $ LoadTests - un tableau de chemins de test * $ maxProcs - nombre total de processus que nous souhaitons exécuter * / while (sizeof ($ runningTests) || sizeof ($ LoadTests)) while (sizeof ($ LoadTests) && sizeof ($ runningTests) < $maxProcs) $runningTests[] = proc_open("phpunit " . array_shift($loadedTests), $descriptorspec, $pipes); //log results and remove any processes that have finished… 

Parce que PHP manque de threads natifs, c'est une méthode typique pour atteindre un certain niveau de concurrence. Les défis particuliers des outils de test utilisant cette méthode peuvent être résumés en trois problèmes centraux:

  • Comment charger les tests?
  • Comment pouvons-nous agréger et signaler les résultats des différents processus PHPUnit??
  • Comment pouvons-nous assurer la cohérence avec l'outil d'origine (c'est-à-dire PHPUnit)??

Examinons quelques techniques qui ont été utilisées dans le passé, puis examinons ParaTest et ses différences avec le reste de la foule..


Ceux qui sont venus avant

Comme indiqué précédemment, l'idée d'exécuter PHPUnit dans plusieurs processus n'est pas nouvelle. La procédure typique utilisée est la suivante:

  • Grep pour les méthodes de test ou charger un répertoire de fichiers contenant des suites de tests.
  • Ouvrir un processus pour chaque méthode de test ou suite.
  • Analyser la sortie du tube STDOUT.

Jetons un coup d'oeil à un outil qui utilise cette méthode.

Bonjour, Paraunit

Paraunit était le premier coureur parallèle associé à l'outil Sausage de Sauce Labs et servait de point de départ pour ParaTest. Regardons comment il aborde les trois principaux problèmes mentionnés ci-dessus.

Test de chargement

Paraunit a été conçu pour faciliter les tests fonctionnels. Il exécute chaque méthode de test plutôt que toute une suite de tests dans un processus PHPUnit qui lui est propre. Étant donné le chemin d'accès à une collection de tests, Paraunit recherche des méthodes de test individuelles, via une correspondance de modèle avec le contenu du fichier..

 preg_match_all ("/ function (test [^ \ (] +) \ (/", $ fileContents, $ correspond à));

Les méthodes de test chargées peuvent ensuite être exécutées comme suit:

 proc_open ("phpunit --filter = $ testName $ testFile", $ descriptorspec, $ pipes);

Dans un test où chaque méthode configure et ferme un navigateur, cela peut accélérer les choses si chacune de ces méthodes est exécutée dans un processus séparé. Cependant, cette méthode pose quelques problèmes.

Alors que les méthodes qui commencent par le mot, "tester,"est une convention solide parmi les utilisateurs de PHPUnit, les annotations sont une autre option. La méthode de chargement utilisée par Paraunit ignorerait ce test parfaitement valide:

 / ** * @test * / fonction publique twoTodosCheckedShowsCorrectClearButtonText () $ this-> todos-> addTodos (array ('un', 'deux')); $ this-> todos-> getToggleAll () -> click (); $ this-> assertEquals ('Effacer 2 éléments terminés', $ this-> todos-> getClearButton () -> text ()); 

En plus de ne pas prendre en charge les annotations de test, l'héritage est également limité. Nous pourrions discuter des avantages de faire quelque chose comme ceci, mais considérons la configuration suivante:

 classe abstraite TodoTest étend PHPUnit_Extensions_Selenium2TestCase protected $ browser = null; fonction publique setUp () // configurer le navigateur fonction publique testTypingIntoFieldAndHittingEnterAddsTodo () // la magie du sélénium / ** * ChromeTodoTest.php * Aucune méthode de test à lire! * / class ChromeTodoTest étend TodoTest protected $ browser = 'chrome';  / ** * FirefoxTodoTest.php * Aucune méthode de test à lire! * / class FirefoxTodoTest étend TodoTest protected $ browser = 'firefox'; 

Les méthodes héritées ne sont pas dans le fichier, elles ne seront donc jamais chargées.

Affichage des résultats

Paraunit agrège les résultats de chaque processus en analysant la sortie générée par chaque processus. Cette méthode permet à Paraunit de capturer toute la gamme de codes courts et de commentaires formulés par PHPUnit..

L’inconvénient d’agréger les résultats de cette manière est qu’il est difficile à manier et à casser. Il y a beaucoup de résultats différents à prendre en compte et beaucoup d'expressions régulières au travail pour afficher des résultats significatifs de cette manière..

Cohérence avec PHPUnit

En raison du grepping des fichiers, Paraunit est assez limité dans les fonctionnalités PHPUnit qu’il peut prendre en charge. C'est un excellent outil pour exécuter une structure simple de tests fonctionnels, mais en plus des difficultés déjà mentionnées, il manque certaines fonctionnalités utiles de PHPUnit. Parmi ces exemples, citons les suites de tests, la spécification des fichiers de configuration et d’amorçage, la journalisation des résultats et l’exécution de groupes de tests spécifiques..

De nombreux outils existants suivent ce modèle. Grep un répertoire de fichiers de test et soit exécuter le fichier entier dans un nouveau processus ou chaque méthode - jamais les deux.


ParaTest At Bat

ParaTest a pour objectif de prendre en charge les tests en parallèle pour divers scénarios. Créé à l'origine pour combler les lacunes de Paraunit, il est devenu un outil de ligne de commande robuste permettant d'exécuter des suites de tests et des méthodes de test en parallèle. Cela fait de ParaTest un candidat idéal pour des tests longs de différentes formes et tailles.

Comment ParaTest gère les tests parallèles

ParaTest s'écarte de la norme établie afin de prendre en charge davantage de PHPUnit et constitue un candidat réellement viable pour les tests en parallèle.

Test de chargement

ParaTest charge les tests de la même manière que PHPUnit. Il charge tous les tests dans un répertoire spécifié qui se termine par le * Test.php suffixe, ou chargera des tests basés sur le fichier de configuration XML standard de PHPUnit. Le chargement est accompli, par réflexion, il est donc facile de supporter @tester méthodes, héritage, suites de tests et méthodes de test individuelles. La réflexion simplifie l'ajout de la prise en charge des autres annotations.

Parce que la réflexion permet à ParaTest de saisir des classes et des méthodes, il peut exécuter des suites de tests et des méthodes de test en parallèle, ce qui en fait un outil plus polyvalent..

ParaTest impose certaines contraintes, mais bien fondées dans la communauté PHP. Les tests doivent respecter la norme PSR-0 et le suffixe par défaut du fichier * Test.php n'est pas configurable, comme c'est le cas dans PHPUnit. Une branche est en cours pour prendre en charge la même configuration de suffixe que celle autorisée dans PHPUnit..

Affichage des résultats

ParaTest s'écarte également du chemin d'analyse des tubes STDOUT. Au lieu d'analyser les flux de sortie, ParaTest enregistre les résultats de chaque processus PHPUnit au format JUnit et agrège les résultats de ces journaux. Il est beaucoup plus facile de lire les résultats de test à partir d'un format établi qu'un flux de sortie.

        

L'analyse des journaux JUnit présente quelques inconvénients mineurs. Les tests ignorés et ignorés ne sont pas signalés dans le retour immédiat, mais ils seront reflétés dans les valeurs totalisées affichées après un test..

Cohérence avec PHPUnit

Reflection permet à ParaTest de prendre en charge davantage de conventions PHPUnit. La console ParaTest prend en charge davantage de fonctionnalités PHPUnit prêtes à l'emploi que tout autre outil similaire, telles que la possibilité d'exécuter des groupes, de fournir la configuration et les fichiers d'amorçage, ainsi que de consigner les résultats au format JUnit..


Exemples de ParaTest

ParaTest peut être utilisé pour gagner de la vitesse dans plusieurs scénarios de test.

Tests fonctionnels avec sélénium

ParaTest excelle dans les tests fonctionnels. Il supporte un -F basculer dans sa console pour activer le mode fonctionnel. Le mode fonctionnel demande à ParaTest d’exécuter chaque méthode de test dans un processus séparé, au lieu de la méthode par défaut, qui consiste à exécuter chaque suite de tests dans un processus séparé..

Il arrive souvent que chaque méthode de test fonctionnel effectue beaucoup de travail, par exemple ouvrir un navigateur, naviguer dans la page, puis fermer le navigateur..

L'exemple de projet, paratest-selenium, illustre le test d'une application de tâche Backbone.js avec Selenium et ParaTest. Chaque méthode de test ouvre un navigateur et teste une fonctionnalité spécifique:

 fonction publique setUp () $ this-> setBrowserUrl ('http://backbonejs.org/examples/todos/'); $ this-> todos = new Todos ($ this-> prepareSession ());  fonction publique testTypingIntoFieldAndHittingEnterAddsTodo () $ this-> todos-> addTodo ("paralléliser les tests phpunit \ n"); $ this-> assertEquals (1, sizeof ($ this-> todos-> getItems ()));  fonction publique testClickingTodoCheckboxMarksTodoDone () $ this-> todos-> addTodo ("assurez-vous que vous pouvez compléter todos"); $ items = $ this-> todos-> getItems (); $ item = array_shift ($ items); $ this-> todos-> getItemCheckbox ($ item) -> click (); $ this-> assertEquals ('done', $ item-> attribut ('classe'));  //… plus de tests

Ce cas de test pourrait prendre une seconde chaude s'il devait être exécuté en série, via vanUnity PHPUnit. Pourquoi ne pas exécuter plusieurs méthodes à la fois?

Gestion des conditions de course

Comme pour tout test parallèle, nous devons être conscients des scénarios qui présenteront des conditions de concurrence, telles que plusieurs processus essayant d'accéder à une base de données. La branche dev-master de ParaTest présente une fonctionnalité de jeton de test très pratique, écrite par le collaborateur Dimitris Baltas (dbaltas sur Github), qui facilite grandement les bases de tests d'intégration..

Dimitris a inclus un exemple utile qui illustre cette fonctionnalité sur Github. Dans les propres mots de Dimitris:

TEST_TOKEN tente de résoudre le problème des ressources communes de manière très simple: clonez les ressources pour vous assurer qu'aucun processus simultané n'aura accès à la même ressource.

UNE TEST_TOKEN La variable d'environnement est fournie aux tests, et est recyclée à la fin du processus. Il peut être utilisé pour modifier conditionnellement vos tests, comme ceci:

 fonction publique setUp () parent :: setUp (); $ this -> _ filename = sprintf ('out% s.txt', getenv ('TEST_TOKEN')); 

Laboratoires ParaTest et Sauce

Sauce Labs est l'Excalibur des tests fonctionnels. Sauce Labs fournit un service qui vous permet de tester facilement vos applications dans divers navigateurs et plates-formes. Si vous ne les avez pas vérifiées auparavant, je vous encourage fortement à le faire.

Tester avec Sauce est peut-être un didacticiel en soi, mais ces assistants ont déjà fourni des didacticiels permettant d’utiliser PHP et ParaTest pour écrire des tests fonctionnels avec leur service..


L'avenir de ParaTest

ParaTest est un excellent outil pour combler certaines des lacunes de PHPUnit, mais, finalement, ce n'est qu'un bouchon dans le barrage. Un meilleur scénario serait le support natif dans PHPUnit!

Entre temps, ParaTest continuera à prendre en charge davantage de comportements natifs de PHPUnit. Il continuera à offrir des fonctionnalités utiles aux tests en parallèle, en particulier dans les domaines de la fonctionnalité et de l'intégration..

ParaTest a de nombreux projets en cours pour renforcer la transparence entre PHPUnit et lui-même, principalement en ce qui concerne les options de configuration prises en charge..

La dernière version stable de ParaTest (v0.4.4) prend en charge confortablement Mac, Linux et Windows, mais il existe quelques demandes d'extraction et fonctionnalités intéressantes dans maître de dev qui s'adressent certainement aux foules Mac et Linux. Donc, ce sera une conversation intéressante pour aller de l'avant.

Lecture et ressources supplémentaires

Il existe une poignée d'articles et de ressources sur le Web qui présentent ParaTest. Donnez-leur une lecture, si cela vous intéresse:

  • ParaTest sur Github
  • Giorgio Sironi, contributeur parallèle à PHPUnit by ParaTest et responsable de l'extension PHPUnit Selenium
  • Contribuer à Paratest. Un excellent article sur le WrapperRunner expérimental de Giorgio pour ParaTest
  • Code source WrapperRunner de Giorgio
  • tripsta / paratest-sample. Un exemple de la fonctionnalité TEST_TOKEN de son créateur Dimitris Baltas
  • brianium / paratest-sélénium. Un exemple d'utilisation de ParaTest pour écrire des tests fonctionnels