Test unitaire

Le principe du test unitaire est de vérifier le fonctionnement d'une partie de code, en testant tous les cas. Pour cela, on injecte des données connues en entrée et l'on vérifie que le résultat retourné est bien celui attendu. Il s'agit d'un fonctionnement par assertion.

Elaboration des tests

Les tests doivent permettre de vérifier tous les cas possible, c'est à dire aussi bien les résultats valides que ceux qui provoquent des erreurs. Les règles de l'art voudraient que les tests unitaires soient élaborés avant même que le squelette du code n'ait été écrit, nous allons donc procéder ainsi.

Je prendrais comme exemple une classe de calculatrice simplifiée (les 4 opérations basiques) en PHP, qui comporte bien sûr de grosses lacunes afin que nos tests soient représentatifs. Tout d'abord, nous allons écrire les tests pour chaque opération. Pour nous simplifier la tâche, nous utiliserons chaque fois le même jeu de données.

Addition

  • 1, 1doit retourner 2
  • 9, 3 doit retourner 12
  • 7, 0 doit retourner 7
  • 11, a doit retourner "erreur"
  • f, 2 doit retourner "erreur"
  • j, d doit retourner "erreur"

Soustraction

  • 1, 1doit retourner 0
  • 9, 3 doit retourner 6
  • 7, 0 doit retourner 7
  • 11, a doit retourner "erreur"
  • f, 2 doit retourner "erreur"
  • j, d doit retourner "erreur"

Multiplication

  • 1, 1doit retourner 1
  • 9, 3 doit retourner 27
  • 7, 0 doit retourner 0
  • 11, a doit retourner "erreur"
  • f, 2 doit retourner "erreur"
  • j, d doit retourner "erreur"

Division

  • 1, 1doit retourner 1
  • 9, 3 doit retourner 3
  • 7, 0 doit retourner "erreur"
  • 11, a doit retourner "erreur"
  • f, 2 doit retourner "erreur"
  • j, d doit retourner "erreur"

Le script de test

PHPUnit propose différentes assertions, nous nous bornerons à tester les égalités. Nous allons suivre l'exemple Writing tests for PHPUnit pour créer la classe de test:

<?php
require_once '/var/www/math/calc.php'; // Il s'agit du fichier de notre classe "calc"
require_once 'PHPUnit/Framework.php';
 
class calcTest extends PHPUnit_Framework_TestCase {
 
   public function testSum() {
        $this->assertEquals(2, calc::sum(1, 1));
        $this->assertEquals(12, calc::sum(9, 3));
        $this->assertEquals(7, calc::sum(7, 0));
        $this->assertEquals('erreur', calc::sum(11, 'a'));
        $this->assertEquals('erreur', calc::sum('f', 2));
        $this->assertEquals('erreur', calc::sum('j', 'd'));
    }
 
   public function testMinus() {
        $this->assertEquals(0, calc::minus(1, 1));
        $this->assertEquals(6, calc::minus(9, 3));
        $this->assertEquals(7, calc::minus(7, 0));
        $this->assertEquals('erreur', calc::minus(11, 'a'));
        $this->assertEquals('erreur', calc::minus('f', 2));
        $this->assertEquals('erreur', calc::minus('j', 'd'));
    }
 
   public function testMulti() {
        $this->assertEquals(1, calc::multi(1, 1));
        $this->assertEquals(27, calc::multi(9, 3));
        $this->assertEquals(0, calc::multi(7, 0));
        $this->assertEquals('erreur', calc::multi(11, 'a'));
        $this->assertEquals('erreur', calc::multi('f', 2));
        $this->assertEquals('erreur', calc::multi('j', 'd'));
    }
 
   public function testDivid() {
        $this->assertEquals(1, calc::divid(1, 1));
        $this->assertEquals(3, calc::divid(9, 3));
        $this->assertEquals('erreur', calc::divid(7, 0));
        $this->assertEquals('erreur', calc::divid(11, 'a'));
        $this->assertEquals('erreur', calc::divid('f', 2));
        $this->assertEquals('erreur', calc::divid('j', 'd'));
    }
 
}
?>

La classe Calculatrice

<?php
class calc {
 
	function sum($arg1, $arg2) {
		$res = $arg1 + $arg2;
		return $res;
	}
 
	function minus($arg1, $arg2) {
		$res = $arg1 - $arg2;
		return $res;
	}
 
	function multi($arg1, $arg2) {
		$res = $arg1 * $arg2;
		return $res;
	}
 
	function divid($arg1, $arg2) {
		if ($arg2 == 0) return 'erreur';
		$res = $arg1 / $arg2;
		return $res;
	}
 
}
?>

Nous pouvons maintenant lancer le test:

$ ./phpunit testCalc.php
PHPUnit 3.4.9 by Sebastian Bergmann.
 
FFFF
 
Time: 0 seconds, Memory: 4.75Mb
 
There were 4 failures:
 
1) testCalc::testSum
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-erreur
+11
 
/usr/share/PHPUnit/testCalc.php:11
 
2) testCalc::testMinus
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-erreur
+11
 
/usr/share/PHPUnit/testCalc.php:20
 
3) testCalc::testMulti
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-erreur
+0
 
/usr/share/PHPUnit/testCalc.php:29
 
4) testCalc::testDivid
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-erreur
+0
 
/usr/share/PHPUnit/testCalc.php:39
 
FAILURES!
Tests: 4, Assertions: 17, Failures: 4.

Le résultat parle de lui-même, toutes nos fonctions ne remplissent pas les conditions voulues, les tests ont tous échoué. Notez que PHPUnit abandonne un test sitôt qu'une assertion échoue. En effet, il n'y a eu que 17 assertions testées sur les 24.

Test

Nous allons donc modifier les fonctions pour qu'elles passent les tests et soient donc conformes à la demande:

<?php
class calc {
 
	function sum($arg1, $arg2) {
		if (!is_numeric($arg1) || !is_numeric($arg2)) return 'erreur';
		$res = $arg1 + $arg2;
		return $res;
	}
 
	function minus($arg1, $arg2) {
		if (!is_numeric($arg1) || !is_numeric($arg2)) return 'erreur';
		$res = $arg1 - $arg2;
		return $res;
	}
 
	function multi($arg1, $arg2) {
		if (!is_numeric($arg1) || !is_numeric($arg2)) return 'erreur';
		$res = $arg1 * $arg2;
		return $res;
	}
 
	function divid($arg1, $arg2) {
		if (!is_numeric($arg1) || !is_numeric($arg2)) return 'erreur';
		if ($arg2 == 0) return 'erreur';
		$res = $arg1 / $arg2;
		return $res;
	}
 
}
?>

Les tests sont cette fois corrects:

PHPUnit 3.4.9 by Sebastian Bergmann.
 
....
 
Time: 0 seconds, Memory: 5.50Mb
 
OK (4 tests, 24 assertions)

Couverture de code

La couverture de code permet de vérifier que l'on passe bien dans toutes les parties du code, ce qui prouve que les tests sont complets et qu'il n'y a pas de code inutile. Dans un prochain billet, j'expliquerais comment installer PHPUnit afin qu'il puisse générer les rapports de couverture de code, mais voici le rapport correspondant aux tests que nous venons d'effectuer:

Couverture de code

Nous pouvons voir dessus que toutes les fonctions ont été testées, nos tests sont complets. Si nous ajoutions une fonction power (pour élever à une puissance) sans ajouter de tests, voici ce que nous obtiendrions:

Couverture de code incomplète

Cet exemple est simpliste, mais il est très parlant. La couverture de code permet de vérifier la cohérence des tests, mais aussi de votre code. Si malgré tous vos tests vous ne passez pas dans une partie de code, il y a de fortes chance qu'il soit inutile ou que vous ayez une erreur de conception.