Fermer
DéveloppementWeb

Introduction aux tests unitaires en PHP avec PHPUnit

Le Galaxy Fold reste extrêmement fragile, et Samsung vous met en garde

N’avez-vous jamais corrigé un problème dans votre code source, et entrainé malheureusement un autre bug dans vos applications ? Répondre jamais serait faux. En effet, c’est un problème récurrent. C’est pourquoi dans cet article nous allons voir comment éviter ce genre de désagrément en mettant en œuvre des tests unitaires, et plus particulièrement en utilisant PHPUnit.

Présentation

C’est une situation familière : vous développez un projet depuis de nombreuses heures et vous sentez que vous tournez en rond… En effet, vous corrigez un bug et un autre survient. Parfois, c’est le même que celui que vous avez trouvé il y a trente minutes, et parfois c’est nouveau, mais vous savez juste qu’ils sont liés. Pour la plupart des développeurs Web, les moyens de débogage sont soit l’actualisation du site Web, soit la déclaration d’un bon nombre de trace afin de détecter le problème.

N’ai-je pas raison ? Pour toutes ces frustrations passées, ne vous êtes vous jamais demandé s’il y avait une meilleure façon de faire ? Et bien, vous avez de la chance ! Il y en a une, et elle n’est pas aussi difficile que vous pourriez le penser. Faire des tests unitaires de votre application ne vous fera pas seulement réduire certains maux de tête que vous pourriez avoir pendant le développement, mais il peut aboutir à un code plus facile à maintenir, vous permettant de faire des changements plus audacieux (comme modification majeure) sans hésitation.

La clé de la compréhension des tests unitaires est de définir ce qu’on entend par “Unitaire”. Une “unité” est tout simplement un morceau de fonctionnalité qui effectue une action spécifique où vous pouvez tester le résultat. Un “test unitaire” est donc un test de cohérence pour s’assurer que le morceau de fonctionnalité fait ce qu’il doit censé faire.

Une fois que vous avez écrit votre série de tests, chaque fois que vous effectuez une modification dans votre code, tout ce que vous avez à faire est d’exécuter l’ensemble des tests et regarder qu’il n’y a pas de problème. De cette façon, vous pouvez être rassuré que vous n’avez pas accidentellement cassé une autre partie de votre application.

Le gros problème avec un langage de programmation tel que PHP, c’est qu’il est flexible, permissif et facile à appréhender.  Ainsi, beaucoup de développeurs PHP qui n’ont pas de formation spécifique en programmation ou qui sont peu expérimentés ne perçoivent pas l’importance d’un processus de développement, et de test en particulier. Or, l’absence de tests correctement implémentés engendre plusieurs problématiques telles qu’un code source non testé en profondeur, un code source non robuste et non réutilisable par un autre développeur…

Démystifier les mythes de tests unitaires

Je suis sûr que vous êtes assis en train de vous dire, “si ce genre de tests unitaires sont si impressionnants, pourquoi tout le monde ne les met pas en application dans toutes leurs applications ?” Il y a quelques réponses à cette question, mais aucune ne constitue une bonne excuse. Voyons à travers les ressentis et expliquons pourquoi il n’y pas de réelle raison d’en écrire.

Un temps non négligeable ?

Une des plus grandes préoccupations dès lors que l’on souhaite écrire des tests unitaires c’est qu’ils sont longs à mettre oeuvre. Bien sûr, quelques-uns des environnements de développement vont générer automatiquement un ensemble de tests de base pour vous, mais écrire des tests complets pour votre code prend un certain temps. Comme beaucoup des meilleures pratiques en développement, un petit investissement de temps pour les générer peut vous faire économiser beaucoup de temps sur la durée de votre projet. Ecrire une suite de tests solide en est certainement une. Par ailleurs, vous avez probablement déjà testé votre code en allant sur votre site et en cliquant chaque fois sur chacune des nouvelles fonctionnalités que vous venez de développer… L’exécution d’une suite de tests mise en place peut être beaucoup plus rapide que tester manuellement toutes vos fonctionnalités.

Il n’est pas nécessaire de tester … mon code fonctionne déjà !

Une autre déclaration commune que l’on entend souvent des développeurs quant à l’écriture d’un ensemble de tests est que l’application fonctionne, il n’y a donc pas de réel besoin de la tester. Ils connaissent leur code et l’enchaînement des méthodes dans le code, et corriger un bug peut parfois se faire en quelques secondes. Mais, ajoutez un nouveau développeur dans le développement de votre application, et vous pourriez commencer à voir pourquoi ces tests seraient une bonne idée… Sans eux, imaginez le « novice » en train de développer une nouvelle fonctionnalité, tout en apportant des modifications sur des fonctionnalités existantes, vous n’êtes pas à l’abri d’un problème. Avec un test reproductible, certains de ces bugs peuvent être évités.

Ce n’est pas forcément amusant

La dernière raison pour laquelle les développeurs n’aiment pas écrire des tests, c’est que les écrire n’est pas très passionnant. Les développeurs, de par leur nature, veulent résoudre les problèmes. En conséquence, ils voient l’écriture de tests comme ennuyeuse, et qu’il s’agit plus d’une tâche qu’ils pourraient faire une fois le gros du travail fait. Or, les tests unitaires doivent se faire tout au long du développement de votre application afin de maintenir un processus cohérent et fonctionnel. Je suppose que tout le monde est d’accord pour dire qu’il n’est pas vraiment passionnant de gaspiller des heures à la recherche d’un bug nuisible, et qu’en mettant en place des tests unitaires avec un petit effort, pourrait vous éviter beaucoup de frustration.

Un exemple

Nous voilà dans la partie la plus intéressante puisque nous allons partir d’un exemple pratique afin de bien comprendre comment mettre œuvre des tests unitaires, et pourquoi ceux-ci sont très utiles. Pour le bien de mes exemples, je vais seulement utiliser l’un des plus populaires outils de tests unitaires, PHPUnit. Cette application a été développée par Sebastian Bergmann, et elle fournit une excellente série de fonctionnalités pour vous aider à rapidement mettre en place des tests dans votre application. Voici en bref quelques qualités de ce framework Open Source :

  • Une syntaxe simple, facile à comprendre et à retenir
  • Un grand nombre de méthodes de tests
  • Une Organisation et exécution des test flexibles
  • Un utilitaire en ligne de commande complet
  • Support de Selenium RC (tests fonctionnels)
  • Journalisation des tests aux format XML, JSON, TAP ou dans une base de données

Si vous n’avez pas encore installé PHPUnit, le plus simple est de l’ajouter à l’aide des commandes PEAR, décrites dans le listing ci-dessous :

[sourcecode language=”bash”]
pear channel-discover pear.phpunit.de  
pear channel-discover components.ez.no  
pear channel-discover pear.symfony-project.com  
pear install phpunit/PHPUnit
[/sourcecode]  

Si tout va bien, vous aurez tous les outils dont vous avez besoin d’installer. L’installeur PEAR va récupérer toutes les dépendances que vous pourriez avoir besoin pour son exécution.

Si vous voulez l’installer manuellement, il y a des instructions dans le manuel de PHPUnit. Cependant, notez que vous aurez besoin d’une installation de PEAR pour utiliser PHPUnit. Cette dernière s’appuie sur plusieurs autres bibliothèques PEAR, et donc si vous n’avez pas les bonnes dépendances, des erreurs vous seront retournées lorsque vous essaierez d’exécuter PHPUnit.

Ecrire votre premier scénario test

Avec PHPUnit, la chose la plus basique que vous pouvez écrire est un scénario de test. Ce dernier va juste effectuer différents tests sur une classe donnée, tous liés à la même fonctionnalité. Il y a quelques règles que vous aurez besoin de connaître lorsque vous écrivez votre scénario avec PHPUnit :

  • Le plus souvent, vous aurez envie d’avoir votre classe de test héritant de la classe PHPUnit_Framework_TestCase. Cela vous donne accès pour vos tests à des fonctionnalités intégrées comme les méthodes setUp() et tearDown(). Nous verrons en détail par la suite ces deux méthodes
  • Le nom de la classe de test doit être quasiment identique au nom de la classe que vous testez. Par exemple, pour tester la classe User, vous devriez utiliser UserTest
  • Lorsque vous créez les méthodes de test, vous devez toujours les préfixer par “test” (comme pour testUserCreate()). Toutes les méthodes doivent être publiques. Vous pouvez avoir des méthodes privées dans vos tests, mais celles-ci ne seront pas exécutées par PHPUnit comme des méthodes de test
  • Aucun paramètre doit-être déclaré dans vos méthodes de test. Quand vous les écrirez, vous aurez besoin de les rendre aussi indépendantes que possible. Cela peut être très frustrant parfois, mais cela permet de faire des tests plus efficaces

Nous allons commencer par lancer des tests sur quelques fonctionnalités de notre classe User. Il s’agit d’exemple assez basique afin de garder les choses simples.

[sourcecode language=”PHP”]
<?php
class User {
var $name = null;
public __construct($name = null) {
$this->name = $name;
}

public function getUserName() {
if ($this->name == null){
throw new Exception(“Veuillez préciser un nom !”);
}

return $this->name;
}

public function returnSampleObject() {
return $this;
}
}
?>
[/sourcecode]

Ainsi, par exemple, si nous devions tester cette fonctionnalité pour demander le nom de notre utilisateur, notre test pourrait ressembler à :

[sourcecode language=”PHP”]
<?php
require_once(‘User.php’);

class UserTest extends PHPUnit_Framework_TestCase {
public function setUp(){}
public function tearDown(){}

public function testUserName(){
// test pour s’assurer que l’objet à un nom valide
$userName = ‘blognt’;
$user = new User($userName);
$this->assertTrue($user->getUserName() !== false);
}
}
?>
[/sourcecode]

Vous remarquerez que la classe hérite de la classe mère de PHPUnit venant enrichir notre classe de test. Les deux premières méthodes (setUp() et tearDown()) sont des exemples de ce genre de fonctionnalités intégrées. Ce sont des fonctions qui permettent de fournir une fixture, autrement dit un “contexte d’exécution” commun à toutes les méthodes de test. Dans la pratique, il peut s’agir d’une connexion à une base de données, ou toute autre instruction à vocation collective. La méthode setUp() se comporte en constructeur et s’exécute avant la méthode de test, tandis que tearDown() s’exécute après celle-ci, faisant office de destructeur. En dépit d’être pratique, nous n’allons pas encore les utiliser. L’objectif réel est notre méthode testUserName(). Celle-ci créée une nouvelle instance de notre classe User, et appelle la méthode getUserName().

Maintenant, nous allons entrer plus en détail de notre test. Vous devez vous demander à quoi sert la méthode assertTrue() ? C’est une énième fonction utilitaire de PHPUnit. Elle permet de vérifier si une expression booléenne est vraie. D’autres fonctions utilitaires vous permettent de tester les propriétés des objets, l’existence de fichiers, la présence d’une clé donnée dans un tableau, pour n’en nommer que quelques-unes. Dans notre cas, nous voulons être sûrs que le résultat de getUserName() n’est pas faux (false). Cela voudrait dire que pour une raison quelconque, notre utilisateur n’a pas de nom.

Exécutons nos tests

Pour lancer l’exécution de vos tests il vous suffit d’appeler l’exécutable phpunit en pointant vers vos tests. Le listing ci-dessous montre un exemple d’appel de notre test décrit ci-dessus.

[sourcecode language=”bash”]
phpunit /path/to/tests/UserTest.php
[/sourcecode]

Simple, non ? La sortie est toute aussi fondamentale : pour chacun des tests de votre scénario, PHPUnit les éxécute et rassemble quelques statistiques comme le taux de réussite, d’échec, le nombre de tests et d’affirmations faites. Voici un exemple de la sortie de l’exécution lancée précédemment est affiché :

[sourcecode language=”bash”]
PHPUnit 3.4 by Sebastian Bergmann
.
Time: 1 second
Tests: 1, Assertions: 1, Failures 0
[/sourcecode]

Pour chaque test qui est exécuté, vous verrez un indicateur de résultat fourni, “.” si votre test est réussi (comme dans notre exemple), un “F” s’il y a eu un problème, un “I” si le test est marqué comme incomplet, ou un “S” s’il est sauté.

Par défaut, PHPUnit est configuré afin de passer dans l’ensemble de vos tests et faire un rapport avec le total des statistiques.

Notre test retourne un résultat valide puisque nous savons que nous avons fourni correctement le paramètre, notre méthode fonctionne comme prévu. Mais, vous devez aussi vous assurer du résultat de votre test dès lors qu’il y a un problème. Qu’arrive t-il si votre utilisateur n’a pas de nom ? La méthode lève t-elle une exception comme nous aimerions qu’elle le fasse ? Soyez sûr que quand vous écrivez des tests de bien avoir des vérifications que ce soit pour un résultat positif ou négatif.

Par exemple, dans le cas de la méthode getUserName() de notre classe, fournir un nom invalide pour la récupération du nom de l’utilisateur va lever une exception. La gestion des exceptions avec PHPUnit va au delà des explications de cet article, mais je vous recommande la lecture du chapitre correspondant de la documentation de PHPUnit, si vous voulez creuser le sujet.

PHPUnit propose plusieurs assertions qui peuvent vous aider à tester les résultats de toutes sortes d’appels dans vos applications. Parfois, vous devrez faire preuve d’un peu plus de créativité afin de tester une partie plus complexe, mais les tests fournis par PHPUnit couvrent la majorité des cas que vous souhaitez tester. Voici une liste de quelques-uns des plus courants :

  • assertEquals($valeur, $valeurAttendu [, $message]) vérifie si les valeurs sont identiques (ex: assertEquals(0, 1) lèvera une erreur)
  • assertFalse($condition [, $message]) vérifie si la condition est fausse (ex: assertFalse(0==0) lèvera une erreur)
  • assertNotEquals($valeur, $valeurAttendu [, $message]) vérifie si les valeurs sont différentes (ex: assertNotEquals(1, 1) lèvera une erreur)
  • assertNotNull($valeur [, $message]) vérifie si la valeur n’est pas null (ex: assertNotNull(null) lévera une erreur)
  • assertNull($valeur [, $message]) vérifie si la valeur est null (ex: assertNull("toto") lèvera une erreur)
  • assertSame($valeur, $valeurAttendu [, $message]) vérifie si les paramètres sont de même type et même valeur (i.e. la même instance s’il s’agit d’objet) (ex: assertSame(0, false) et assertSame(new MaClasse(), new MaClasse()) lèveront une erreur)
  • assertNotSame($valeur, $valeurAttendu [, $message]) inverse de assertSame()
  • assertTrue($condition [, $message]) vérifie si la condition est vraie (ex: assertTrue(1==0) lèvera une erreur)

Par exemple, disons que nous récupérons un objet d’une méthode (comme celui retourné par notre méthode returnSampleObject()) et nous voulons voir si c’est une instance d’une classe particulière. Voici ce test particulier :

[sourcecode language=”PHP”]
<?php
function testIsRightObject() {
$user = new User(‘blognt’);
$returnedObject = $user->returnSampleObject();
$this->assertType(‘user’, $returnedObject);
}
?>
[/sourcecode]

Notre méthode a été écrite afin de retourner l’objet, ce test devrait donc passer sans problème. Nous pouvons continuer sans crainte !

Une affirmation par test

Comme pour n’importe quel domaine de développement, il y a quelques bonnes pratiques lors de l’écriture des tests unitaires. Une pratique importante est l’idée de “un test, une affirmation”. Chacun de nos exemples de test ont suivi ce principe. Certains développeurs, cependant, pense que cela peut être un gaspillage d’espace, voici un exemple :

[sourcecode language=”PHP”]
<?php
public function testIsString(){
$string = “Yohann Poiron”;
$this->assertGreaterThan(0,strlen($string));
$this->assertContains(“42”,$string);
}
?>
[/sourcecode]

L’exemple du listing ci-dessus présente des tests pour deux affirmations. La première vérifie que la chaîne n’est pas vide (longueur supérieure à 0), la seconde vérifie que la chaîne contient le nombre “42”. Comme vous pouvez imaginer cela parait compliqué… De plus, le test échouera même si la chaîne avait été par exemple, “quarante-deux”. Vous auriez exactement le même résultat si la chaîne était vide, ce qui pourrait être causé par un bug totalement différent. En revanche, la cause d’échec peut être trompeuse et pourrait causer une certaine confusion quant à ce qui s’est réellement passé dans l’application.

D’autres exemples

Tests sur la sortie

Comme annoncé précédemment PHPUnit fournit des extensions à la classe de base afin de l’enrichir. Ainsi nous avons la classe PHPUnit_Framework_TestCase, que nous permet de réaliser des tests de non-régressions mais aussi de performance.

Le code ci-dessous montre comment utiliser la sous-classe PHPUnit_Extensions_OutputTestCase et plus particulièrement la méthode expectOutputString() pour définir les résultats attendus. Si cette sortie attendue n’est pas générée, le test sera retourné comme un échec.

[sourcecode language=”PHP”]
<?php
require_once ‘PHPUnit/Extensions/OutputTestCase.php’;

class OutputTest extends PHPUnit_Extensions_OutputTestCase {
public function testExpectFooActualFoo() {
$this->expectOutputString(‘foo’);
print ‘foo’;
}

public function testExpectBarActualBaz() {
$this->expectOutputString(‘bar’);
print ‘baz’;
}
}
?>
[/sourcecode]

Le retour depuis la console est affiché ici :

[sourcecode language=”bash”]
phpunit OutputTest
PHPUnit 3.2.10 by Sebastian Bergmann.

.F

Time: 0 seconds

There was 1 failure:

1) testExpectBarActualBaz(OutputTest)
Failed asserting that two strings are equal.
expected string <bar>
difference < x>
got string <baz>

FAILURES!
Tests: 2, Failures: 1.
[/sourcecode]

Tests de performance

Vous pouvez étendre votre classe de test avec la classe PHPUnit_Extensions_PerformanceTestCase pour tester si l’exécution d’un appel de fonction ou de méthode, par exemple, dépasse un délai déterminé.

Le listing ci-dessous montre comment utiliser cette classe et en particulier la méthode setMaxRunningTime() qui nous permet de régler le temps de fonctionnement maximum pour le test. Si le test n’est pas exécuté dans ce délai, il sera compté comme un échec.

[sourcecode language=”PHP”]
<?php
require_once ‘PHPUnit/Extensions/PerformanceTestCase.php’;

class PerformanceTest extends PHPUnit_Extensions_PerformanceTestCase {
public function testPerformance() {
$this->setMaxRunningTime(5);
sleep(1);
}
}
?>
[/sourcecode]

Tests de base de données

Après avoir mis en place une série de tests et que vous trouvez ça très pratique, vous voudrez sûrement réaliser des tests unitaires sur votre base de données. Pour cela, une extension a été créée pour fournir un moyen facile de placer dans votre base de données des « sondes » pour que lorsque vous exécutez des instructions sur votre base de données, de s’assurer que les données attendues se trouve bien dans la base de données.
La meilleure façon est d’hériter de la classe PHPUnit_Extensions_Database_TestCase. Cette dernière fournit les fonctionnalités pour créer une connexion à la base et différents tests vous permettant de savoir si les données insérées correspondent à vos attentes. Dans le listing ci-dessous, vous pouvez voir des exemples des implémentations des méthodes getConnection() et getDataSet().

[sourcecode language=”PHP”]
<?php
require_once ‘PHPUnit/Extensions/Database/TestCase.php’;

class DatabaseTest extends PHPUnit_Extensions_Database_TestCase {
protected function getConnection() {
$pdo = new PDO(‘mysql:host=localhost;dbname=testdb’, ‘root’, ”);
return $this->createDefaultDBConnection($pdo, ‘testdb’);
}

protected function getDataSet() {
return $this->createFlatXMLDataSet(dirname(__FILE__).’/_files/file.xml’);
}
}
?>
[/sourcecode]

La méthode getConnection() doit retourner une instance de l’interface PHPUnit_Extensions_Database_DB_IDatabaseConnection. La méthode createDefaultDBConnection() peut être utilisée pour retourner une connexion à la base. Elle accepte un objet PDO en tant que premier paramètre et le nom du schéma que vous testez comme second paramètre.

La méthode getDataSet() doit retourner une implémentation de l’interface PHPUnit_Extensions_Database_DataSet_IDataSet. Il existe actuellement trois différents jeux de données disponibles dans PHPUnit.

Les frameworks supportant les tests unitaires

Plusieurs frameworks populaires basés sur PHP (comme Zend Framework et Symfony) ont inclus la possibilité d’écrire des tests unitaires au sein même de leur différentes fonctionnalités. Puisque les frameworks basés sur le MVC impliquent un peu plus que ce que vous trouveriez dans un simple script PHP ou une bibliothèque, ils ont fourni des utilitaires afin d’écrire des tests.

Pour mieux comprendre, je vous propose un exemple dans le code ci-dessous qui va nous permettre de comprendre un test utilisant le framework Zend, afin de vérifier l’acheminement d’une URL à un contrôleur.

[sourcecode language=”PHP”]
<?php
class CommentControllerTest extends Zend_Test_PHPUnit_ControllerTestCase {
public function setUp() {
parent::setUp();
}
public function tearDown() {
parent::tearDown();
}
public function appBootstrap() {
$this->frontController->registerPlugin(new Initializer(‘test’));
}
public function testGoHome() {
$this->dispatch(‘/home’);
$this->assertController(‘home’);
}
}
?>
[/sourcecode]

Bien sûr, cet exemple est un peu idiot, puisque nous évaluons une fonctionnalité interne au framework plutôt que notre propre code, mais il s’agit seulement de vous fournir une idée. Votre test hérite d’une classe de test différente, Zend_Test_PHPUnit_ControllerTestCase, afin qu’il sache comment faire pour tester un contrôleur du framework Zend. Cependant, vous remarquerez que nous utilisons toujours PHPUnit pour effectuer les tests. Vous y retrouverez la plupart des tests, mais vous aurez également d’autres méthodes apportées par Zend, telles que la méthode assertController. Vous pouvez trouver lra documentation complète du composant Zend_Test dans le manuel du framework.

Tests Driven Development

Il serait négligent de ne pas mentionner une technique de plus en plus utilisée par de nombreux développeurs : le Test Driven Development. Le développement piloté par les tests (TDD) est une technique utilisée pendant le développement. L’idée fondamentale derrière un TDD est que vous écrivez un “premier” test, avant même qu’une seule ligne de code dans votre application ne soit écrite. Mais attendez, comment savez-vous ce qu’il faut mettre dans les tests sans avoir écrit une seule ligne de code ? Et bien, c’est justement le principe de cette technique.

Avec un développement TDD, vous écrivez le test pour vérifier une fonctionnalité prévue, et ensuite vous écrivez le code pour correspondre au mieux à votre fonctionnalité. Lorsque vous commencez et que vous avez votre première série de tests, évidemment ils échouent tous. Dès lors que vous allez écrire vos premières lignes de code, vous travailler jusqu’à ce que le scénario de test établi soit complètement “vert”, autrement dit que tout passe. Cette méthode vous permet de vous concentrer davantage sur les premières conditions, plutôt que de vous perdre inutilement dans votre code.

Cette méthode peut être difficile pour un novice dans l’écriture de tests unitaires. Dans un premier temps, je vous recommande d’écrire votre code, de sorte que vous puissiez en apprendre d’avantage sur les tests et sur l’utilisation de PHPUnit. Ensuite, si vous voulez faire le grand saut, vous pourrez commencer votre prochain projet avec un développement en TDD. Soyez conscient que votre première peut être lente. Heureusement, vous pourrez utiliser vos connaissances acquises à l’aide de PHPUnit, vous permettant ainsi de mieux aborder les différents tests à mettre en oeuvre.

[download id=”6600″]

Conclusion

C’est ainsi que ce termine ce mini tutoriel qui vous aura permis de découvrir comment mettre en place des tests unitaires en PHP au sein de vos applications à l’aide de PHPUnit.

Bien qu’il y ait une multitude de sujets que je n’ai pas abordés, j’ai essayé de vous donner un bon point de départ pour commencer à écrire vos propres tests. Même si c’est juste quelques tests ici et là, vous pourrez diminuer vos craintes de bugs quant au futur développement. Après quelques essais, vous devriez ne plus imaginer de développer une application sans appliquer une méthodologie rigoureuse de tests.

Faites-vous des tests unitaires en PHP ? Si oui, avec quel(s) outil(s) ? PHPUnit ? Venez en parler…

Mots-clé : développement WeboutilsPHPPHPUnittests unitaires
Yohann Poiron

L’auteur Yohann Poiron

J’ai fondé le BlogNT en 2010. Autodidacte en matière de développement de sites en PHP, j’ai toujours poussé ma curiosité sur les sujets et les actualités du Web. Je suis actuellement engagé en tant qu’architecte interopérabilité.