Introduction
Dans le cadre de projets professionnels, un développeur est amené à contribuer à des programmes très volumineux dont peu des membres du projet, voire aucun, connaissent l'ensemble du code. Lors d'une modification, il peut arriver que des fonctions déjà existantes soient affectées sans que cela soit voulu. Un tel effet de bord pourrait mettre à mal certaines fonctionnalités, voire le programme complet. Pour éviter ce cas de figure, les développeurs créent des tests automatisés qui vérifieront l'intégrité du code à chaque modification ou ajout.
Définitions
Impossible d'accéder à la ressource audio ou vidéo à l'adresse :
La ressource n'est plus disponible ou vous n'êtes pas autorisé à y accéder. Veuillez vérifier votre accès puis recharger la vidéo.
Objectifs
Comprendre ce qu'est un test automatisé ;
Connaître les deux principaux types de tests.
Mise en situation
La notion de test est souvent sous-estimée par les développeurs. Ils se lancent dans des projets qui ont pour vocation à grandir et négligent l'atout que représente les tests pour maintenir leur développement. Pour comprendre leur nécessité, il faut d'abord comprendre ce que sont ces tests.
Définition : Test automatisé
Un test automatisé est un programme chargé d'en tester un autre.
Pour réaliser ces tests, les tests comparent la valeur retournée par le programme testé avec une valeur prévue fournie lors de l'écriture du test. Si les deux valeurs ne sont pas égales, le test échouera.
Remarque :
Il existe de nombreux types de tests : unitaires, fonctionnels, d'intégrité, d'intégration, etc.
Nous nous concentrerons sur les deux types les plus communs : unitaires et fonctionnels.
Fondamental : Maintenir son code
Ces tests sont utilisés afin de maintenir le code.
Lorsqu'un développeur créer ou modifie une fonction, il arrive que des effets de bord apparaissent. Un effet de bord est un impact indirect et imprévu sur le fonctionnement attendu d'un morceau de code. Par effet de cascade, le fonctionnement complet du programme pourrait être compromis.
Lorsqu'un projet grandit, il possède un nombre de fonctionnalités si important qu'il n'est plus possible des les tester manuellement et de s'assurer que les évolutions ne provoquent pas d'effets de bord. Les tests automatisés deviennent stratégiques afin de vérifier l'intégrité du projet à chaque modification majeure.
Définition : Régression
L'introduction d'un bug sur une fonctionnalité existante lors d'une mise à jour s'appelle une régression : les tests servent également à éviter les régressions.
Définition : Test unitaire
Le but d'un test unitaire est de vérifier le bon fonctionnement d'une composante d'un programme, le plus souvent une fonction.
Le test unitaire essaiera d'être exhaustif en testant tous les types de scénarios possibles pour une fonction. Par exemple, si une fonction contient une condition, il faudra au moins deux tests : un dans lequel la condition est vraie et l'autre dans lequel la condition est fausse.
Une telle exhaustivité permet aussi de vérifier que chaque partie du code est utile et atteignable dans des scénarios réalistes.
Définition : Test fonctionnel
Le but d'un test fonctionnel est de vérifier le bon fonctionnement d'une fonctionnalité complète.
Le test sera effectué sur une boîte noire, c'est-à-dire sans accès aux détails du code.
Ainsi, contrairement au test unitaire qui étudie une composante de code isolée, le test fonctionnel étudie le comportement d'un programme complet. Il s'agit donc d'un test plus global qui ne sera pas aussi exhaustif que le test unitaire, mais validera que les composantes du programme sont correctement assemblées.
Exemple : Comprendre la différence entre fonctionnel et unitaire
Pour comprendre la différence entre test unitaire et test fonctionnel, prenons un exemple simple : un bouton d'achat sur un site de e-commerce. Lors d'un clic sur la bouton Acheter, un message est envoyé au serveur pour lui dire d'acheter le produit. Le serveur se charge de l'opération et renvoie un message de réussite ou d'erreur. Voici ce que seraient les différents tests dans ce cas :
Test unitaire : ce test consistera à vérifier qu'un clic sur le bouton résulte bien sur un message envoyé au serveur.
Test fonctionnel : ce test consistera à vérifier l'ensemble de la chaîne en testant la nature de la réponse du serveur suite au clic sur le bouton.
Souvent, les tests unitaires sont développés en premier. La validation des tests unitaires est en général requise avant de dérouler les tests fonctionnels, car les fonctionnalités globales s'appuient sur les composantes du code.
Méthode : Quand développer un test ?
Il est recommandé de développer des tests à chaque fois qu'on livre une nouvelle fonctionnalité. Ces tests permettront d'une part de faire comprendre le fonctionnement de la fonctionnalité à un autre développeur et d'autre part d'assurer que la fonctionnalité ne sera pas compromise par des modifications futures.
Complément : Les frameworks de test
Il existe des frameworks de test qui encadrent et facilitent l'écriture et l'utilisation de tests. Ils complexifient l'écriture des programmes, mais dans le cadre de projets professionnels, il est vivement recommandé d'utiliser ces frameworks.
En Python, les deux frameworks principaux sont
unittest
etpytest
. Le premier est le framework historique inclus par défaut à l'installation de Python. Le second est le plus récent et commence à faire l'unanimité dans la communauté Python.En JavaScript, il existe de nombreux frameworks mais les plus populaires sont :
Mocha
etJasmine
.
À retenir
Les tests automatisés sont des outils très puissants pour maintenir son code et éviter les régressions.
Les tests unitaires se concentrent sur le fonctionnement d'un morceau de code isolé
Les tests fonctionnels se concentrent les fonctionnalités attendues du programme global
Impossible d'accéder à la ressource audio ou vidéo à l'adresse :
La ressource n'est plus disponible ou vous n'êtes pas autorisé à y accéder. Veuillez vérifier votre accès puis recharger la vidéo.
Appliquer la notion
Voici une page Wikipédia à lire afin de pouvoir répondre à ce quiz :
Qu'est-ce que le Test-Driven Development ?
Une méthode de développement
Un langage de programmation
Un framework pour développer des tests automatisés
Un standard de développement obligatoire
Ce cycle de développement possèdent trois lois, quelles sont-elles ?
Vous devez écrire un test qui échoue avant de pouvoir écrire le code de production correspondant.
Vous devez écrire une seule assertion à la fois, qui fait échouer le test ou qui échoue à la compilation.
Vous devez écrire systématiquement un test après avoir écrit une fonction
Vous devez écrire le minimum de code de production pour que l'assertion du test actuellement en échec soit satisfaite.
Vous devez écrire un test qui réussit avant de pouvoir écrire le code de production correspondant.
Vous devez documenter chaque fonction en s'appuyant sur ses tests si cela est pertinent.
Parmi ces propositions, lesquelles expliquent le gain de productivité dû à l'utilisation du TDD ?
TDD permet de livrer une nouvelle version d'un logiciel avec un haut niveau de confiance dans la qualité des livrables, confiance justifiée par la couverture et la pertinence des tests à sa construction.
TDD permet de produire une documentation plus riche grâce à des tests qui remplacent des paragraphes longs et parfois peu compréhensibles. Cela permet également de ne pas avoir à traduire la documentation car le code est identique quel que soit la langue ou la culture du programmeur.
TDD permet de rendre plus efficaces les fonctionnalités grâce une conception plus consciencieuse du code et de ses modifications futures.
TDD permet d'éviter les accidents de parcours, où des tests échouent sans qu'on puisse identifier le changement qui en est à l'origine, ce qui aurait pour effet d'allonger la durée d'un cycle de développement.
Qu'est-ce que la programmation binomiale ?
Une méthode de programmation permettant de trouver des bugs en s'appuyant sur la loi binomiale (en Probabilités).
Une méthode de programmation qui implique de développer chaque fonctionnalité dans un binôme de langage et de toujours retenir l'implémentation la plus claire.
Une méthode de développement dans laquelle deux programmeurs développent ensemble. Le premier écrit un test en échec puis le second développe la fonctionnalité. Pour la fonctionnalité suivante, les rôles sont inversés.
Qu'est-ce que le Test-Driven Development ?
Une méthode de développement
Un langage de programmation
Un framework pour développer des tests automatisés
Un standard de développement obligatoire
Une méthode de développement
Un langage de programmation
Un framework pour développer des tests automatisés
Un standard de développement obligatoire
Il ne s'agit pas d'un standard à proprement parler. Chaque développeur a sa propre méthode de développement. Il existe des méthodologies formalisées avec un but précis, comme le TDD, que les développeurs sont libres d'adopter et d'adapter.
Ce cycle de développement possèdent trois lois, quelles sont-elles ?
Vous devez écrire un test qui échoue avant de pouvoir écrire le code de production correspondant.
Vous devez écrire une seule assertion à la fois, qui fait échouer le test ou qui échoue à la compilation.
Vous devez écrire systématiquement un test après avoir écrit une fonction
Vous devez écrire le minimum de code de production pour que l'assertion du test actuellement en échec soit satisfaite.
Vous devez écrire un test qui réussit avant de pouvoir écrire le code de production correspondant.
Vous devez documenter chaque fonction en s'appuyant sur ses tests si cela est pertinent.
Vous devez écrire un test qui échoue avant de pouvoir écrire le code de production correspondant.
Vous devez écrire une seule assertion à la fois, qui fait échouer le test ou qui échoue à la compilation.
Vous devez écrire systématiquement un test après avoir écrit une fonction
Vous devez écrire le minimum de code de production pour que l'assertion du test actuellement en échec soit satisfaite.
Vous devez écrire un test qui réussit avant de pouvoir écrire le code de production correspondant.
Vous devez documenter chaque fonction en s'appuyant sur ses tests si cela est pertinent.
Ceci est une très bonne pratique mais n'est pas une loi du TDD.
Le TDD se base sur l'idée générale suivante : écrire les tests qui échoue avant d'implémenter le code qui résout le problème. De cette manière, on est certain que le test est correct et que la fonction répond bien aux attentes.
Parmi ces propositions, lesquelles expliquent le gain de productivité dû à l'utilisation du TDD ?
TDD permet de livrer une nouvelle version d'un logiciel avec un haut niveau de confiance dans la qualité des livrables, confiance justifiée par la couverture et la pertinence des tests à sa construction.
TDD permet de produire une documentation plus riche grâce à des tests qui remplacent des paragraphes longs et parfois peu compréhensibles. Cela permet également de ne pas avoir à traduire la documentation car le code est identique quel que soit la langue ou la culture du programmeur.
TDD permet de rendre plus efficaces les fonctionnalités grâce une conception plus consciencieuse du code et de ses modifications futures.
TDD permet d'éviter les accidents de parcours, où des tests échouent sans qu'on puisse identifier le changement qui en est à l'origine, ce qui aurait pour effet d'allonger la durée d'un cycle de développement.
TDD permet de livrer une nouvelle version d'un logiciel avec un haut niveau de confiance dans la qualité des livrables, confiance justifiée par la couverture et la pertinence des tests à sa construction.
TDD permet de produire une documentation plus riche grâce à des tests qui remplacent des paragraphes longs et parfois peu compréhensibles. Cela permet également de ne pas avoir à traduire la documentation car le code est identique quel que soit la langue ou la culture du programmeur.
La documentation reste nécessaire car un code reste peu intelligible sans explication. Cela n'est pas affaire de langue ou de culture mais de manière de penser.
TDD permet de rendre plus efficaces les fonctionnalités grâce une conception plus consciencieuse du code et de ses modifications futures.
TDD n'exige rien sur la maintenabilité du code en lui-même.
TDD permet d'éviter les accidents de parcours, où des tests échouent sans qu'on puisse identifier le changement qui en est à l'origine, ce qui aurait pour effet d'allonger la durée d'un cycle de développement.
Qu'est-ce que la programmation binomiale ?
Une méthode de programmation permettant de trouver des bugs en s'appuyant sur la loi binomiale (en Probabilités).
Une méthode de programmation qui implique de développer chaque fonctionnalité dans un binôme de langage et de toujours retenir l'implémentation la plus claire.
Une méthode de développement dans laquelle deux programmeurs développent ensemble. Le premier écrit un test en échec puis le second développe la fonctionnalité. Pour la fonctionnalité suivante, les rôles sont inversés.
Test unitaire
Impossible d'accéder à la ressource audio ou vidéo à l'adresse :
La ressource n'est plus disponible ou vous n'êtes pas autorisé à y accéder. Veuillez vérifier votre accès puis recharger la vidéo.
Objectif.
Savoir écrire un test unitaire simple.
Mise en situation
Une première approche des tests est celle du test unitaire. Il s'agit de tester chaque composant, chaque fonction de notre programme de manière isolée.
Imaginez un programme de gestion d'une association, et qui est donc en charge de plusieurs fonctionnalités : gérer les cotisations, la comptabilité, les dons effectués, etc. Le programme est composé de très nombreuses fonctions, comme par exemple une fonction qui calcule un abattement fiscal sur un don. L'idée du test unitaire sera d'écrire un test qui va se charger de lancer cette fonction plusieurs paramètres différents, et de s'assurer que le résultat est toujours celui que l'on attend.
Rappel : Test unitaire
Le but d'un test unitaire est de vérifier le bon fonctionnement d'une composante d'un programme, le plus souvent une fonction. Le test unitaire essaiera d'être exhaustif en vérifiant que chaque ligne de code est viable et ne provoque pas d'erreur. Par exemple, si une fonction contient une condition, il faudra au moins deux tests : un dans lequel la condition est vraie et l'autre dans lequel la condition est fausse.
Fondamental :
On appellera test unitaire une fonction qui appelle de multiples fois une autre fonction afin de tester sa validité selon différents paramètres.
Méthode : Couverture de code
Une ligne de code est considérée comme couverte par un test si elle est exécutée durant le test.
Pour couvrir complètement le code, il faut tester la fonction avec différentes valeurs permettant de vérifier toutes les conditions.
Les conditions doivent être couvertes car elles sont souvent à l'origine de bugs.
Les frameworks de test permettent de calculer facilement la couverture de son code.
Exemple : Tester les valeurs retournées
Voici un exemple de test sur une fonction simple. Si le résultat attendu n'est pas identique à celui retourné par la fonction, le test échoue et affiche un message d'erreur.
/** JavaScript : teste unitairement la fonction backetPrice. */
function basketPrice (listItemPrices) {
let res = 0
for (let i = 0; i < listItemPrices.length; i++) {
res = res + listItemPrices[i]
}
return res
}
function testBasketPrice () {
if (basketPrice([2, 5, 29]) !== 36) {
console.log('Test échoué totalPanier pour [2, 5, 29]')
return false
}
if (basketPrice([]) !== 0) {
console.log('Test échoué totalPanier pour []')
return false
}
console.log('Test totalPanier réussi')
return true
}
testBasketPrice()
"""Python : teste unitairement la fonction backet_price. """
def basket_price(list_item_prices):
res = 0
for i in range(len(list_item_prices)):
res = res + list_item_prices[i]
return res
def test_basket_price():
if basket_price([2, 5, 29]) != 36:
print("Test échoué basket_price pour [2, 5, 29]")
return False
if basket_price([]) != 0:
print("Test échoué basket_price pour []")
return False
print("Test basket_price réussi")
return True
test_basket_price()
Si lors d'une modification du code, une erreur est introduite, par exemple :
for (let i = 0; i < listItemPrices.length - 1; i++)
Le test échouera et affichera :
Test échoué totalPanier pour [2, 5, 29]
Ainsi, l'erreur est détectée et corrigeable avant que les utilisateurs n'expérimentent le bug directement.
Complément : Utiliser la syntaxe assert
Pour simplifier les tests, il est possible d'utiliser un nouvel élément de syntaxe : l'assertion. Une assertion ne fera rien de particulier si l'expression qui la suit est vraie mais une erreur sera émise si l'expression est fausse (AssertionError
pour être précis).
/** JavaScript : teste unitairement la fonction backetPrice en utilisant des assert. */
function basketPrice (listItemPrices) {
let res = 0
for (let i = 0; i < listItemPrices.length; i++) {
res = res + listItemPrices[i]
}
return res
}
function testBasketPrice () {
const assert = require('assert')
assert(basketPrice([2, 5, 29]) === 36)
assert(basketPrice([]) === 0)
console.log('Test totalPanier réussi')
return true
}
testBasketPrice()
"""Python : teste unitairement la fonction total_panier en utilisant des assert. """
def basket_price(list_item_prices):
res = 0
for i in range(len(list_item_prices)):
res = res + list_item_prices[i]
return res
def test_basket_price():
assert basket_price([2, 5, 29]) == 36
assert basket_price([]) == 0
print("Test basket_price réussi")
return True
test_basket_price()
Exemple :
Voici un programme qui teste une fonction renvoyant la liste des prix TTC à partir des prix HT :
/** JavaScript : teste unitairement la fonction htToTtc. */
function htToTtc (htPriceList) {
const ttcPriceList = []
for (let i = 0; i < htPriceList.length; i++) {
const ttcPrice = htPriceList[i] * 1.2
ttcPriceList.push(ttcPrice)
}
return ttcPriceList
}
function testHtToTtc () {
if (htToTtc([2, 5, 10]).toString() !== [2.4, 6, 12].toString()) {
console.log('Test htToTtc échoue pour [2, 5, 10]')
return false
}
if (htToTtc([]).toString() !== [].toString()) {
// On teste également les valeurs extrêmes
console.log('Test htToTtc échoue pour [2, 5, 10]')
return false
}
console.log('Test réussi')
return true
}
testHtToTtc()
"""Python : teste unitairement la fonction HT_to_TTC. """
def HT_to_TTC(ht_price_list):
ttc_price_list = []
for i in range(len(ht_price_list)):
ttc_price = ht_price_list[i] * 1.2
ttc_price_list.append(ttc_price)
return ttc_price_list
def test_HT_to_TTC():
if HT_to_TTC([2, 5, 10]) != [2.4, 6, 12]:
print("Test HT_to_TTC échoue pour [2, 5, 10]")
return False
if HT_to_TTC([]) != []: # On teste également les valeurs extrêmes
print("Test HT_to_TTC échoue pour []")
return False
print("Test réussi")
return True
test_HT_to_TTC()
Complément : Mocker des variables
Un développeur Web écrira des fonctions faisant des requêtes web ou vers des bases de données.
Dans le cadre d'un test, ces appels ne seront pas possibles car on ne peut se permettre d'interroger les systèmes en production. Pour pallier ce problème, il est possible d'utiliser des mocks qui remplacent certaines variables par une valeur pré-définie.
À retenir
Un test unitaire vérifie automatiquement le comportement d'une fonction, avec des valeurs simples et des valeurs extrêmes.
Un bon test unitaire doit prévoir tous les cas d'utilisation de la fonction testée.
Impossible d'accéder à la ressource audio ou vidéo à l'adresse :
La ressource n'est plus disponible ou vous n'êtes pas autorisé à y accéder. Veuillez vérifier votre accès puis recharger la vidéo.
Appliquer la notion
Voici une fonction qui trie dans l'ordre croissant la liste qui lui est passée en paramètre :
function bubbleSort (unsortedList) {
const intList = unsortedList.slice()
for (let i = intList.length - 1; i > 0; i--) {
for (let j = 0; j < i; j++) {
if (intList[j + 1] < intList[j]) {
// Echanger les deux valeurs
const temp = intList[j + 1]
intList[j + 1] = intList[j]
intList[j] = temp
}
}
}
return intList
}
Écrire une fonction testBubbleSort
qui teste le résultat de la fonction pour [3,2,1], [1,2,3], [1,3,2].
Cette fonction doit afficher une erreur si le résultat attendu ne correspond pas au résultat renvoyé.
Il est possible de comparer deux tableaux « a »
et « b »
, en prenant l'ordre en compte, avec le test :
a.toString() !== b.toString()
function testBubbleSort () {
if (bubbleSort([3, 2, 1]).toString() !== [1, 2, 3].toString()) {
console.log('Test échoué pour [3, 2, 1]')
return false
}
if (bubbleSort([1, 2, 3]).toString() !== [1, 2, 3].toString()) {
console.log('Test échoué pour [1, 2, 3]')
return false
}
if (bubbleSort([1, 3, 2]).toString() !== [1, 2, 3].toString()) {
console.log('Test échoué pour [1, 3, 2]')
return false
}
console.log('Test réussi')
return true
}
testBubbleSort()
Quel test de valeur extrême faudrait-il rajouter pour s'assurer que la fonction est correcte ?
Ajouter ce test.
Les valeurs extrêmes sont en général des limites. Pour un entier positif, on testerait le comportement si la valeur d'entrée vaut 0.
Il serait pertinent d'ajouter un test pour un tableau vide : []
if (bubbleSort([]).toString() !== [].toString()) {
console.log('Test échoué pour []')
return false
}
Complément :
Les tests fonctionnent. Mais en revanche, pour être vraiment exhaustifs, il faudrait aussi tester des valeurs aberrantes : envoyer un nombre, une valeur undefined
, etc.
Avec le code actuel, la fonction renverra une erreur. Cette erreur doit être maîtrisée, et il faut le vérifier.
Test fonctionnel
Impossible d'accéder à la ressource audio ou vidéo à l'adresse :
La ressource n'est plus disponible ou vous n'êtes pas autorisé à y accéder. Veuillez vérifier votre accès puis recharger la vidéo.
Objectif
Savoir écrire un test fonctionnel.
Mise en situation
Les tests fonctionnels testent directement les fonctionnalités d'un programme du point de vue de l'utilisateur. Pour cela, au lieu de tester une fonction du code source, on va tester une fonctionnalité du programme, par exemple que l'inscription fonctionne correctement sur un site web. Cette manière de tester est moins ancrée dans la technique que les tests unitaires, et plus orientée vers l'expérience utilisateur. On veut s'assurer ici que l'on n'introduit pas de régressions dans le fonctionnement de notre logiciel.
Rappel : Test fonctionnel
Le but d'un test fonctionnel est de vérifier le bon fonctionnement d'une fonctionnalité complète. Le test sera effectué sur une boite noire, c'est-à-dire que les opérations intermédiaires effectuées par la fonctionnalité sont ignorées. Ainsi, contrairement au test unitaire qui étudie le déroulé d'une composante, le test fonctionnel étudie le comportement d'un programme. Il s'agit donc d'un test plus global qui ne sera pas aussi exhaustif que le test unitaire.
Méthode : Comment tester fonctionnellement ?
Pour tester fonctionnellement, il y a deux étapes : le fonctionnement normal et le fonctionnement accidentel.
Le fonctionnement normal assure que la fonctionnalité se comporte correctement dans les grandes lignes sans essayer tous les cas de figures (la combinatoire est en général trop importante).
Le fonctionnement accidentel essaie des scénarios anormaux qui pourraient faire planter le programme.
Fondamental : Programme résistant aux accidents
Un programme que les tests de fonctionnement accidentel ne font pas planter, sera considéré comme un programme résistant aux accidents.
Il arrive également de croiser une terminologie anglaise plus négativement connotée : idiot-proof program
qui fait référence à l'idiotie supposée des entrées testées.
Attention : Loi de Murphy
Selon la Loi de Murphy, Tout ce qui est susceptible d'aller mal, ira mal. Elle est également vraie en programmation car les utilisateurs se trompent parfois dans l'utilisation d'une application. Cette recherche passera très souvent par un usage totalement illogique de la fonctionnalité. Ainsi, tester des cas de figures ridicules permettra d'éviter ce type d'erreurs pouvant parfois être utilisés pour attaquer une application. Par exemple, un utilisateur pourrait entrer « -18 »
dans la case « Age »
pour voir si le programme plantera à cause de l'âge inférieur à 0.
Exemple :
Voici un exemple simplifié de test fonctionnel de transfert d'argent d'une application bancaire.
Ici, deux cas sont essayés :
transférer avec assez de solde,
et transférer sans avoir assez de solde.
Le test fonctionnel sert ici à valider que le transfert échoue si le solde est insuffisant.
/** JavaScript : teste fonctionnellement le transfert d'argent. */
function checkAccount (account, amount) {
if (account.balance >= amount) {
return true
}
return false
}
function transfer (srcAccount, tgtAccount, amount) {
// Copies des variables
const newSrcAccount = { ...srcAccount }
const newTgtAccount = { ...tgtAccount }
if (checkAccount(newSrcAccount, amount)) {
newSrcAccount.balance = newSrcAccount.balance - amount
newTgtAccount.balance = newTgtAccount.balance + amount
console.log('Transfert réussi')
} else {
console.log('Echec du transfert')
}
return [newSrcAccount, newTgtAccount]
}
function testTransfer () {
let srcAcc = { owner: 'Jean Dupont', balance: 100 }
let tgtAcc = { owner: 'Anne Martin', balance: 20 }
let transferRes = transfer(srcAcc, tgtAcc, 60)
srcAcc = transferRes[0]
tgtAcc = transferRes[1]
// Transfert réussi
if (srcAcc.balance !== 40 || tgtAcc.balance !== 80) {
console.log('Test transfer échoué lors du premier transfert.')
return false
}
transferRes = transfer(srcAcc, tgtAcc, 60)
srcAcc = transferRes[0]
tgtAcc = transferRes[1]
// Transfert échoué donc les comptes doivent être inchangés
if (srcAcc.balance !== 40 || tgtAcc.balance !== 80) {
console.log('Test transfer échoué lors du second transfert.')
return false
}
console.log('Test réussi')
return true
}
testTransfer()
"""Python : teste fonctionnellement le transfert d'argent. """
def check_account(account, amount):
if account["balance"] >= amount:
return True
else:
return False
def transfer(src_account, tgt_account, amount):
new_src_account = src_account.copy()
new_tgt_account = tgt_account.copy()
if check_account(new_src_account, amount):
new_src_account["balance"] = new_src_account["balance"] - amount
new_tgt_account["balance"] = new_tgt_account["balance"] + amount
print("Transfert réussi")
else:
print("Echec du transfert")
return new_src_account, new_tgt_account
def test_transfer():
src_acc = {"owner": "Jean Dupont", "balance": 100}
tgt_acc = {"owner": "Anne Martin", "balance": 20}
src_acc, tgt_acc = transfer(src_acc, tgt_acc, 60)
# Transfert réussi
if src_acc["balance"] != 40 or tgt_acc["balance"] != 80:
print("Test transfer échoué lors du premier transfert.")
return False
src_acc, tgt_acc = transfer(src_acc, tgt_acc, 60)
# Transfert échoué donc les comptes doivent être inchangés
if src_acc["balance"] != 40 or tgt_acc["balance"] != 80:
print("Test transfer échoué lors du premier transfert.")
return False
print("Test réussi")
return True
test_transfer()
Rappel : Tester les entrées/sorties
Tester les entrées/sorties d'un programme est souvent une tâche complexe.
Ces tests sont facilités par les frameworks de test.
Les développeurs ont recours à des mocks, qui simulent des réponses de bases de données ou de serveurs.
Complément : Test d'intégration
Un test d'intégration vérifie que la communication de plusieurs fonctions se fait comme prévue. Il contrôle les assemblages de fonctions.
À retenir
Les tests fonctionnels sont des tests globaux qui testent le comportement d'une fonctionnalité.
Il est nécessaire de tester des valeurs correctes ainsi que des valeurs intentionnellement fausses afin que les fonctionnalités soient résistantes aux usages imprévus.
Pour développer des tests pour des projets de grande envergure il est nécessaire d'utiliser un framework de test.
Impossible d'accéder à la ressource audio ou vidéo à l'adresse :
La ressource n'est plus disponible ou vous n'êtes pas autorisé à y accéder. Veuillez vérifier votre accès puis recharger la vidéo.
Appliquer la notion
Une application bancaire permet d'acheter des produits grâce à la fonction buy
que voici :
function checkAccount (account, amount) {
if (account.balance >= amount) {
return true
}
return false
}
function buy (account, product) {
// Copie de la variable account
const newAccount = { ...account }
if (checkAccount(newAccount, product.price)) {
newAccount.balance = newAccount.balance - product.price
console.log('Produit acheté:', product.name)
} else {
console.log('Echec de la transaction')
}
return newAccount
}
Les enregistrements gérant un compte sont de la forme suivante :
let account = {
owner: 'Pierre',
balance: 10
}
Les produits sont de la forme suivante :
const product = {
name: 'Traces',
price: 19
}
Écrire le code permettant de créer un compte avec une balance de 100 €, ainsi qu'un produit d'une valeur de 60 €.
let acc = {
owner: 'Jean Dupont',
balance: 100
}
const prod = {
name: 'Carte graphique',
price: 60
}
Écrire le test fonctionnel testBuy
de la fonctionnalité de paiement, qui vérifie qu'un solde suffisant valide l'achat et qu'un solde insuffisant le faire échouer.
Tenter d'acheter deux fois le produit avec le compte créé précédemment.
Vérifier que le test échoue.
Une implémentation possible est la suivante :
function testBuy () {
let acc = {
owner: 'Jean Dupont',
balance: 100
}
const prod = {
name: 'Carte graphique',
price: 60
}
acc = buy(acc, prod)
// Paiement réussi
if (acc.balance !== 40) {
console.log('Test buy échoué sur le premier paiement')
return false
}
acc = buy(acc, prod)
// Echec du paiement
if (acc.balance !== 40) {
console.log('Test buy échoué sur le second paiement')
return false
}
console.log('Test réussi')
return true
}
testBuy()
Son exécution renvoie :
Produit acheté: Carte graphique
Echec de la transaction
Test réussi
Ce qui valide que la fonctionnalité se comporte comme attendue.
Quiz
Quiz - Culture
Quel est l'intérêt de développer des tests automatisés ?
Accélérer le développement
Faciliter l'évolution et la maintenance de son code
Réduire la taille de son code
Améliorer la lisibilité du code
Détecter des erreurs dans son code
Mocha est un framework de tests. Quel langage concerne-t-il ?
Python
JavaScript
Java
Pytest
Quels sont les critères qui font qu'un test peut être automatisé ? Aidez-vous de cette page : https://fr.wikipedia.org/wiki/Automatisation_de_test
Répétitivité
Simplicité
Systématique
Rapidité
Comment appelle-t-on une variable ou une fonction dont la valeur a été simulée dans le cadre d'un test ? Ceci est réalisé grâce à des frameworks pour tester des fonctions qui appellent des éléments externes comme des ressources Web ou une base de données. Voici une page Web qui pourrait aider : https://fr.wikipedia.org/wiki/Mock_(programmation_orient%C3%A9e_objet)
Joker
Mock
Remplaçante
Dummy
Quels rôles peuvent remplir les tests fonctionnels ?
Tester le bon affichage d'une interface web
Tester le bon comportement d'une fonctionnalité
Tester la résistance aux valeurs invalides
Qu'est-ce qu'un programme robuste aux accidents ?
Un programme qui résistera même si l'utilisateur entre des valeurs absurdes
Un programme qui corrige automatiquement les erreurs (accidents) du développeur
Un programme d'intelligence artificielle
Un programme de signalisation routière
Quiz - Code
La fonction isFriend()
est-elle couverte à 100 % par testIsFriend()
?
function isFriend (friendList, user) {
for (let i = 0; i < friendList.length; i++) {
if (JSON.stringify(user) === JSON.stringify(friendList[i])) {
return true
}
}
return false
}
function testIsFriend () {
const friends = [
{ lastName: 'Jean', firstName: 'Zay' },
{ lastName: 'Marie', firstName: 'Curie' },
{ lastName: 'Veil', firstName: 'Simone' }
]
const user = { lastName: 'Veil', firstName: 'Simone' }
if (!isFriend(friends, user)) {
console.log('Test estAmi a échoué')
return false
}
console.log('Test estAmi a réussi')
return true
}
testIsFriend()
Oui
Non
Le test ci-dessous affiche : Test estAmi a réussi
function isFriend (friendList, user) {
for (let i = 0; i < friendList.length; i++) {
if (JSON.stringify(user) === JSON.stringify(friendList[i])) {
return true
}
}
return false
}
function testIsFriend () {
const friends = [
{ lastName: 'Jean', firstName: 'Zay' },
{ lastName: 'Marie', firstName: 'Curie' },
{ lastName: 'Veil', firstName: 'Simone' }
]
const user = { lastName: 'Veil', firstName: 'Simone' }
if (!isFriend(friends, user)) {
console.log('Test estAmi a échoué')
return false
}
if (isFriend(friends, {})) {
console.log('Test estAmi a échoué')
return false
}
console.log('Test estAmi a réussi')
return true
}
testIsFriend()
Vrai
Faux
Quiz - Culture
Quel est l'intérêt de développer des tests automatisés ?
Accélérer le développement
Faciliter l'évolution et la maintenance de son code
Réduire la taille de son code
Améliorer la lisibilité du code
Détecter des erreurs dans son code
Accélérer le développement
Le développement n'est pas accéléré. Au contraire, il pourrait être allongé lorsque les développeurs ne sont pas habitués à cette pratique.
Faciliter l'évolution et la maintenance de son code
Réduire la taille de son code
Au contraire, le code sera plus volumineux car il inclura les tests.
Améliorer la lisibilité du code
La lisibilité n'est pas améliorée. Au mieux, les tests permettent à un relecteur de comprendre comme chaque fonction devrait être utilisée mais la lisibilité intrinsèque de la fonction elle-même est inchangée.
Détecter des erreurs dans son code
Les tests permettent de valider le bon fonctionnement du code et d'éviter l'introduction de bug lors des évolutions du code, donc de faciliter sa maintenance.
Mocha est un framework de tests. Quel langage concerne-t-il ?
Python
JavaScript
Java
Pytest
Quels sont les critères qui font qu'un test peut être automatisé ? Aidez-vous de cette page : https://fr.wikipedia.org/wiki/Automatisation_de_test
Répétitivité
Simplicité
Systématique
Rapidité
Répétitivité
Plus un test est répétitif, plus ce sera une tâche qui devrait être fait par une machine.
Simplicité
Il est évidemment préférable que le test soit simple, mais un test complexe n'est pas nécessairement une barrière à son automatisation.
Systématique
Dans le meilleur des cas, ce test doit être utilisé à chaque nouvelle version. Plus un test est utilisé, plus l'intérêt de son automatisation est grand.
Rapidité
Comment appelle-t-on une variable ou une fonction dont la valeur a été simulée dans le cadre d'un test ? Ceci est réalisé grâce à des frameworks pour tester des fonctions qui appellent des éléments externes comme des ressources Web ou une base de données. Voici une page Web qui pourrait aider : https://fr.wikipedia.org/wiki/Mock_(programmation_orient%C3%A9e_objet)
Joker
Mock
Remplaçante
Dummy
Quels rôles peuvent remplir les tests fonctionnels ?
Tester le bon affichage d'une interface web
Tester le bon comportement d'une fonctionnalité
Tester la résistance aux valeurs invalides
Tester le bon affichage d'une interface web
Tester le bon comportement d'une fonctionnalité
Tester la résistance aux valeurs invalides
Un test fonctionnel peut réaliser sa tâche en utilisant des valeurs invalides notamment pour vérifier que des entrées accidentelles ne fassent pas planter le programme.
Qu'est-ce qu'un programme robuste aux accidents ?
Un programme qui résistera même si l'utilisateur entre des valeurs absurdes
Un programme qui corrige automatiquement les erreurs (accidents) du développeur
Un programme d'intelligence artificielle
Un programme de signalisation routière
Quiz - Code
Oui
Non
La fonction n'est pas couverte à 100 % car le test ne vérifie pas que la fonction renvoie faux lorsqu'on lui fournit une personne qui ne fait pas partie de la liste d'amis.
Vrai
Faux
Défi
Nois développons une application d'e-commerce. Il est possible d'acheter les produits grâce à un porte-monnaie virtuel inclus dans l'application. Voici les fonctions de l'application dans un programme montrant comment elles peuvent être utilisées :
// On définit une classe représentant un panier
class Basket {
constructor (items = [], totalPrice = 0) {
this.items = items
this.totalPrice = totalPrice
}
}
function addToBasket (basket, item) {
basket.items.push(item)
basket.totalPrice = basket.totalPrice + item.price
}
function removeFromBasket (basket, item) {
for (let i = 0; i < basket.items.length; i++) {
if (JSON.stringify(item) === JSON.stringify(basket.items[i])) {
basket.items.splice(i, 1)
basket.totalPrice = basket.totalPrice - item.price
break
}
}
}
function transactionAllowed (userAccount, priceToPay) {
if (userAccount.balance >= priceToPay) {
return true
}
return false
}
function payBasket (userAccount, basket) {
if (transactionAllowed(userAccount, basket.totalPrice)) {
userAccount.balance = userAccount.balance - basket.totalPrice
console.log('Paiement du panier réussi')
} else {
console.log('Paiement du panier échoué')
}
}
const currentBasket = new Basket()
const item1 = { name: 'Carte mère', price: 100 }
const item2 = { name: 'Carte graphique', price: 300 }
const user = { name: 'Perceval', balance: 500 }
addToBasket(currentBasket, item1)
addToBasket(currentBasket, item2)
// Plus qu'un produit dans le panier
removeFromBasket(currentBasket, item1)
console.log(currentBasket)
payBasket(user, currentBasket)
console.log(user)
// Perceval n'a plus que 200 euros
Développer un test unitaire qui ajoute un produit au panier et vérifie que le montant est bien celui prévu. Ce test sera nommé testAdd()
.
Par exemple :
Ajouter un produit dans le panier, comme :
const item = {name: "Carte mère", price: 100}
Vérifier que
basket.totalPrice
vaut 100 :
function testAdd () {
const testBasket = new Basket()
const item = { name: 'Carte mère', price: 100 }
addToBasket(testBasket, item)
if (testBasket.totalPrice !== 100) {
console.log('Test ajout échoué')
return false
}
console.log('Test ajout réussi')
return true
}
testAdd()
Développer un test unitaire qui supprime un produit du panier et vérifie que le montant est bien celui prévu. Ce test sera nommé testRemove()
.
Il faudra au préalable ajouter un produit dans un panier vide avant de pouvoir retirer celui-ci pour tester le retrait.
Par exemple :
Ajouter un objet au panier ;
Le retirer immédiatement ;
Vérifier que le montant total du panier est nul.
function testRemove () {
const testBasket = new Basket()
const item = { name: 'Carte mère', price: 100 }
addToBasket(testBasket, item)
removeFromBasket(testBasket, item)
if (testBasket.totalPrice !== 0) {
console.log('Test retrait échoué lors du premier retrait')
return false
}
console.log('Test retrait réussi')
return true
}
testRemove()
On constate qu'il est possible de factoriser les tests unitaires des fonctions d'ajout et de retrait précédentes.
Donner le test factorisé nommé testAddRemove()
.
Le test de retrait se base sur l'ajout d'un produit. Il est donc possible de tester d'abord l'ajout d'un produit, puis son retrait, au sein de la même fonction.
function testAddRemove () {
const testBasket = new Basket()
const item = { name: 'Carte mère', price: 100 }
addToBasket(testBasket, item)
if (testBasket.totalPrice !== 100) {
console.log("Test ajout-retrait échoué lors de l'ajout")
return false
}
removeFromBasket(testBasket, item)
if (testBasket.totalPrice !== 0) {
console.log('Test ajout-retrait échoué lors du premier retrait')
return false
}
console.log('Test ajout-retrait réussi')
return true
}
testAddRemove()
Donner maintenant un test unitaire qui teste entièrement la fonction transactionAllowed()
. La fonction de test s'appellera testTransactionAllowed()
.
Il faut que chacune des branches de la condition soit vérifiée, donc il faudra appeler deux fois la fonction testée : une fois pour un solde suffisant, une fois pour un solde insuffisant.
Par exemple :
Ajouter un utilisateur avec un solde de 500 € ;
Vérifier qu'une transaction de 400 € est autorisée ;
Vérifier qu'une transaction de 600 € est interdite.
function testTransactionAllowed () {
const testUser = { name: 'Perceval', balance: 500 }
if (!transactionAllowed(testUser, 400)) {
console.log('Test transactionAllowed échoué pour 400')
return false
}
if (transactionAllowed(testUser, 600)) {
console.log('Test transactionAllowed échoué pour 600')
return false
}
console.log('Test transactionAllowed réussi')
return true
}
testTransactionAllowed()
Écrire un test fonctionnel pour le règlement d'un panier qu'on nommera testPayBasket()
. Le test vérifiera que le solde de l'utilisateur est bien mis à jour après règlement.
Pour réaliser ce test fonctionnel, il faut créer un panier puis essayer de régler deux fois le panier. La première fois, la transaction doit réussir. La seconde fois, la transaction doit échouer car l'utilisateur n'a plus assez d'argent, donc son solde ne doit pas être mis à jour.
Par exemple :
Ajouter un utilisateur avec un solde de 500 € ;
Ajouter un produit à son panier valant 300 € ;
Effectuer le règlement et vérifier que son solde est passé à 200 € ;
Essayer d'effectuer un deuxième règlement et vérifier que son solde reste à 200 €.
function testPayBasket () {
const testUser = { name: 'Perceval', balance: 500 }
let testBasket = new Basket()
testBasket = addToBasket(testBasket, { name: 'Carte mère', price: 300 })
payBasket(testUser, testBasket)
// Paiement réussi
if (testUser.balance !== 200) {
console.log('Test régler panier échoué lors de la première transaction')
return false
}
payBasket(testUser, testBasket)
// Paiement échoué car le solde n'a pas changé
if (testUser.balance !== 200) {
console.log('Test régler panier échoué lors de la première transaction')
return false
}
console.log('Test de la fonctionnalité de règlement du panier réussi')
return true
}
testPayBasket()
Écrire une fonction testAppEcommerce()
qui lance successivement tous les tests. Cette fonction affichera « OK »
si tous les tests sont passés, et « ERREUR »
sinon.
Écrire le programme complet permettant de réaliser tous les tests.
Tous les tests retournent true
ou false
.
Il est possible de faire des opérations booléennes :
const success = true && false // Ici, on utilise l'opérateur logique ET
On pourra alors vérifier que l'intégralité des tests a renvoyé true
, en chaînant les &&
.
function testAppEcommerce () {
let success = testAddRemove()
success = success && testTransactionAllowed()
success = success && testPayBasket()
if (success) {
console.log('OK')
} else {
console.log('ERREUR')
}
}
// On définit une classe représentant un panier
class Basket {
constructor (items = [], totalPrice = 0) {
this.items = items
this.totalPrice = totalPrice
}
}
function addToBasket (basket, item) {
basket.items.push(item)
basket.totalPrice = basket.totalPrice + item.price
}
function removeFromBasket (basket, item) {
for (let i = 0; i < basket.items.length; i++) {
if (JSON.stringify(item) === JSON.stringify(basket.items[i])) {
basket.items.splice(i, 1)
basket.totalPrice = basket.totalPrice - item.price
break
}
}
}
function transactionAllowed (userAccount, priceToPay) {
if (userAccount.balance >= priceToPay) {
return true
}
return false
}
function payBasket (userAccount, basket) {
if (transactionAllowed(userAccount, basket.totalPrice)) {
userAccount.balance = userAccount.balance - basket.totalPrice
console.log('Paiement du panier réussi')
} else {
console.log('Paiement du panier échoué')
}
}
function testAddRemove () {
const testBasket = new Basket()
const item = { name: 'Carte mère', price: 100 }
addToBasket(testBasket, item)
if (testBasket.totalPrice !== 100) {
console.log("Test ajout-retrait échoué lors de l'ajout")
return false
}
removeFromBasket(testBasket, item)
if (testBasket.totalPrice !== 0) {
console.log('Test ajout-retrait échoué lors du premier retrait')
return false
}
console.log('Test ajout-retrait réussi')
return true
}
function testTransactionAllowed () {
const testUser = { name: 'Perceval', balance: 500 }
if (!transactionAllowed(testUser, 400)) {
console.log('Test transactionAllowed échoué pour 400')
return false
}
if (transactionAllowed(testUser, 600)) {
console.log('Test transactionAllowed échoué pour 600')
return false
}
console.log('Test transactionAllowed réussi')
return true
}
function testPayBasket () {
const testUser = { name: 'Perceval', balance: 500 }
const testBasket = new Basket()
addToBasket(testBasket, { name: 'Carte mère', price: 300 })
payBasket(testUser, testBasket)
// Paiement réussi
if (testUser.balance !== 200) {
console.log('Test régler panier échoué lors de la première transaction')
return false
}
payBasket(testUser, testBasket)
// Paiement échoué car le solde n'a pas changé
if (testUser.balance !== 200) {
console.log('Test régler panier échoué lors de la deuxième transaction')
return false
}
console.log('Test de la fonctionnalité de règlement du panier réussi')
return true
}
function testAppEcommerce () {
let success = testAddRemove()
success = success && testTransactionAllowed()
success = success && testPayBasket()
if (success) {
console.log('OK')
} else {
console.log('ERREUR')
}
}
testAppEcommerce()
Conclusion
Écrire des tests pour son code est une tâche qui fait partie du métier de développeur et qu'il ne faut pas négliger. Les tests permettent d'apporter plus de fiabilité, et donc plus de sérénité lorsque l'on déploie une mise à jour de son code. Il est important de ne pas remettre les tests à plus tard, et de commencer leur écriture dès le début du développement d'un programme.
Nous avons vu qu'il existe deux types de tests, unitaire et fonctionnel. Leur approche n'est pas concurrente mais complémentaire, et ces deux types de tests sont généralement mis en place conjointement.