Développement piloté par les tests.

Il ne semble pas évident au premier abord de faire des tests du code que l'on écrit. En général, on se borne à vérifier qu'il n'y a pas d'erreurs de syntaxe. On vérifie que les 3 ou 4 pages que l'on vient de modifier fonctionnent toujours de la même façon. Cette manière de fonctionner atteint rapidement ses limites au fur et à mesure que l'application grossie. Comment se rappeler que le code écrit aujourd'hui n'aura pas d'impact sur le code écrit il y a 6 mois ? Sans tests exhaustifs, c'est tout simplement impossible.

Il est hors de question de dérouler les tests "à la main". On va écrire des tests automatiques ou plus particulièrement des tests de non-régression, c'est-à-dire des tests qui permettent de vérifier que les modifications apportées à l'application n'ont pas apportées de bugs ou de modifications fonctionnelles. En fait ils permettent de s'assurer que l'application fonctionnera de la même façon.

En PHP, il y a de nombreuses bibliothèques pour se faciliter la tâche, notament PHPUnit ou Simple Test. En ayant un peu regardé les deux je me suis décidé à utiliser Simple Test.

Après avoir lu un livre sur l'Extrême Programming, il me parait plus judicieux d'écrire les tests avant de commencer à coder quoi que ce soit. Ca peut sembler étrange au premier abord mais en utilisant cette pratique de plus en plus souvent cela semble bien plus naturel et surtout plus facile que d'écrire le code et de s'assurer que les tests que l'on va mettre en place couvrent bien tout l'aspect fonctionnel du code.

Un p'tit exemple valant mieux qu'un long discours, imaginons que l'on veuille écrire une "interface" de connexion à une zone membre. On veut que l'utilisateur ayant un couple identifiant/mot de passe correct soit connecté à la zone et que les autres utilisateurs soient rejettés.

Code préliminaire: // login_test.php

<?php
require_once('../simpletest/web_tester.php');
require_once(
'../simpletest/reporter.php');
class 
TestOfLogging extends WebTestCase {
  function 
testAuth() {
    
$this->WebTestCase();
  }
}
$test =  new TestOfLogging();
$test->run(new HtmlReporter());
?>

Et on obtient le joli panneau suivant:

TestOfLogging

1/1 test cases complete: 0 passes, 0 fails and 0 exceptions.

C'est un peu normal puisque l'on a rien testé! Premier test:

<?php
require_once('../simpletest/web_tester.php');
require_once(
'../simpletest/reporter.php');

class 
TestOfLogging extends WebTestCase {
  function 
testAuth() {
    
$this->WebTestCase();
  }

  function 
testUnknownUser() {
    
$this->get ('http://localhost/xp/login1.php');
    
$this->setField('login'"inconnu");
    
$this->setField('password''mot-de-passe');
    
$this->clickSubmit('Ok');
    
$this->assertWantedPattern('/utilisateur inconnu/i');
  }
}

$test =  new TestOfLogging();
$test->run(new HtmlReporter());
?>

On obtient ceci:

TestOfLogging

Fail: testUnknownUser -> Pattern [/utilisateur inconnu/i] not detected in [String: <br /> <b>Warning</b>: require_once(./classes/Auth.php) [<a href='function.require-once'>function.require-once</a>]: failed to open stream: No such file or directory in <b>/home/nicolas/webspace/simp...] at [/home/nicolas/webspace/simpletests/tests/login_test.php line 15]
1/1 test cases complete: 0 passes, 1 fails and 0 exceptions.

C'est normal puisque l'on n'a pas encore écrit de code! Une fois le formulaire soumis on affiche le message "utilisateur inconnu". C'est ce message que l'on cherche dans le test

L'idée principal du développement piloté par les tests (écrire les tests puis coder) est d'écrire les tests fonctionnels dans un premier temps puis d'écrire le code minimal qui permette de valider le test. On ne cherche pas à faire le code le plus beau et le plus performant à cette étape. Au test d'après on peut factoriser des bouts de code pour améliorer la qualité générale. On n'aura l'assurance que l'on a rien cassé grâce aux tests. Donc pour passer le test, il suffit d'ajouter dans le script login1.php (qui devient login2.php), une ligne avec "Utilisateur inconnu". C'est loin d'être optimal et c'est même un peu moche comme code. On est confronté à un nouveau problème. La première fois que l'on arrive sur la page, on a le message "Utilisateur inconnu".

Lorsqu'on se trouve avec un "bug" ou un comportement non voulu, on commence par écrire le test correspondant et qui échoue nécessairement. Le voici (en tête de la fonction testUnknownUser):

<?php
$this
->assertNoUnWantedPattern('/utilisateur inconnu/i');
?>

Il nous faut maintenant écrire le code minimal qui permette de passer ce test. Il suffit d'ajouter le code suivant: (login3.php)

<?php
if (isset($_POST['sub'])) {
  echo 
'<p>Utilisateur inconnu</p>';
}
?>

Et on arrive au résultat attendu:

TestOfLogging

1/1 test cases complete: 2 passes, 0 fails and 0 exceptions.

Attaquons-nous à la partie "utilisateur reconnu"! On va créer un objet User qui regroupera toutes les propriétés d'un utilisateur et en particulier le fait que son identification est correcte. Les tests dans login.php se résumeront à appeler une méthode vérifiant la validité du couple login/mot de passe pour pouvoir accéder à la zone membre. On va supposer dans un premier temps que l'objet existe et que l'on peut l'utiliser de la manière suivante:

<?php
require_once('./classes/User.php');
$is_validfalse;

if (isset(
$_POST['sub'])) {
  
$user = new User();
  
$is_valid $user->isValid($_POST['login'], $_POST['password']);
}
if (
$is_valid) {
  echo 
'Vous êtes connecté<br>';
} else {
  
// affichage du formulaire
}
?>

Ecrivons maintenant les tests vérifiant qu'un utilisateur reconnu (login: Lebon et mot de passe: Secret) peut se connecter:

<?php
require_once('../simpletest/web_tester.php');
require_once(
'../simpletest/reporter.php');
require_once(
'../classes/User.php');

class 
TestOfLogging extends WebTestCase {
  function 
testAuth() {
    
$this->WebTestCase();
  }

  function 
testUnknownUser() {
    
$this->get ('http://localhost/xp/login.php');
    
$this->assertNoUnWantedPattern('/utilisateur inconnu/i');
    
$this->setField('login'"inconnu");
    
$this->setField('password''mot-de-passe');
    
$this->clickSubmit('Ok');
    
$this->assertWantedPattern('/utilisateur inconnu/i');
  }

  function 
testKnownUserCanLogIn() {
    
$this->get ('http://localhost/xp/login.php');
    
$this->setField('login''Lebon');
    
$this->setField('password''Secret');
    
$this->clickSubmit('Ok');
    
$this->assertWantedPattern('/connecté/i');
  }
}

$test =  new TestOfLogging();
$test->run(new HtmlReporter());
?>

En testant, on obtient ça:

Warning: require_once(../classes/User.php) [function.require-once]: failed to open stream: No such file or directory in /var/www/xp/test/login_test.php on line 4
Fatal error: require_once() [function.require]: Failed opening required '../classes/User.php' (include_path='.:/usr/share/php') in /var/www/xp/test/login_test.php on line 4

Après avoir créer la classe User, on obtient:

TestOfLogging

Fail: testUnknownUser -> Pattern [/utilisateur inconnu/i] not detected in [String: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html lang="fr"> <head> <title>Zone membre</title> </head> <body> <h1>Zone membre</h1> <br /> <b>Fatal...] at [/var/www/xp/test/login_test.php line 17]
Fail: testKnownUserCanLogIn -> Pattern [/connecté/i] not detected in [String: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html lang="fr"> <head> <title>Zone membre</title> </head> <body> <h1>Zone membre</h1> <br /> <b>Fatal...] at [/var/www/xp/test/login_test.php line 25]
1/1 test cases complete: 1 passes, 2 fails and 0 exceptions.

On va donc écrire une classe User minimale permettant de passer le nouveau test. Elle pourrait ressembler à ça:

<?php
class User 
{
  public function 
__construct() {
  }

  public function 
isValid($login$password) {
    if ((
trim($login)=='Lebon') && (trim($password)=='Secret')) {
      return 
true;
    } else {
      return 
false;
    }
  }
}
?>

Et on obtient alors le beau panneau vert suivant:

TestOfLogging

1/1 test cases complete: 3 passes, 0 fails and 0 exceptions.

Tout le code écrit n'est pas forcément optimal mais on l'idée générale du développement dirigé par les tests. On va pouvoir modifier le code sans risquer d'avoir des régressions fonctionnelles ce qui est un des buts principaux de la méthode.

  • Version finale de la page d'accès à la zone membre: login.php
  • Version finale du script de tests: login_test.php
  • Version finale de la classe User: User.php

Haut de page