Porter son module en D8 - #2 Les tests

Cet article est le deuxième de la série qui suit, étape par étape, la conversion du module Multiple E-mail Addresses à Drupal 8. Il s'attache à la réécriture des tests fonctionnels existants afin de garantir que le processus de portage du module n'entraîne pas de changement significatif dans son fonctionnement. (Mis à jour le 1er juin 2015)

Mise à jour du 1er juin : ajout de la section concernant la conversion de getInfo() en annotations. Voir "Documenter et grouper les tests".

Dans un monde où l'on exige toujours plus de contrôle de la qualité des produits que nous utilisons au quotidien, il est normal que l'industrie logicielle ait dû se plier à la conception et la mise en œuvre de tests automatisés. Drupal n'échape pas à cette règle et c'est un gage de stabilité que de proposer des tests au sein de votre module.

Cet article est le deuxième de la série qui suit, étape par étape, la conversion d'un module contribué, Multiple E-mail Addresses, de Drupal 7 vers Drupal 8. Dans le premier article, nous avons évoqué les éléments à prendre en compte pour démarrer le portage et faire en sorte qu'il soit possible d'installer le module.

Contrôle qualité

Pyramide inversée des types de tests (unitaires, d'intégration et fonctionnels)Les tests logiciels sont catégorisés suivant plusieurs systèmes de classification différents. Personnellement, celle que j'utilise le plus est basée sur un concept de couches applicatives. Les tests unitiaires, au plus bas, assurent le bon fonctionnement des composants en testant, de manière isolée, que chaque sous partie du code produit le résultat escompté. Les tests d'intégration, plus rares, permettent de vérifier que les diverses parties de l'application communiquent correctement entre elles. Les tests fonctionnels ou tests de non-régression ont pour objectif de valider l'effectivité fonctionnelle de l'application et sont idéalement rédigés en collaboration avec le responsable fonctionnel (le client, dans le cadre d'une prestation de développement). Enfin, très rares encore aujourd'hui, les tests d'interface qui valident l'aspect et l'interactivité de l'interface graphique.

Drupal 7, de par son architecture, favorise l'utilisation de tests fonctionnels. En effet, le fait que le cœur de Drupal 7 soit encore très majoritairement procédural rend les tests unitaires très complexes car ils nécessitent de pouvoir isoler les composants à tester les uns des autres, ce qui est rendu plus facile par une architecture objet. Un exemple de test fonctionnel serait de vérifier qu'à la soumission d'un formulaire contenant des données erronées, un message d'erreur soit bien affiché.

La refonte complète du cœur de Drupal 8 a donc été pensée pour rendre l'écriture des tests fonctionnels plus simple car ils sont beaucoup plus rapides à exécuter et permettent un contrôle plus fin des composants qui pourraient avoir régressé. Le pincipe de ces tests est très simple. Il s'agit d'isoler au maximum un fragment du code, généralement contenu dans une méthode, et de l'exécuter avec des jeux de données couvrant tous les cas possibles en entrée et en contrôlant que les données en sortie sont conformes aux attentes. Pour réaliser ces tests, la communauté a choisi le moteur de test PHPUnit.

Dans le contexte du portage d'un module de Drupal 7 à Drupal 8 il est souhaitable de commencer par la conversion des tests. Ceci nous permettra au fil du travail de réécriture, d'identifier les points qui posent problème et de nous assurer que le fonctionnement existant ne soit pas altéré. Dans un second temps, si cela s'avère nécessaire, il sera toujours possible d'augmenter la couverture de tests en ajoutant, par exemple, des tests unitaires.

Anatomie d'un test fonctionnel

Dans la version 7 de Drupal, un module souhaitant implémenter des tests doit disposer d'un fichier <modulename>.test, référencé par une clef files[] du fichier <modulename>.info. Ce fichier peut contenir plusieurs classes PHP. Chacune correspond à une série de tests différents et chaque test représenté par une méthode dont le nom commence par "test" (ie. le fichier modules/block/block.test du cœur contient la classe BlockTestCase qui implémente la méthode testCustomBlock).
De plus, de nombreux modules ont besoin d'initialiser un environnement dans un état particulier pour que les tests soient effectifs. Pour y parvenir, ils proposent dans un répertoire "tests" un ou plusieurs modules dont le seul but est de mettre en place le contexte nécessaire aux tests décrits dans le fichier <modulename>.test. Par exemple, le module Field du cœur propose un module field_test définissant quelques permissions, un type d'entité, un type de champ, etc.

Dans la version 8 de Drupal, à l'heure actuelle (bêta 10), les fichiers <modulename>.test ont disparu au profit de fichiers respectant le standard PSR-4. On retrouve donc des classes de tests, une par fichier, dans les répertoires src/Tests, pour les tests fonctionnels, et tests/src/Unit, pour les tests unitaires. Comme pour Drupal 7, certains modules proposent des sous modules destinés à configurer un environnement de tests.

Convertir les tests

Le but de cet article n'est pas de faire en sorte que tous les anciens tests passent au vert car cela voudrait dire, en théorie, avoir terminé le portage. Nous allons nous concentrer sur le fait de rendre les tests exécutables par Drupal 8 et nous nous baserons ensuite sur les résultats pour faire progresser notre travail.

Astuce : un debugger comme xdebug peut être très utile dans cette phase du développement mais il faut savoir que le fait qu'il soit activé sur votre environnement va grandement augmenter le temps d'exécution de vos tests et potentiellement rendre les messages d'erreur plus confus. Il est donc recommandé de désactiver votre débugger à moins que vous en ayez absolument besoin.

Image du répertoire src/Tests

Diviser pour mieux régner

Comme indiqué précédemment, les tests dans D8 sont séparés dans des fichiers chargés automatiquement selon la norme PSR-4. Nous allons donc séparer toutes les classes présentes dans notre fichier <modulename>.test en autant de fichiers dans src/Tests.

Le module Multiple E-mail Addresses contient 4 classes de tests dans le fichier multiple_email.test :

  • MultipleEmailAdminUserTestCase ;
  • MultipleEmailBasicUserTestCase ;
  • MultipleEmailUserTestCase ;
  • MultipleEmailLateInstallTestCase.

Nous allons donc créer 4 fichiers contenant chacun une de ces classes que nous renommerons plus simplement.

La convention veut que le nom de la classe termine par "Test" pour différencier les classes de tests des classes fonctionnelles plus facilement dans les IDE proposant l'autocomplétion.

Nous obtenons donc 4 fichiers nommés respectivement :

  • AdminUserTest.php, ;
  • BasicUserTest.php ;
  • UserTest.php ;
  • LateInstallTest.php.

Chacun contient une classe PHP du même nom (sans le ".php" évidemment), déclarée dans le namespace Drupal\multiple_email\Tests.

Exemple :

class MultipleEmailUserTestCase extends DrupalWebTestCase {
  ...
}

devient

namespace Drupal\multiple_email\Tests;

class UserTest extends DrupalWebTestCase {
  ...
}

Une fois la séparation terminée, vous devriez voir apparaître vos tests dans l'interface de Simpletest (admin/config/development/testing) et vous devriez donc être en mesure de les exécuter. C'est d'ailleurs en essayant de les faire tourner que nous allons pouvoir détecter les prochaines corrections à y apporter.

La classe parente

À l'exécution de la classe UserTest nous obtenons immédiatement une erreur fatale PHP "Fatal error: Class 'Drupal\multiple_email\Tests\DrupalWebTestCase' not found in /modules/multiple_email/src/Tests/UserTest.php on line 17".

Comme souvent à partir de maintenant, la réponse se trouvera dans les Change records de Drupal 8 qui, avec une recherche sur le nom de la classe en erreur DrupalWebTestCase, nous donnera plusieurs résultats intéressants dont un qui traite précisément du renommage des classes de test.

On applique donc la recette trouvée dans le Change record et on renomme la classe parente de nos tests en WebTestBase, en important son namespace à l'aide d'une clause "use". 

namespace Drupal\multiple_email\Tests;

use Drupal\simpletest\WebTestBase;

class UserTest extends WebTestBase {
  ...
}

Documenter et grouper les tests

La méthode getInfo() présente dans les tests Drupal 7 avait pour objectif de fournir au moteur de SimpleTest des métadonnées permettant, entre autres, d'identifier et de regrouper les tests par contexte. Comme l'indique ce Change record, celle-ci a  été supprimée au profit d'annotations (une forme de commentaires sur laquelle on reviendra plus tard) sur la classe de tests. Depuis peu, l'annotation @group est d'ailleurs obligatoire sur toutes les classes de tests. Ainsi,

public static function getInfo() {
  return array(
    'name' => t('User Tests with Permission'),
    'description' => t('User registers for the site with use multiple emails permission, and interacts with the module.'),
    'group' => t('Multiple E-mail'),
  );
}

devient

/**
 * User registers for the site with use multiple emails permission, and 
 * interacts with the module.
 *
 * @group multiple_email
 */
class UserTest extends WebTestBase {
  ...
}

Conclusion

Maintenant que les erreurs structurelles ont été corrigées, les tests peuvent s'exécuter et nous allons devoir composer avec des erreurs pas forcément aussi directes. Le prochain article s'attardera sur certaines des erreurs les plus courantes rencontrées lors du portage des tests à Drupal 8 et vous donnera des pistes pour résoudre vos propres erreurs.

Votre commentaire

Le contenu de ce champ sera maintenu privé et ne sera pas affiché publiquement.
Votre adresse servira à afficher un Gravatar et à vous notifier des réponses. Votre commentaire sera anonymisé si ce billet est dépublié pendant plus de 3 mois.

À propos de Edouard

Expert technique

Après un premier contact douloureux avec Drupal en 2009 en autodidacte, j'ai suivi une formation qui m'a convaincu de mon choix technologique et m'a vraiment mis en selle. Durant plusieurs années suite à cela j'ai accompagné des entreprises locales dans le développement de leurs projets de toutes sortes, de la simple vitrine à l'intranet social en passant par le projet e-commerce.