Nous allons prendre un premier exemple très simple, qui servira de base pour expliciter concrètement la notion de contrat de service d'un composant, mais également pour écrire quelques premiers tests unitaires.
Si vous n'êtes pas à l'aise en Javascript, vous pouvez consulter les excellentes ressources d'un gourou de ce langage : Douglas Crockford on Javascript.
Voici donc le contenu de notre super fonction (!) :
module.js
function calculate_age(p) {
var date_diff = new Date(Date.now() - p.birth.getTime()),
age = Math.abs(date_diff.getUTCFullYear() - 1970);
return age;
}
Cette fonction calculate_age()
, mise à disposition par un développeur de l'équipe, est censée, comme son nom l'indique, calculer… l'âge d'une personne à partir de sa date de naissance. Elle est très simple et nous allons tenter de l'utiliser en lui fournissant une date en paramètre :
Console Javascript
var date = new Date(1954, 01, 23);
calculate_age(date);
Résultat
TypeError: Cannot call method 'getTime' of undefined
Notre tentative d'utilisation de la fonction qui nous a été fournie est un échec… Ça commence mal.
En fait, l'auteur a oublié de nous dire que le paramètre de cette fonction n'était pas une date mais un objet Javascript modélisant une personne, comportant un attribut birth
représentant la date de naissance de la personne.
La meilleure manière d'éviter ce genre de confusion est de fournir un contrat de service avec nos composants : faisons le sous la forme d'une documentation au format JSDoc. La fonction calculate_age()
aurait alors cette allure :
module.js
/**
* Calculate a person's age in years.
*
* @param {object} p An object representing a person, implementing a birth Date parameter.
* @return {number} The age in years of p.
*/
function calculate_age(p) {
var date_diff = new Date(Date.now() - p.birth.getTime()),
age = Math.abs(date_diff.getUTCFullYear() - 1970);
return age;
}
OK, à présent les utilisateurs de la fonction savent précisément à quoi s'attendre en l'utilisant.
Nous allons à présent commencer à implémenter quelques tests unitaires pour notre fonction calculate_age()
. Comme Javascript n'intègre pas de mécanismes dédiés au testing unitaire, nous allons simplement utiliser un framework de test : QUnit.
Un cas de test nominal est le test d'un cas d'utilisation normal du système, autrement dit ce que l'on s'attend à réaliser en condition normal.
Pour notre fonction, un cas de test nominal est l'appel de calculate_age()
avec un unique paramètre représentant un objet Javascript dont l'un des attributs est de type Date
et se nomme birth
.
Implémentons donc simplement ce test (cliquez sur « Result » pour lancer le test et voir les résultats dans votre navigateur) :
Dans ce cas de test nominal très très simple, nous avons réalisé deux assertions (en d'autres termes, deux « vérifications ») :
bertignac
créé pour l'occasion, qui a un attribut birth
de type Date
et un autre attribut name
, et vérifie que l'âge calculé est le bon.calculate_age()
, et ne comportant qu'un seul attribut birth de type Date
.En lançant les tests, vous remarquez que tout se déroule comme prévu : sur des cas nominaux, les tests passent !
Nous l'avons vu plus tôt, il se peut que la fonction soit appelée de manière erronée. Nous allons essayer de tester ces cas non-nominaux.
Un cas simple est le suivant : l'utilisateur de la fonction calculate_age()
l'appelle en fournissant un paramètre qui n'est pas un objet mais une chaîne de caractère, un nombre… Que se passe-t-il dans ce cas ? En bon programmeurs que nous sommes, nous nous attendons à ce que la fonction lève une exception du genre « Parameter should be an object ». Vérifions si c'est le cas :
Remarquez que notre nouveau test utilise un autre type d'assertion : nous vérifions via l'assertion Throws
qu'une exception est bien lancée, et nous vérifions si elle correspond à l'exception attendue.
En cliquant sur « Result », vous vous rendez-compte que notre nouveau test échoue… C'est logique : aucun mécanisme de contrôle du type du paramètre de la fonction calculate_age()
n'est implémenté, celle-ci ne peut donc pas lever une exception « Parameter should be an object » si on lui passe un entier ou une chaîne de caractère.
Qu'à cela ne tienne, modifions le code de la fonction calculate_age()
pour qu'elle vérifie le type de paramètre passé en entrée et lève une exception si ce n'est pas un objet, en ajoutant les lignes suivantes au début de la fonction :
if (typeof p != "object") {
throw "Parameter should be an object"
}
Après cette correction, relançons nos tests pour voir comment ils se comportent :
Très bien, tous les tests sont au vert à présent !
OK, notre code est plus robuste à présent. Mais il y a encore un petit soucis : imaginons que l'utilisateur de la fonction calculate_age()
l'appelle en lui passant en paramètre un objet Javascript (jusqu'ici tout va bien), ne présentant pas d'attribut birth
. Souvenez-vous, notre fonction attend en entrée un objet qui contient un attribut birth
de type Date
.
Pour vérifier ceci, nous allons implémenter un nouveau test. Une nouvelle fois, nous attendons à ce qu'une exception soit levée si le paramètre ne comporte pas le bon attribut birth
:
Pas de surprise : le test échoue. Affinons de nouveau le code de la fonction calculate_age()
pour que ce nouveau test passe, tout en n'affectant pas les autres tests. Pour cela, ajoutons simplement un nouveau test à notre fonction, comme suit :
if (typeof p.birth == "undefined") {
throw "Parameter should have a birth Bate attribute"
}
Vérifions à présent que notre nouveau test passe :
Parfait !
Bien, vous avez maintenant compris le principe… Il reste somme toute à gérer un dernier cas : que se passe-t-il si le programmeur appelle la fonction calculate_age()
en lui fournissant un objet contenant bien un attribut birth
, mais ne contenant pas une Date
?
C'est à vous de jouer ! Modifiez le code que nous avons créé jusqu'à présent pour intégrer un nouveau test qui va probablement échouer, puis modifiez le code de la fonction pour faire en sorte que ce teste passe. Utilisez l'éditeur ci-dessous pour modifier le code :
Cette première plongée dans l'écriture de tests unitaires, que nous avons réalisée en Javascript (peu importe, le principe est le même dans tous les langages, nous le verrons par la suite), a été assez dense : prenez le temps de relire et ré-exécuter tous les exemples.
Vous devez à présent comprendre le principe d'écriture de tests unitaires, bien avoir compris leur intérêt concret en situation, et être capable d'écrire vos premiers tests unitaires !