Un premier exemple de tests unitaires en Javascript

Nous allons enfin écrire des tests unitaires. Dans ce premier exemple, nous illustrerons notre raisonnement sur un exemple en Javascript très simple, que vous pourrez expérimenter vous-même dans votre navigateur, sur cette page !

Une fonction Javascript (très) simple…

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

Les choses ne commencent pas fort bien…

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.

Premiers tests unitaires

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.

Tester les cas nominaux

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 ») :

  • Le premier test utilise un objet 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.
  • Le second test fait le même genre de vérification mais avec un objet créé lors de l'appel de la fonction 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 !

Tester les autres cas…

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.

Important – Gardez bien à l'esprit que vous ne pourrez jamais prévoir tous les cas imaginables ! Ce n'est pas grave… Vous complèterez vos tests tout au long de la vie de votre système, quand vous penserez à un nouveau cas, quand vous rencontrerez une erreur que vous devrez corriger, etc.

Le cas du paramètre de mauvais type

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 !

Très important – Remarquez que nous avons lancé l'ensemble des tests créés depuis le début (« Standard usage » et « Wrong type parameter »). Ceci permet de vérifier que nos nouveaux développements n'ont rien cassé dans le code existants, en faisant échouer des tests qui passaient précédemment. On appelle ceci le test de non-régression.

Le cas du paramètre de bon type mais ne présentant pas d'attribut birth

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 !

Le cas du paramètre de bon type mais présentant un attribut birth du mauvais type

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 :

Conclusion

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.

À retenir – Les tests unitaires permettent de s'assurer qu'un composant fonctionne exactement comme son contrat de service le stipule (ce qui signifie qu'avant d'écrire les tests, il faut avoir établi clairement ce contrat de service), et de s'assurer qu'il continue de fonctionner de la sorte lorsqu'on le fait évoluer. On évite ainsi les régressions.

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 !