Et si les tests étaient la meilleure manière de documenter votre code ?
Une application est un programme complexe, qui résulte de l’assemblage de briques simples.
Pour prendre l’analogie d’un bâtiment, il faut d’une part s’assurer que chaque brique est bien robuste (tests unitaires), puis que les différentes parties s’emboitent bien (tests d’intégration) et enfin que la construction est conforme (tests fonctionnels)
On parle aussi de white-box et de black-box testing suivant que le code source de l’application peut être testé (white-box) ou uniquement les entrées sorties et le fonctionnement global de l’application (black-box).
Nous voulons créer une fonction qui affiche à l’écran une somme d’argent restante en tenant compte du pluriel par exemple :
On veut vérifier que notre fonction marche, et écrire une fonction pour la tester.
On écrit tout d’abord notre fonction dans un fichier pluriel.js
function afficherArgentRestant(somme) {
return `J'ai ${somme} euro`+(somme>=1?'s':'');
}
On crée ensuite un test unitaire dans un fichier pluriel-test.js
console.log(afficherArgentRestant(1));
console.log(afficherArgentRestant(10));
console.log(afficherArgentRestant(0));
Enfin on inclut les scripts dans une page testPluriel-1.html
qui appellera les tests.
L’assertion ou vérification est le coeur du test unitaire. Le terme peut être trompeur en français mais c’est une fonction qui a pour unique but de renvoyer l’info de réussite ou d’échec au test.
Cette appellation est standard dans le domaine du test. On peut définir une fonction dit d’assertion, appelée assert
est définie dans sa forme la plus simple comme suit :
function assert(message, expr){
if(!expr){
throw new Error(message);
}
}
Le message
est la définition du texte destinée à être lue par un humain, tandis que expr
est le résultat booléen du test.
Il est toujours intéressant d’afficher sous forme de message l’état du test :
function assert(message, expr){
if(!expr){
output(false, message);
throw new Error(message);
}
output(true, message);
}
function output(result, message){
var p = document.createElement('p');
message += result ? ' : SUCCESS' : ' : FAILURE';
p.style.color = result ? '#0c0' : '#c00';
p.innerHTML = message;
document.body.appendChild(p);
}
Nous pouvons maintenant relancer nos tests avec ces fonctions
Nous allons faire une fonction nécessitant un peu plus de test. Créez une fonction de conversion de l’euro vers 3 devises étrangères : les dollars US, la livre anglaise et la Yen Japonaise. La fonction renvoie une erreur en cas de devise inconnue, message Monnaie non gérée
.
function convertirEuro(euro, devise){
switch(devise){
case 'USD' :
return euro * 1.3;
case 'GBP' :
return euro * 0.87;
case 'JPY' :
return euro * 124.77;
default : {
throw new Error('Monnaie non gérée');
}
}
}
Afin de rendre les résultats plus digestes, on souhaite regrouper les tests pour chaque devise. C’est la notion de suite de test. Pour mettre cela en place, il faut créer une fonction déroulant une série de tests unitaires, ici la fonction testcase
:
function testCase(message, tests) {
// Initialisations
var total = 0; // compteur du nombre de test de la suite
var succeed = 0; // nombre de test réussis
var p = document.createElement('p');
p.innerHTML = message;
document.body.appendChild(p);
// Parcours des tests
for(test in tests){
total++;
try {
// Passage du test
tests[test]();
succeed++;
} catch(err) {
// Erreurs non renvoyées pour passer les autres tests
}
}
var p = document.createElement('p');
p.innerHTML = 'succeeded tests ' + succeed + '/' + total ;
document.body.appendChild(p);
}
Petit rappel pour bien comprendre la syntaxe utilisée :
// Crée objet JS où chaque propriété est une fonction
const objet = { 'Diego':function() { console.log('Maradona')},
'Jesus':function() { console.log('Christ') } }
// Parcours des propriétés de l'objet
for (key in objet) { console.log(key) }
// Parcours des propriétés de l'objet et affichage des valeurs qu'elles contiennent
for (key in objet) { console.log( objet[key] ) }
// Parcours des propriétés de l'objet et exécution des fonctions qu'elles contiennent
for (key in objet) { console.log( objet[key]() ) }
Nous pouvons ensuite créer les suites de tests pour chaque devise sur le modèle suivant :
testcase('Conversion euro → dollars us', {
'Test avec 1 euro' : function(){
assert('1€ doit renvoyer 1,3$', convertEuro(1, 'USD') === 1.3);
},
'Test avec 2 euros' : function(){
assert('2€ doit renvoyer 2,6$', convertEuro(2, 'USD') === 2.6);
}
})
Écrire l’ensemble des tests pour les autres devises. Créer aussi un test pour une devise non gérée. Remarque : utiliser ALT+0165 pour le caractère ¥
Enfin, il peut nécessaire d’initialiser les objets avant de démarrer un test et de les libérer à la fin d’une suite de test. Ce sont les concepts de setUp et tearDown. Par rapport à l’exemple ci-dessous, on passe en paramètre aux tests la devise pour éviter d’éventuelles erreurs de copier-coller dans la rédaction des tests.
function testcase(message, tests){
var total = 0;
var succeed = 0;
var hasSetup = typeof tests.setUp === 'function';
var hasTeardown = typeof tests.tearDown === 'function';
var p = document.createElement('p');
p.innerHTML = message;
document.body.appendChild(p);
for(test in tests){
if(test !== 'setUp' && test !== 'tearDown'){
total++;
}
try{
if(hasSetup){
tests.setUp();
}
tests[test]();
if(test !== 'setUp' && test !== 'tearDown'){
succeed++;
}
if(hasTeardown){
tests.tearDown();
}
}catch(err){
}
}
var p = document.createElement('p');
p.innerHTML = 'succeed tests ' + succeed + '/' + total ;
document.body.appendChild(p);
}
testcase('I convert euro to usd', {
'setUp' : function(){
this.currency = 'USD';
},
'I test with one euro' : function(){
assert('1€ should return 1,3$', convertEuro(1, this.currency) == 1.3);
},
'I test with two euros' : function(){
assert('2€ should return 2,6$', convertEuro(2, this.currency) == 2.6);
}
})
Il y a 4 phases dans l’exécution d’un test unitaire :
Pour les valeurs numériques, il est souvent préfèrable de définir une tolérance de valeurs plutôt qu’une valeur fixe.
Voir plus d’infos sur la page Wikipédia du Test Driven Development.
L’idée de base est de définir les tests avant de coder. Cela est toujours possible car dès qu’une fonction est définie, on en connait les résultats attendus et on peut donc écrire des tests.
Les 3 lois du TDD sont formulables ainsi :
Le développement orienté test TDD évolue pour devenir le Behavior Driven Development (BDD). L’esprit pour les codeurs est sensiblement le même, on parle comportement plutôt que de test pour une approche plus transversale côté métier.
Après le “Mobile First”, voici le “Write Test First”
Nous allons utiliser le framework de test Jasmine qui est à la fois populaire et assez facile à prendre en main. Il peut notamment s’utiliser directement depuis le navigateur, sans utiliser NodeJS ou un framework particulier.
Pour installer et utiliser Jasmine :
SpecRunner.html
avec le Live Serverspec
Pour intégrer Jasmine à votre projet :
jasmine
à la racine de votre projetjasmine/lib/jasmine-standalone-3.6.0
Pour faire fonctionner le test suivant avec Jasmine :
testAddition.html
et ajouter les lignes suivantes dans l’entête :<link rel="shortcut icon" type="image/png" href="lib/jasmine-3.6.0/jasmine_favicon.png">
<link rel="stylesheet" type="text/css" href="lib/jasmine-3.6.0/jasmine.css">
<script type="text/javascript" src="lib/jasmine-3.6.0/jasmine.js"></script>
<script type="text/javascript" src="lib/jasmine-3.6.0/jasmine-html.js"></script>
<script type="text/javascript" src="lib/jasmine-3.6.0/boot.js"></script>
<!-- Inclusion des fonctions js à tester -->
<script src="addition.js"></script>
<!-- Inclusion des test js -->
<script src="test-addition.js"></script>
describe("Test de l'avaScript addition operator", function () {
it('adds two numbers together', function () {
expect(1 + 2).toEqual(3);
});
});
La syntaxe est la suivante :
Reprendre les tests de la fonction de conversion euros pour qu’ils utilisent le framework Jasmine
Lisez la documentation à l’adresse suivante :
Lire la doc de QUnit et les tests de la fonction de conversion euros pour qu’ils utilisent le framework QUnit.
Utiliser la version CDN de QUnit à appeler dans le navigateur
Afin d’éviter les copier-coller de tests, nous allons plutôt définir un tableau JS des valeurs à tester et créer des tests automatiquement à partir des valeurs à tester.
// Création d'un tableau de définition des tests
// [0]:devise, [1]:montant €, [2]:résultat attendu, [3]:caractère monnaie
var tabAttendusTests = [
['USD', 1, 1.3, '$'],
['USD', 2, 2.6, '$'],
['GBP', 1, 0.87, '£'],
['GBP', 2, 1.74, '£'],
['JPY', 1, 124.77, '¥'],
['JPY', 2, 249.54, '¥']
];
Utiliser une boucle sur le tableau pour créer les tests automatiquement.
Le cahier des charges a été exprimé par le client comme suit :
Je souhaite avoir une application dans laquelle je puisse rentrer les dates anniversaires de mes proches et qui me donne leur âge en les triant par date d’anniversaire la plus proche. Les anniversaires très proches ( la veille ou le jour même) doivent être mis en avant. Un plus serait d’avoir des notifications.
Les architectes agiles ont défini un MVP (minimum viable product) et les sprints suivants :
localstorage
du navigateur et rechargées avec la page{id:"xxx", nom:"Name", dateAnniv:objet Date, comment:"commentaire"}
index.html
est le point d’entrée de l’applicationtemplate.html
rassemble uniquement des bribes de codes Html Bootstrap pour tester des mises en page. Utilisez là pour tester votre mise en page personnelle. Ces bouts se retrouvent plus ou moins fidèlement dans l’application.Personne
: getAge()
et getJoursAvantAnniv()
pour qu’elles valident les tests unitaires (dossier test
).index.html
pour que la date soit affichée dans le sous-titre de l’applicationFaites une recherche sur le mot clef ‘TODO’ dans le code permet aussi de voir les choses identifiées comme à faire.
Vous pouvez ensuite suivre les étapes proposées du développement ou choisir celles qui vous intéressent pour faire vos Sprints.
N’oubliez pas de mettre en place les tests AVANT de coder les nouvelles fonctions.
Au delà des tests, il existe une jungle d’outils pour valider tous les aspects de vos codes et automatiser les étapes de tests et déploiement (CI/CD) :
Pour accéder à ces outils et les installer, il faut souvent commencer par utiliser des gestionnaires de packets logiciels comme yarn ou npm.
La norme ISO/CEI 9126, puis ISO 25010 définissent un langage commun pour modéliser les qualités d’un logiciel :
Fonctionnalité | Fiabilité | Utilisabilité | Rendement | Maintenabilité | Portabilité |
---|---|---|---|---|---|
Pertinence | Maturité | Compréhension | Comportement temporel | Analyse | Adaptation |
Exactitude | Tolérance aux pannes | Apprentissage | Utilisation des ressources | modification | Installation |
Interopérabilité | Facilité de récupération | Exploitation | Stabilité | Coexistence | |
Sécurité | Testabilité | Interchangeabilité | |||
Conformité |
Les frameworks de test unitaire JS les plus utilisés sont :
Les solutions des exercices sont disponibles dans le dépot GIT https://gitlab.com/exercices-et-tp/tests-unitaires-js-jasmine-qunit