Les tests unitaires

Pour un code plus solide vis à vis des évolutions

Ecrire des « tests unitaires » est un moyen de s’assurer à chaque évolution d’un logiciel que la série de tests fonctionne toujours. On peut ainsi être sûr que des bugs ne sont pas revenus, et que le programme se comporte toujours comme attendu.

Test unitaire et programmation extrème dans SPIP

SPIP, assez récemment, poussé par Fil, s’était mis à écrire des tests unitaires. Un script passe en revue et lance tous les tests, et le nombre d’erreur, de réussite s’affiche alors.

Un article paru sur SPIP Blog parlait même à l’époque de « programmation extrème » [1] bien que ce concept ne s’intéresse pas qu’aux simples tests unitaires [2].

D’autres méthodes de programmation

Moins connu que l’extrème programming, la méthode La Rache, conçue par Iilar (International Institute of La Rache) est souvent employée, bien que peu citée. C’est une méthode fonctionnelle et empirique qui fait et a fait ses preuves. Elle a de nombreux adeptes et est très bien décrite sur le site de référence [3].

Revenons donc aux tests unitaires.

Le test avant le code

En XP (Extreme Progamming), on écrit souvent les tests avant de coder les méthodes. On commence donc par dire « voilà ce que je souhaite faire » (le test pour l’instant echoue), puis on s’arrange à coder la méthode pour que le test se mette à fonctionner.

Au fur et a mesure de l’avancement, les tests deviennent de plus en plus nombreux, et l’introduction d’un bug est aussitôt repéré : un test échoue alors. Par conséquent, reprendre du code, le remodeler, devient plus facile, puisqu’on sait par avance que les tests nous diront si l’on obtient le résultat escompté.

La librairie « simpleTest »

Simple Test est une librairie PHP (et même un ensemble de librairies) qui permet de produire facilement des tests unitaires. Elle est facilement extensible pour d’autres projets.

Simple Test dispose d’un certain nombre de fonctions de tests de base, appelés « assertions » pour valider les codes. Dans Simple Test, chaque fonction commençant par ’test’ (on parle souvent de ’méthode’ dans la cas d’une classe php) d’une classe héritée de l’objet ’UnitTestCase’ devient un jeu de test potentiel.

Voici un exemple pour mieux comprendre l’écriture des tests :

    function testChaineHelloWorld() {
      $hw = 'Hello World';
      $this->assertTrue($hw == 'Hello World');

      $this->assertEqual($hw,'Hello World');
      $this->assertNotEqual($hw,'Hello Kitty');

      $this->assertPattern('/hello world/i', $hw);
      $this->assertNoPattern('/hello kitty/i', $hw);

      // ...
    }

On comprendra que toutes les méthodes commençant par « assert » sont des méthodes de test. Le reste est de l’anglais compréhensible pour un programmeur php.

Quelques méthodes fournies par Simple Test

Le plus simple est d’aller étudier la documentation anglaise de SimpleTest (la française n’est pas tout à fait actualisée).

On découvrira les assertions suivantes :

  • assertTrue($x) vérifie $x vrai
  • assertFalse($x) vérifie $x faux
  • assertNull($x) vérifie $x inexistant
  • assertNotNull($x) vérifie que $x existe
  • assertIsA($x, $t) vérifie que l’objet $x est de type $t
  • assertNotA($x, $t) vérifie que l’objet $x n’est pas de type $t
  • assertEqual($x, $y) vérifie $x == $y
  • assertNotEqual($x, $y) vérifie $x != $y
  • assertWithinMargin($x, $y, $m) vérifie (abs($x - $y) < $m) est faux
  • assertOutsideMargin($x, $y, $m) vérifie (abs($x - $y) < $m) est vrai
  • assertIdentical($x, $y) vérifie $x === $y
  • assertNotIdentical($x, $y) vérifie $x !== $y
  • assertReference($x, $y) vérifie $x référence de $y
  • assertClone($x, $y) vérifie $x copie de $y
  • assertPattern($p, $x) vérifie que $pattern (expression régulière) capture $x
  • assertNoPattern($p, $x) vérifie que $pattern ne capture pas $x
  • expectError($x) vérifie qu’il y a une erreur php générée

Plugin Simple Test pour SPIP

Un plugin pour SPIP a été développé depuis pour utiliser les avantages de la librairie Simple Test.

Pour créer des tests unitaires pour votre projet SPIP, il suffit d’installer le plugin, de créer un dossier « tests » dans votre plugin, et d’y copier a minima le fichier « lanceur_spip.php » contenu dans le répertoire « tests/ » du plugin simpleTests.

Ce fichier permet de démarrer SPIP (et simpleTest) depuis le répertoire test de votre plugin (exemple http://localhost/mon_spip/plugins/mon_plugin/tests/)

Il vous faudra aussi inclure ce fichier dans tous les sous dossiers de votre répertoire « tests » où vous placez des tests unitaires.

Vous pourrez aussi éventuellement copier le fichier « all_tests.php » qui permet d’executer tous les fichiers de tests d’un répertoire et de ses sous répertoires (il faut par contre l’éditer pour créer un nom de classe unique).

Créons un premier fichier de test

On supposera qu’on a bien copié « lanceur_spip.php » dans son répertoire tests. Créons un fichier de test « testMaBaliseAutoriser.php » comme ceci :

<?php
require_once('lanceur_spip.php');
include_spip('inc/autoriser');

class Test_ma_balise_autoriser extends SpipTest{
		
	function testAutoriserOkNiet(){
		$this->assertFalse(autoriser('niet'));
		$this->assertTrue(autoriser('ok'));
	}
}

On remarquera qu’on étend une classe appelée « SpipTest » (elle-même étend des tests de Simple Test), qui permet d’ajouter un certain nombre de fonctions et d’assertions à celles prévues par défaut. On y reviendra plus tard.

Le nom de la classe doit être unique et explicite sur ce qu’il va faire, comme celui du fichier d’ailleurs. Il est possible, et souvent utile, de mettre plusieurs classes dans un fichier, et plusieurs méthodes de tests dans une classe.

Les noms des méthodes doivent aussi être explicite sur ce qu’elles font, particulièrement dans les tests, car en cas d’erreur, ce sont ces noms qui vont apparaître (nom du fichier, de la classe, de la méthode, puis erreur particulière à l’assertion avec le numéro de ligne de l’erreur).

Il est donc conseillé d’écrire plein de petites méthodes pour les tests, plutôt d’une grosse regroupant tous les tests à faire.

	function testAutoriserOkNiet(){
		$this->assertFalse(autoriser('niet'));
		$this->assertTrue(autoriser('ok'));
	}

Ici, la fonction teste que autoriser('niet') retourne bien la valeur qui est attendu.

Exécuter les tests

En se rendant sur http://localhost/mon_spip/plugins/mon_plugin/tests/testMaBaliseAutoriser.php , nous devons voir le nom de notre fichier, et un message de réussite (ou d’echec des opérations pourquoi pas).

Si vous avez copié « all_tests.php », http://localhost/mon_spip/plugins/mon_plugin/tests/all_tests.php doit à peu près indiquer les mêmes choses.

De la même manière, si vous avez copié les anciens tests unitaires de SPIP [4] dans un répertoire tests/ à la racine de votre SPIP, aller sur http://localhost/mon_spip/tests/ doit lancer les tests de SPIP et ceux des plugins ensuite (vers la fin).

Méthodes fournies par SpipTest

  • assertOk($x) vérifie $x == ’ok’ (casse indiférente)
  • assertNotOk($x) vérifie $x != ’ok’ (casse indiférente)
  • recuperer_code($code, $contexte, ...) crée un squelette pour le code donné, et le calcule avec le contexte passé
$x = $this->recuperer_code("#ENV{troll}",array('troll'=>'interface'));
$this->assertEqual('interface',$x);
  • assertOkCode($code, $contexte,...) que la compilation du code $code et son calcul avec le contexte $contexte retourne ’ok’ (casse indiférente)
  • assertNotOkCode($code, $contexte,...)
function testAutoriserSqueletteOkNiet(){
	$this->assertOkCode('[(#AUTORISER{ok})ok]');
	$this->assertOkCode('[(#AUTORISER{niet}|sinon{ok})]');
}
  • asserEqualCode($value, $code, $contexte,...)
  • asserNotEqualCode($value, $code, $contexte,...)
  • asserPatternCode($pattern, $code, $contexte,...)
  • recuperer_infos_code($code, $contexte, ...) retourne un tableau d’infos sur la compilation, ainsi que les erreurs de compilation générée

Subtilités Non Applicables

Parfois, pour une raison particulière, certains tests ne peuvent pas être appliqués. Par exemple parce qu’un contenu adéquat manque dans la base de donnée de test.

Pour le signaler, il est possible dans les tests de faire envoyer une chaine commençant par NA. Celle ci sera vu par assertOk() et assertOkCode() comme une exception de type ’Non applicable’, et le texte suivant NA est alors affiché.

Exemple :

function testLesAuteursRenvoieQqc(){
	$code = "
		<BOUCLE_a(ARTICLES){id_auteur>0}{0,1}>
			[(#LESAUTEURS|?{OK,'#LESAUTEURS a echoue'})]
		</BOUCLE_a>
		NA Ce test ne fonctionne que s'il existe un article ayant un auteur !
		<//B_a>
	";
	$this->assertOkCode($code);
}

De la même manière, il faut parfois forcer un affichage de type non applicable plutôt que d’executer un test que l’on va savoir faux. Deux fonctions peuvent nous aider : isNa() (renvoie vrai si la chaine passée commence par NA) et exceptionSiNa() (ajoute une exception Non Applicable, et retourne false). Exemple :

function testCoupeIntroduction(){
	@define('_INTRODUCTION_SUITE', '&nbsp;(...)');
	$suite = _INTRODUCTION_SUITE;
	$code = "
		[(#REM) une introduction normale doit finir par _INTRODUCTION_SUITE]
		<BOUCLE_a(ARTICLES){chapo=='.{100}'}{texte>''}{descriptif=''}{chapo!=='^='}{0,1}>
		[(#INTRODUCTION)]
		</BOUCLE_a>
		NA necessite un article avec un texte long et pas de descriptif
		<//B_a>
	";
	if (!$this->exceptionSiNa($res = $this->recuperer_code($code))) {
		$this->assertPattern("/".preg_quote($suite)."$/", $res);
	}
}

Enfin, on peut forcer l’envoi d’une exception de la sorte :

$val = $this->recuperer_code('<BOUCLE_meta(META){nom=nom_site}>#VALEUR</BOUCLE_meta>');
if (!$val) {
	throw new SpipTestException('Il faut donner un nom de site non vide !');
}

Transmettre des fonctions aux squelettes

Avec la méthode options_recuperer_code(array()) il est possible de transmettre certaines options à la fonction recuperer_code(). L’argument est un tableau clé=>valeur

  • ’avant_code’ : pour ajouter systematiquement un code avant
  • ’apres_code’ : pour ajouter systematiquement un code apres
  • ’fonctions’ : pour ajouter un fichier de fonctions au squelette.

Exemples :

function testRecupererCodeAvantApres(){
	$this->options_recuperer_code(array(
		'avant_code'=>'Nice ',
		'apres_code'=>' So Beautiful',
	));
	$this->assertEqualCode('Nice Hello World So Beautiful', 'Hello World');		
}
function testBaliseDeclareeEtParamsUtiles(){
	$this->options_recuperer_code(array(
		'fonctions' => '
				function balise_ZEXISTE_dist($p){
					if (!$p1 = interprete_argument_balise(1,$p))
						$p1 = "\'\'";
					$p->code = "affiche_jexiste($p1)";
					return $p;
				}
				function affiche_jexiste($p1){
					return $p1;
				}
				',
	));
	$this->assertEqualCode('', '#ZEXISTE');
	$this->assertOkCode('#ZEXISTE{ok}');
	$this->assertEqualCode('avantokapres', '[avant(#ZEXISTE{ok})apres]');
	$this->assertEqualCode('avant apres', '[avant(#ZEXISTE{ok}|oui)apres]');
	$this->assertEqualCode('', '[avant(#ZEXISTE{ok}|non)apres]');
}

Attention : Ces options sont persistantes entre les méthodes de tests. Pour les annuler/effacer, il faut executer la fonction sans argument.

$this->options_recuperer_code();

Tester des erreurs de compilation

Parfois, on aimerait vérifier que la compilation génère bien des erreurs attendues.

Pour cela, on peut attraper les erreurs générées avec la fonction
infos_recuperer_code() qui renvoie le tableau des erreurs generées (et bien d’autres informations)

/**
 * Un inclure manquant doit créer une erreur de compilation pour SPIP
 * qui ne doivent pas s'afficher dans le public si visiteur
 */ 
function testInclureManquantGenereErreurCompilation(){
	foreach(array(
		'<INCLURE{fond=carabistouille/de/tripoli/absente}/>ok',
		'#CACHE{0}[(#INCLURE{fond=carabistouille/de/montignac/absente}|non)ok]',
	) as $code) {
		$infos = $this->recuperer_infos_code($code);
		$this->assertTrue($infos['erreurs']);
	}
}

Tester le site public

Il est possible de faire appeler les codes générées par un navigateur interne à Simple Test (qui n’est donc pas logé par défaut)

Pour cela, il faut inclure les librairies qui vont bien, et faire par exemple :

// apres le require_once
include_spip('simpletest/browser');
include_spip('simpletest/web_tester');

// dans la classe
function testInclureManquantNAffichePasErreursDansPublic(){
	foreach(array(
		'<INCLURE{fond=carabistouille/de/tripoli/absente}/>ok',
		'#CACHE{0}[(#INCLURE{fond=carabistouille/de/montignac/absente}|non)ok]',
	) as $code) {

		// non loggue, on ne doit pas voir d'erreur...
		$browser = &new SimpleBrowser();
		$browser->get($this->urlTestCode($code));

		// header 200 : ok
		$this->assertEqual($browser->getResponseCode(), 200);

		// simplement ok : pas d'erreur affichee
		$this->assertOk($browser->getContent());
	}
}

Notes

[2Lire par exemple : Extreme Programming (Wikipedia)

[4

cd mon_spip/
svn co svn://zone.spip.org/spip-zone/_dev_/tests

Voir aussi : Tests unitaires de SPIP

Discussion

2 discussions

Ajouter un commentaire

Avant de faire part d’un problème sur un plugin X, merci de lire ce qui suit :

  • Désactiver tous les plugins que vous ne voulez pas tester afin de vous assurer que le bug vient bien du plugin X. Cela vous évitera d’écrire sur le forum d’une contribution qui n’est finalement pas en cause.
  • Cherchez et notez les numéros de version de tout ce qui est en place au moment du test :
    • version de SPIP, en bas de la partie privée
    • version du plugin testé et des éventuels plugins nécessités
    • version de PHP (exec=info en partie privée)
    • version de MySQL / SQLite
  • Si votre problème concerne la partie publique de votre site, donnez une URL où le bug est visible, pour que les gens puissent voir par eux-mêmes.
  • En cas de page blanche, merci d’activer l’affichage des erreurs, et d’indiquer ensuite l’erreur qui apparaît.

Merci d’avance pour les personnes qui vous aideront !

Par ailleurs, n’oubliez pas que les contributeurs et contributrices ont une vie en dehors de SPIP.

Qui êtes-vous ?
[Se connecter]

Pour afficher votre trombine avec votre message, enregistrez-la d’abord sur gravatar.com (gratuit et indolore) et n’oubliez pas d’indiquer votre adresse e-mail ici.

Ajoutez votre commentaire ici

Ce champ accepte les raccourcis SPIP {{gras}} {italique} -*liste [texte->url] <quote> <code> et le code HTML <q> <del> <ins>. Pour créer des paragraphes, laissez simplement des lignes vides.

Ajouter un document

Suivre les commentaires : RSS 2.0 | Atom