Le développement piloté par les tests et les tests unitaires sont les derniers moyens de garantir que le code continue de fonctionner comme prévu malgré les modifications et les ajustements majeurs. Dans cet article, vous apprendrez à tester unitairement votre code PHP au niveau du module, de la base de données et de l'interface utilisateur (UI).
Il est 3 heures du matin. Comment savons-nous que notre code fonctionne toujours ?
Les applications Web fonctionnent 24 heures sur 24, 7 jours sur 7, donc la question de savoir si mon programme est toujours en cours d'exécution me dérangera la nuit. Les tests unitaires m'ont aidé à acquérir suffisamment de confiance dans mon code pour pouvoir bien dormir.
Les tests unitaires sont un cadre permettant d'écrire des cas de test pour votre code et d'exécuter ces tests automatiquement. Le développement piloté par les tests est une approche de tests unitaires basée sur l'idée que vous devez d'abord écrire des tests et vérifier que ces tests peuvent détecter des erreurs, puis commencer seulement à écrire le code qui doit réussir ces tests. Lorsque tous les tests réussissent, la fonctionnalité que nous avons développée est terminée. L’intérêt de ces tests unitaires est que nous pouvons les exécuter à tout moment : avant d’archiver le code, après une modification majeure ou après le déploiement sur un système en cours d’exécution.
Tests unitaires PHP
Pour PHP, le framework de tests unitaires est PHPUnit2. Ce système peut être installé en tant que module PEAR en utilisant la ligne de commande PEAR : % pear install PHPUnit2.
Après avoir installé le framework, vous pouvez écrire des tests unitaires en créant des classes de test dérivées de PHPUnit2_Framework_TestCase.
Tests unitaires du module
J'ai découvert que le meilleur endroit pour commencer les tests unitaires est dans le module de logique métier de l'application. J'utilise un exemple simple : c'est une fonction qui additionne deux nombres. Pour commencer les tests, nous écrivons d’abord le cas de test comme indiqué ci-dessous.
Listing 1. TestAdd.php
<?phprequire_once 'Add.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestAdd extends PHPUnit2_Framework_TestCase{ function test1() { $this->assertTrue( add( 1, 2 ) == 3 ); } function test2() { $this->assertTrue( add( 1, 1 ) == 2 ); }}?>
Cette classe TestAdd a deux méthodes, qui utilisent toutes deux le préfixe test. Chaque méthode définit un test, qui peut être aussi simple que le listing 1 ou très complexe. Dans ce cas, nous affirmons simplement que 1 plus 2 est égal à 3 dans le premier test, et 1 plus 1 est égal à 2 dans le deuxième test.
Le système PHPUnit2 définit la méthode assertTrue(), qui est utilisée pour tester si la valeur de la condition contenue dans le paramètre est vraie. Nous avons ensuite écrit le module Add.php, qui a initialement produit des résultats incorrects.
Listing 2. Add.php
<?phpfunction add( $a, $b ) { return 0; }?>
Désormais, lors de l'exécution des tests unitaires, les deux tests échouent.
Listing 3. Échecs des tests
% phpunit TestAdd.phpPHPUnit 2.2.1 par Sebastian Bergmann.FFTime : 0,0031270980834961Il y a eu 2 échecs :1) test1(TestAdd)2) test2(TestAdd)ÉCHECS!!!Tests exécutés : 2, Échecs : 2, Erreurs : 0, Tests incomplets : 0.
Maintenant, je sais que les deux tests fonctionnent correctement. Par conséquent, la fonction add() peut être modifiée pour réellement faire la chose.
Les deux tests réussissent désormais.
<?phpfunction add( $a, $b ) { return $a+$b; }?>
Listing 4. Test réussi
% phpunit TestAdd.phpPHPUnit 2.2.1 par Sebastian Bergmann...Temps : 0,0023679733276367OK (2 tests)%
bien que Cet exemple de développement piloté par les tests est très simple, mais on peut comprendre ses idées. Nous avons d’abord créé le scénario de test et disposions de suffisamment de code pour exécuter le test, mais le résultat était erroné. Ensuite, nous vérifions que le test échoue effectivement, puis implémentons le code réel pour que le test réussisse.
Je trouve qu'au fur et à mesure que j'implémente du code, je continue d'ajouter du code jusqu'à ce que j'aie un test complet couvrant tous les chemins de code. À la fin de cet article, vous trouverez quelques conseils sur les tests à rédiger et comment les rédiger.
Tests de base de données
Après le test du module, des tests d'accès à la base de données peuvent être effectués. Les tests d'accès aux bases de données soulèvent deux problèmes intéressants. Tout d'abord, nous devons restaurer la base de données à un point connu avant chaque test. Deuxièmement, sachez que cette récupération peut endommager la base de données existante, nous devons donc tester sur une base de données hors production, ou faire attention à ne pas affecter le contenu de la base de données existante lors de l'écriture des cas de test.
Les tests unitaires de base de données démarrent à partir de la base de données. Pour illustrer ce problème, nous devons utiliser le modèle simple suivant.
Listing 5. Schema.sql
DROP TABLE IF EXISTS auteurs ; CREATE TABLE auteurs (id MEDIUMINT NOT NULL AUTO_INCREMENT, nom TEXT NOT NULL, PRIMARY KEY (id)) ;
Le listing 5 est une table d'auteurs et chaque enregistrement a un ID associé.
Ensuite, vous pouvez rédiger des cas de test.
Listing 6. TestAuthors.php
<?phprequire_once 'dblib.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestAuthors extends PHPUnit2_Framework_TestCase{ function test_delete_all() { $this->assertTrue( Authors::delete_all() ); } function test_insert() { $this->assertTrue( Authors::delete_all() ); $this->assertTrue( Authors::insert( 'Jack' ) ); function test_insert_and_get() { $this->assertTrue( Authors ::delete_all() ); $this->assertTrue( Authors::insert( 'Jack' ) ); $this->assertTrue( Authors::insert( 'Joe' ) ); ; $this->assertTrue( $found != null ); $this->assertTrue( count( $found ) == 2 }}?>
Cet ensemble de tests couvre la suppression d'auteurs de la table et l'insertion d'auteurs dans la table); Ainsi que des fonctions comme insérer l'auteur tout en vérifiant que l'auteur existe. Il s'agit d'un test cumulatif, que je trouve très utile pour trouver des bugs. En observant quels tests fonctionnent et lesquels ne fonctionnent pas, vous pouvez rapidement comprendre ce qui ne va pas et mieux comprendre les différences.
La version du code d'accès à la base de données PHP dblib.php qui a initialement provoqué l'échec est indiquée ci-dessous.
Listing 7. dblib.php
<?phprequire_once('DB.php'); class Authors{ public static function get_db() { $dsn = 'mysql://root:password@localhost/unitdb'; :Connect( $dsn, array() ); if (PEAR::isError($db)) { die($db->getMessage()); return $db; } public static function delete_all() { return false; } public static function insert( $name ) { return false; } public static function get_all() { return null }}?>
L'exécution de tests unitaires sur le code du listing 8 montrera que les trois tests échouent :
Listing 8. dblib. php
% phpunit TestAuthors.phpPHPUnit 2.2.1 par Sebastian Bergmann.FFFTime : 0,007500171661377Il y a eu 3 échecs :1) test_delete_all(TestAuthors)2) test_insert(TestAuthors)3) test_insert_and_get(TestAuthors)FAILURES!!!Tests exécutés : 3, échecs : 3, Erreurs : 0, Tests incomplets : 0,%
Nous pouvons maintenant commencer à ajouter le code pour accéder correctement à la base de données - méthode par méthode - jusqu'à ce que les 3 tests réussissent. La version finale du code dblib.php est présentée ci-dessous.
Listing 9. Complétez dblib.php
<?phprequire_once('DB.php');class Authors{ public static function get_db() { $dsn = 'mysql://root:password@localhost/unitdb'; ::Connect( $dsn, array() ); if (PEAR::isError($db)) { die($db->getMessage() ); return $db; } fonction statique publique delete_all() { $ db = Authors::get_db(); $sth = $db->prepare( 'DELETE FROM auteurs' ); $db->execute( $sth ); return true; Authors::get_db(); $sth = $db->prepare( 'INSERT INTO auteurs VALUES (null,?)' ); $db->execute( $sth, array( $name ) ); fonction statique get_all() { $db = Authors::get_db(); $res = $db->query( "SELECT * FROM auteurs" ); ) ) { $rows []= $row; } return $rows; }}?>
Test HTML
La prochaine étape du test de l'ensemble de l'application PHP consiste à tester l'interface HTML (Hypertext Markup Language) frontale. Pour effectuer ce test, nous avons besoin d'une page Web comme celle ci-dessous.
Listing 10. TestPage.php
<?phprequire_once 'HTTP/Client.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestPage extends PHPUnit2_Framework_TestCase{ function get_page( $url ) { $client = new HTTP_Client(); ->get( $url ); $resp = $client->currentResponse(); return $resp['body']; function test_get() { $page = TestPage::get_page( 'http://localhost/unit /add.php' ); $this->assertTrue( strlen( $page ) > 0 ); $this->assertTrue( preg_match( '/<html>/', $page ) == 1 ); ) { $page = TestPage::get_page( 'http://localhost/unit/add.php?a=10&b=20' ); $this->assertTrue( strlen( $page ) > 0 ); assertTrue( preg_match( '/<html>/', $page ) == 1 ); preg_match( '/<span id="result">(.*?)</span>/', $page, $out ); $this->assertTrue( $out[1]=='30' ); }}?>
Ce test utilise le module client HTTP fourni par PEAR. Je trouve cela un peu plus simple que la bibliothèque d'URL client PHP (CURL) intégrée, mais cette dernière peut également être utilisée.
Il existe un test qui vérifie la page renvoyée et détermine si elle contient du HTML. Le deuxième test demande la somme de 10 et 20 en plaçant la valeur dans l'URL demandée, puis vérifie le résultat dans la page renvoyée.
Le code de cette page est affiché ci-dessous.
Listing 11. TestPage.php
<html><body><form><input type="text" name="a" value="<?php echo($_REQUEST['a']); ?>" /> + <input type="text" name="b" value="<?php echo($_REQUEST['b']); ?>" /> =<span id="result"><?php echo($_REQUEST ['a']+$_REQUEST['b']); ?></span><br/><input type="submit" value="Ajouter" /></form></body></html >
Cette page est assez simple. Les deux champs de saisie affichent les valeurs actuelles fournies dans la requête. L'étendue des résultats montre la somme de ces deux valeurs. Le balisage marque toutes les différences : il est invisible pour l'utilisateur, mais visible pour les tests unitaires. Les tests unitaires n'ont donc pas besoin d'une logique complexe pour trouver cette valeur. Au lieu de cela, il récupère la valeur d'une balise spécifique. De cette façon, lorsque l'interface change, tant que l'étendue existe, le test réussira.
Comme auparavant, écrivez d’abord le scénario de test, puis créez une version défaillante de la page. Nous testons les échecs puis modifions le contenu de la page pour la faire fonctionner. Les résultats sont les suivants :
Listing 12. Echec du test, puis modification de la page
% phpunit TestPage.phpPHPUnit 2.2.1 par Sebastian Bergmann...Temps : 0.25711488723755OK (2 tests)%
Les deux tests peuvent réussir, ce qui signifie que le test code Cela fonctionne bien.
Lors de l'exécution de tests sur ce code, tous les tests s'exécutent sans problème, nous savons donc que notre code fonctionne correctement.
Mais tester le front-end HTML présente un défaut : JavaScript. Le code client HTTP (Hypertext Transfer Protocol) récupère la page, mais n'exécute pas le JavaScript. Donc, si nous avons beaucoup de code en JavaScript, nous devons créer des tests unitaires au niveau de l'agent utilisateur. La meilleure façon que j'ai trouvée pour obtenir cette fonctionnalité consiste à utiliser la fonctionnalité de couche d'automatisation intégrée à Microsoft® Internet Explorer®. Avec les scripts Microsoft Windows® écrits en PHP, vous pouvez utiliser l'interface COM (Component Object Model) pour contrôler Internet Explorer afin de naviguer entre les pages, puis utiliser les méthodes DOM (Document Object Model) pour rechercher des pages après avoir effectué des actions utilisateur spécifiques dans des éléments. .
C'est le seul moyen que je connaisse pour tester unitairement le code JavaScript frontal. J'admets que ce n'est pas facile à écrire et à maintenir, et ces tests sont facilement interrompus lorsque même de légères modifications sont apportées à la page.
Quels tests écrire et comment les écrire
Lors de l'écriture des tests, j'aime aborder les scénarios suivants :
Tous les tests positifs
Cet ensemble de tests garantit que tout fonctionne comme prévu.
Tous les tests négatifs
utilisent ces tests un par un pour garantir que chaque panne ou anomalie est testée.
Tests de séquence positive
Cet ensemble de tests garantit que les appels dans le bon ordre fonctionnent comme prévu.
Tests de séquence négative
Cet ensemble de tests garantit que les appels échouent lorsqu'ils ne sont pas effectués dans le bon ordre.
Tests de charge
Le cas échéant, un petit ensemble de tests peut être effectué pour déterminer que les performances de ces tests sont conformes à nos attentes. Par exemple, 2 000 appels devraient être terminés en 2 secondes.
Tests de ressources
Ces tests garantissent que l'interface de programmation d'application (API) alloue et libère correctement les ressources - par exemple, en appelant l'API basée sur les fichiers d'ouverture, d'écriture et de fermeture plusieurs fois de suite pour garantir qu'aucun fichier n'est encore ouvert.
Tests de rappel
Pour les API dotées de méthodes de rappel, ces tests garantissent que le code s'exécute normalement si aucune fonction de rappel n'est définie. De plus, ces tests peuvent également garantir que le code peut toujours s'exécuter normalement lorsque des fonctions de rappel sont définies mais que ces fonctions de rappel fonctionnent incorrectement ou génèrent des exceptions.
Voici quelques réflexions sur les tests unitaires. J'ai quelques suggestions sur la façon d'écrire des tests unitaires :
N'utilisez pas de données aléatoires
.Bien que générer des données aléatoires dans une interface puisse sembler une bonne idée, nous voulons éviter de le faire car ces données peuvent devenir très difficiles à déboguer. Si les données sont générées aléatoirement à chaque appel, il peut arriver qu'une erreur se produise dans un test mais pas dans un autre test. Si votre test nécessite des données aléatoires, vous pouvez les générer dans un fichier et utiliser ce fichier à chaque fois que vous l'exécutez. Avec cette approche, nous obtenons des données « bruyantes », mais nous pouvons toujours déboguer les erreurs.
Tests de groupe
Nous pouvons facilement accumuler des milliers de tests dont l'exécution prend plusieurs heures. Il n'y a rien de mal à cela, mais le regroupement de ces tests nous permet d'exécuter rapidement un ensemble de tests et de vérifier les problèmes majeurs, puis d'exécuter l'ensemble complet des tests la nuit.
Écriture d'API robustes et de tests robustes
Il est important d'écrire des API et des tests de manière à ce qu'ils ne se cassent pas facilement lorsque de nouvelles fonctionnalités sont ajoutées ou que des fonctionnalités existantes sont modifiées. Il n'existe pas de solution miracle universelle, mais une règle générale est que les tests qui « oscillent » (ils échouent parfois, réussissent parfois, encore et encore) doivent être rapidement abandonnés.
Conclusion
Les tests unitaires revêtent une grande importance pour les ingénieurs. Ils constituent la base du processus de développement agile (qui met fortement l'accent sur le codage car la documentation nécessite des preuves que le code fonctionne conformément aux spécifications). Les tests unitaires fournissent cette preuve. Le processus commence par des tests unitaires, qui définissent la fonctionnalité que le code doit implémenter mais ne le fait pas actuellement. Par conséquent, tous les tests échoueront initialement. Puis, lorsque le code est presque terminé, les tests réussissent. Lorsque tous les tests réussissent, le code devient très complet.
Je n'ai jamais écrit de code volumineux ni modifié un bloc de code volumineux ou complexe sans utiliser de tests unitaires. J'écris généralement des tests unitaires pour le code existant avant de le modifier, juste pour m'assurer de savoir ce que je casse (ou ne casse pas) lorsque je modifie le code. Cela me donne une grande confiance dans le fait que le code que je fournis à mes clients fonctionne correctement, même à 3 heures du matin.