❑ Introduction
❑ Méthodestraditionnelles et tests
❑ Impacts et coût des défauts
❑ Test Driven Development
❑ Définition
❑ Méthodologie
❑ Avantages, inconvénients
❑ Injection de dépendances, fakes et mocks
❑ Exemple
❑ Expérience personnelle
Introduction
2
❑ Le codelogiciel est fragile
❑ Quasiment n’importe quel changement peut avoir des conséquences
inattendues
❑ Le but de cette présentation est de montrer une méthode de
développement qui permet d'améliorer la qualité logiciel, et de réduire
les coûts de développement
❑ Grâce à cette méthode, nous pourrons à tout moment nous assurer de la
maturité du logiciel, et être immédiatement averti de toute déviation,
régression ou défaut, et ce dès leur introduction dans le code.
Programmation Pilotée par les Tests
=
Test Driven Development
Introduction
4
5.
Méthodes traditionnelles :waterfall
5
User
requirements
Functional
specification
s
Global
technical
design
Detailed
technical
design
Programming
Testing CODE DRIVEN
DEVELOPMENT
6.
Méthodes traditionnelles :cycle en
V
6
User
requirement
s
Functional
specification
s
Global
technical
design
Detailed
technical
design
Programming
Component
tests
Integration
tests
System tests
Acceptance
tests
7.
❑ Lorsque lestests ne sont pas automatisés, éventuellement seuls les tests que l’on pense
être nécessaires seront utilisés
❑ Parfois seulement des tests fonctionnels sont utilisés (surtout en waterfall), rendant
souvent impossible le test des gestions d’erreurs
❑ Les tests unitaires (si existants) sont ajoutés après la phase de codage
❑ Couverture de code non complète
❑ Tout le code n’est pas testable (couplage fort entre interfaces)
❑ Tendre vers 100% de couverture demande un effort exponentiel
❑ On ne connaît pas la maturité du code avant les phases de test (et les résultats peuvent
être trompeurs)
Méthodes traditionnelles & tests
7
8.
Méthodes traditionnelles &tests
8
❑ La détection des défauts intervient bien après la phase de codage
❑ Tous les défauts ne peuvent pas être découverts
❑ Un bug simple peut n’être découvert que très tard
❑ Plus un bug est trouvé tard, plus il est coûteux à fixer
❑ La qualité d’un logiciel ne se mesure pas seulement dans son comportement ni dans
ses performances
❑ Le code doit être maintenable, donc simple à comprendre
❑ Conséquences :
❑ Aucune ou peu de confiance dans la maturité du code
❑ Le projet prend du retard
❑ Les coûts ne sont pas maîtrisés
Exemple de bugcouteux
10
❑ Ariane 5 – Juin 1996
o Un bug du logiciel de la fusée provoqua son autodestruction :
o Récupération du soft Ariane 4 : accélération codée sur 8 bit
o Sur Ariane 5 (accélération plus grande) => dépassement de capacité (9
bits auraient été nécessaires)
o Les pertes sont estimées à un total de 370 millions de dollars.
o http://fr.wikipedia.org/wiki/Vol_501_d%27Ariane_5
❑ Therac-25
o Machine de radiothérapie, évolution des modèles précédents
o Entre 1985 et 1987, des patients reçurent des doses massives de radiation,
au moins cinq patients décédèrent.
o Plusieurs problèmes de gestion du projet informatique furent découverts
après enquête, dont des tests logiciels incomplets.
o http://fr.wikipedia.org/wiki/Therac-25
11.
Tests unitaires :pas suffisant !
11
Code testé :
int checksum(void *p, int len)
{
int sum = 0;
unsigned char* pp = (unsigned char*)p;
int i;
for (i = 0; i <= len; i++)
{
sum += *pp++;
}
return sum;
}
❑ Test unitaire :
char *myString = "foo";
assert(324 == checksum(myString, strlen(myString)));
❑ Couverture incomplète : bug potentiel non détecté !
Le problème avec cette fonction
checksum est dans la boucle for. La
condition i <= len doit être corrigée
pour i < len, car sinon, la boucle
parcourra un octet de plus que la
longueur réelle de la chaîne, ce qui
entraînera un dépassement de
mémoire tampon et un
comportement indéfini. De plus,
vous devriez utiliser un type de
donnée plus grand pour stocker la
somme, comme unsigned int, pour
éviter un débordement de la somme
en cas de longues chaînes.
12.
❑ Code testé:
void checkRange(int i, int j)
{
if (i >= 3) throw std::exception("out of range");
if (j >= 3) throw std::exception("out of range");
}
❑ Test unitaire : 100% de couverture
EXPECT_THROW(checkRange(0, 3))
EXPECT_THROW(checkRange(3, 0))
❑ Mais supposons qu’un nombre négatif ne
soit pas admis :
checkRange(-1, 2);
Tests unitaires : pas suffisant !
12
Le problème principal avec cette fonction est qu'elle utilise std::exception
de manière incorrecte. std::exception est une classe de base pour toutes
les exceptions standard C++, mais elle n'a pas de constructeur qui prend
une chaîne de caractères comme argument.
Pour créer une exception personnalisée avec un message, vous devez
définir votre propre classe d'exception dérivée de std::exception et fournir
un constructeur prenant une chaîne de caractères comme argument.
EXPECT_THROW(checkRange(0, 3), std::out_of_range);
EXPECT_THROW(checkRange(3, 0), std::out_of_range);
try {
checkRange(-1, 2);
} catch (const std::out_of_range& e) {
// Gérer l'exception ici
}
Cette correction permet à la fonction checkRange de lancer une exception
std::out_of_range avec un message spécifique lorsque les valeurs i ou j
sont en dehors de la plage spécifiée.
13.
❑ TDD estune méthode de développement incrémental
❑ Le principe :
o En testant en premier, je conçois le code
❑ La méthode :
o Aucun code n’est implémenté avant d’avoir écrit un test unitaire qui échoue
o Ensuite, le code est implémenté, le test unitaire réussi
❑ Origine :
o eXtreme Programming (XP)
o 1999 Kent Beck, Martin Fowler et autres…
❑ Souvent utilisé dans un cycle Agile, mais ce n’est pas obligatoire !
❑ Très orienté DEVSECOPS avec des cycles CI/CD !
TDD
13
❑ TDD inversela méthode classique de codage en premier et debug plus tard
❑ TDD implique que le développeur d’un module se concentre donc dès le début sur :
o Son interface : comment vais-je l’utiliser ?
o Son comportement : que fait-il ?
o Sa réutilisation : clients multiples du code et des tests
o Ses dépendances : il doit être testé de façon isolée
o Sa cohésion : un module testable a une raison d’être
❑ Les détails d’implémentation sont secondaires
❑ Ceci encourage le découplage des interfaces
o Code plus modulaire
o Un système découplé est plus évolutif
❑ Chaque ajout de code aura un test ou des tests correspondant
Commencer par écrire des tests ?
15
Tests unitaires pour tester :
- ses classes
- ses API privées
- ses API publiques
16.
❑ L’automatisation destests est un élément clé de TDD
o Continuous Integration (aka CI)
❑ Les tests sont simples
❑ A chaque étape du développement, de nouveaux tests sont ajoutés, suivi de
l’implémentation
❑ A chaque changement introduit dans du code existant, les tests sont exécutés
o Test du nouveau code
o Test du code existant
❑ Toute régression est ainsi immédiatement trouvée et est facile à corriger
Automatisation
16
17.
Coûts des défauts
17
Bugintroduit Bug
découvert
Cause
trouvée
Correction
Bug introduit
Bug
découvert
Cause
trouvée
Correction
❑ Traditionnelleme
nt :
❑ Avec TDD :
Parallélisation, réduction des temps d’action !
18.
❑ Meilleure couverturede test (but : 100%)
❑ Moins de défauts, moins d’effets de bord, moins de temps passé à debugger
❑ Le code est conçu de façon à être plus facile à tester
❑ Code plus modulaire
❑ Design plus propre et plus facile à comprendre
❑ La refactorisation devient plus facile
❑ Les tests constituent une documentation bas-niveau
o Pour chaque feature, il y a au moins un exemple d’utilisation
o Toujours à jour !
TDD : avantages
18
19.
❑ TDD negarantie pas une bonne architecture ou design
❑ TDD est plus difficile à utiliser dans les situations où des tests fonctionnels sont requis
pour déterminer le succès ou l'échec
o Interfaces homme-machine
o Base de données
o Réseaux
❑ Le support du management est essentiel
o Sans l’entière organisation convaincue que TDD va améliorer le produit, le temps
passé à écrire les tests est souvent vu comme perdu
❑ Les tests sont typiquement écrits par le développeur du code testé
❑ TDD ne remplace pas les autres activités de test (intégration, validation, conformité,
etc)
TDD : vulnérabilités
19
20.
❑ Bob Martindécrit TDD avec trois règles simples :
o Do not write production code unless it is to make a failing unit test pass ;
o Do not write more of a unit test than is sufficient to fail, and build failures are
failures ;
o Do not write more production code than is sufficient to pass the one failing unit test.
http://butunclebob.com/ArticleS.UncleBob.TheThreeRulesOfTdd
TDD : les trois lois
20
❑ Les testsmontrent que le comportement est correct : le code fonctionne
❑ Mais du code doit être propre et bien structuré
o Plus facile à comprendre
o Plus facile à faire évoluer
o Plus facile à maintenir
❑ C’est le but de la dernière étape du microcycle
❑ La refactorisation est l’activité de changer la structure d’un code sans en changer son
comportement
❑ Souvent appelé « Red-Green-Refactor »
TDD : refactorisation
22
TDD : exemple
25
⮚Test:
#include <assert.h>
#include "quad.h"
int main()
{
Quad q;
assert(6 == q.area(2, 3));
return 0;
}
⮚Code :
• Le test ne compile pas car le code est inexistant !
26.
TDD : exemple
26
⮚Test:
#include <assert.h>
#include "quad.h"
int main()
{
Quad q;
assert(6 == q.area(2, 3));
return 0;
}
⮚Code :
class Quad {
public:
Quad () {;}
int area(int w, int h) { return
6; }
};
→ Cette fonction a pour but de
calculer l’aire d’un quadilatère.
• Le test compile et réussi !
27.
TDD : exemple
27
⮚Test:
#include <assert.h>
#include "quad.h"
int main()
{
Quad q;
assert(6 == q.area(2, 3));
return 0;
}
⮚Code :
class Quad {
public:
Quad () {;}
int area(int w, int h) { return
w * h; }
};
• Le test passe toujours
28.
TDD : exemple
28
⮚Test:
#include <assert.h>
#include "quad.h"
int main()
{
Quad q;
assert(6 == q.area(2, 3));
assert(10 == q.perimeter(2, 3));
return 0;
}
⮚Code :
class Quad {
public:
Quad () {;}
int area(int w, int h) { return
w * h; }
};
• Le test ne compile plus !
29.
TDD : exemple
29
⮚Test:
#include <assert.h>
#include "quad.h"
int main()
{
Quad q;
assert(6 == q.area(2, 3));
assert(10 == q.perimeter(2, 3));
return 0;
}
⮚Code :
class Quad {
public:
Quad () {;}
int area(int w, int h) { return
w * h; }
int perimeter(int wd, int ht)
{ return 10; }
};
• Le test passe de nouveau !
30.
TDD : exemple
30
⮚Test:
#include <assert.h>
#include "quad.h"
int main()
{
Quad q;
assert(6 == q.area(2, 3));
assert(10 == q.perimeter(2, 3));
return 0;
}
⮚Code :
class Quad {
public:
Quad () {;}
int area(int w, int h) { return
w * h; }
int perimeter(int wd, int ht)
{ return wd + wd + ht + ht; }
};
• Le test passe toujours !
31.
TDD : exemple
31
⮚Test:
#include <assert.h>
#include "quad.h"
int main()
{
Quad q;
assert(6 == q.area(2, 3));
assert(10 == q.perimeter(2, 3));
return 0;
}
⮚Code :
class Quad {
public:
Quad (int wd, int ht) :
m_wd(wd),
m_ht(ht)
{;}
int area() { return m_wd * m_ht;
}
int perimeter()
{ return m_wd + m_wd + m_ht +
m_ht; }
private:
int m_wd;
int m_ht;
};
• Le code est refactorisé, mais le test ne compile plus !
• Sachant que le test ne passerait plus, il aurait pu être
modifié en premier
32.
TDD : exemple
32
⮚Test:
#include <assert.h>
#include "quad.h"
int main()
{
Quad q(2, 3);
assert(6 == q.area());
assert(10 == q.perimeter());
return 0;
}
⮚Code :
class Quad {
public:
Quad (int wd, int ht) :
m_wd(wd),
m_ht(ht)
{;}
int area() { return m_wd * m_ht;
}
int perimeter()
{ return m_wd + m_wd + m_ht +
m_ht; }
private:
int m_wd;
int m_ht;
};
• Le test compile de nouveau et passe
33.
❑ Cet exempleest loin d’être parfait et reste incomplet…
o En effet, la classe Quad accepte des valeurs négatives
o Faut-il ajouter des tests avec valeurs négatives ?
o Dans ce cas, elles doivent être testées dans le constructeur
o Faut-il modifier la classe avec des entiers non signés ?
❑ Si les tests réussissent, cela ne signifie pas qu’il n’y a pas de bug !
❑ En règle générale, le développeur doit toujours tester les valeurs extrêmes passées en
argument, les pointeurs nuls, les débordements de variables…
❑ La relecture de code par des pairs devrait donc aussi s’attarder sur les tests
❑ Attention à la refactorisation qui peut induire que le code n’est plus couvert par les
tests à 100%
TDD : exemple
33
34.
❑ Un frameworkde tests unitaires fournit :
o Un langage commun pour exprimer les cas de test (eg: google test)
o Un langage commun pour exprimer les résultats attendus
o Permet d’accéder aux features du langage du code testé
o Collecte les résultats de test, fournit un rapport de test
o Fournit un mécanisme pour faire tourner les tests
o Soit tous les tests
o Soit seulement certains tests
❑ Quatre phases pour chaque test :
1. Etablissement des pré-conditions du test (passant/nominal et générant des
erreurs)
2. Exercice du code testé
3. Vérification des résultats
4. Retour du système testé à son état initial
❑ http://en.wikipedia.org/wiki/List_of_unit_testing_frameworks
Test frameworks
34
35.
❑ Les testsunitaires doivent être confinés à un processus
❑ Il est déconseillé d’avoir des tests unitaires faisant appel à des systèmes externes
(réseau, base de données, hardware, …)
o Ne pas confondre tests unitaires et tests d’intégration
o Dépendances externes pouvant influer sur les tests
o Ralentit l’exécution des tests unitaires
❑ Lorsque du code testé unitairement dépend d’une ressource externe, une interface
doit représenter cette ressource
❑ L’implémentation de cette interface doit être faite de deux façons :
o Appels réels
o Appels simulés ou stubbé/mock (Eg: google moc)
❑ Le code testé par tests unitaires appellera l’implémentation simulée/stubé/mocké
Injection de dépendances
35
36.
❑ Fakes :
oFonctions ou objets qui ne font pas grand-chose, mais qui permettent à un test de
vérifier un comportement correct
o Retour d’une valeur prédéterminée
❑ Mocks :
o Fonctions ou objets qui sont plus évolués que des fakes
o Peuvent contenir eux-mêmes des assertions
o Peuvent simuler un comportement
❑ Un avantage indéniable des fakes et mocks est la possibilité de simuler des conditions
d’erreur, ce qui est parfois impossibles à réaliser avec l’implémentation réelle
❑ Il reste possible de faire tourner une sélection des tests unitaires contre
l’implémentation réelle (= tests d’intégration)
Fakes & mocks
36
37.
❑ Le testunitaire de fonctions statiques ou de méthodes privées reste un débat non
tranché
❑ Le problème est de permettre aux tests unitaires d’appeler ces fonctions :
o Différenciation des interfaces par #ifdef ou autres équivalents
o N’aide pas à garder un code propre
o Ajout de fonctions que les tests appelleront
o Ce peut être automatiquement fait par le préprocesseur
o Par pointeurs sur fonctions
o Certains langages offrent par « réflexion » un moyen de test
Fonctions statiques & méthodes
privées
37
38.
❑ Pratique deTDD de 2003 à 2009 en C et C++ (embedded software)
o Dernier projet : 45Ksloc, ~600 tests, ~45 secondes
❑ TDD est contre intuitif au premier abord
o Ecrire les tests en premier
❑ Maîtriser TDD prend du temps
o Découplage des interfaces
o Eviter les dépendances entre tests
❑ TDD allié à d’autres techniques permet d’atteindre un niveau de maturité
(mesurable) impossible à atteindre autrement
o Stratégie globale de codage et de test
o Tests d’intégration et fonctionnels automatisés
o Une régression doit devenir la priorité numéro 1
o Si défaut trouvé en aval, rajouter un test en amont
Expérience personnelle
38
39.
❑ Qualité ducode de test doit être au même niveau que le code testé
o Aucun warning autorisé (au minimum -Wall –Wextra)
o Facilite la maintenance du code de test
❑ Appliquer TDD à du code existant est difficile voire parfois impossible
❑ TDD ne peut pas sauver un projet si les spécifications ne sont pas correctes
Expérience personnelle
39
Frameworks de testsunitaires
41
❑ Il existe une multitude de framework par langage :
o C : xTests, CUnit, Google/test, API Sanity autotest,
CU, …
o C++ : Google/test, CppUnit, CppTest, Tpunit++, …
o Java : Junit, jWalk, …
o PHP : PHPUnit, line, jMock
o Python : PyUnit / xPyUnit / TestOOB, Nose
o ObjectiveC : Iphone unit testing, ObjcUnit, GHUnit, OCUnit
o Ruby : RSPec, Test::Unit, minitest
o Shell : assert.sh, shUnit/shUnit2, ATF, filterunit
o XML : Xunit, WUnit
Pour plus d’infos :
http://en.wikipedia.org/wiki/List_of_unit_testing_frameworks
42.
Système d’intégration continue
42
❑L'intégration continue est un ensemble de pratiques utilisées en génie logiciel. Elles consistent à
vérifier à chaque modification de code source que le résultat des modifications ne produit pas de
régression de l'application en cours de développement. Bien que le concept existait auparavant,
l'intégration continue se réfère généralement à la pratique de l'extreme programming.
❑ Il existe une multitude de logiciels d’intégration continue :
o Apache Continuum
o Bamboo
o Buildbot
o CruiseControl
o Jenkins/Hudson
o Gitlab
Pour plus d’information :
http://en.wikipedia.org/wiki/Comparison_of_continuous_integration_software
43.
❑ Le fuzzingest une technique pour tester des logiciels. L'idée est d'injecter des données aléatoires
dans les entrées d'un programme. Si le programme échoue (par exemple en crashant ou en
générant une erreur), alors il y a des défauts à corriger. Exemples de point d'entrée d'un
programme :
o Fichiers ;
o Périphériques (clavier, souris, …) ;
o Variable d’environement
o Réseau
o Limitation de ressources (CPU, Mémoire, Accès I/O, … ) ;
o Etc …
Fuzzing
43
44.
❑ Le grandavantage du fuzzing est que l'écriture de tests est extrêmement simple, ne demande aucune
connaissance du fonctionnement du système et permet de trouver des vulnérabilités facilement.
D'ailleurs, le fuzzing est également utilisé pour traquer des failles de sécurité ou dans la rétro-
ingénierie.
❑ La première trace du fuzzing est la publication datant du 12 décembre 1990 : « An Empirical Study of
the Reliability of UNIX Utilities » [1] écrite par Barton P. Miller, Lars Fredriksen, et Bryan So. Le résumé
indique que durant les essais ils ont été capables de crasher 25 à 33% des programmes utilitaires de
n'importe quelle version d'UNIX ». Le rapport présente les outils de test mais également l'origine des
erreurs.
❑ Le fuzzing est tellement simple à utiliser et efficace pour trouver des vulnérabilités que le chercheur
en sécurité informatique Charlie Miller a refusé de dévoiler les vulnérabilités zero day trouvées dans le
code de logiciels célèbres (contrairement au règlement du concours de sécurité informatique
Pwn2own), afin de protester contre les éditeurs qui n'utilisent pas assez cette technique simple selon
lui.
❑ Inversement au fuzzing qui est une méthode de test par boîte noire, la méthode de test par boîte
blanche analyse un système dont on connaît exactement le fonctionnement.
fuzzing
44
❑ 🡪 Google/test: framework multiplateforme permettant l’écriture de tests en C/C++.
Basé sur l’architecture xUnit il supporte la découverte de tests en automatique et offre
une multitude de macro.
o Avantage(s):
o Facile à utiliser via un ensemble de macros
o Nombre de fonctionnalités (jouer un test x fois, random, … )
o Compatible xUnit
o Inconvénient(s):
o Mauvais support autotools
❑ 🡪 Google/mock :
o Avantage(s):
o Permet de tester des composants complexes
o Inconvénient(s):
o Peut être complexe à implémenter (doit être inclus dans les plannings)
o Risque de dérive par rapport au comportement réel
Google test / mock
46
47.
TDD & C/C++/googletest
Développement d’un composant suivant une démarche TDD
47
foo.h/hpp
API
(H/HPP)
Framework
Google/test
Composant compilé par
gcc/g++ :
- binaire sans le main()
- Librairie
statique/dynamique
Test unitaire
gtest_foo.cpp
testant l’API de foo.h
Test unitaire
Librairie
gtest.so
LINK par LD
Exigences
API
API
gtest.
h
libgest
s.so
API
CODE
foo.o gtest_foo.o
Runtime: ./gtest_foo
Résultat des tests au format
48.
48
TDD & C/C++/googletest
Développement d’un composant suivant une démarche TDD
SUT
MOCK
1
MOCK 2
(STUB)
Composant en
développement
(C/C++)
API
(H/HPP)
Test unitaire
LINK par LD
framework
Google/test
Programme
de tests basé
sur l’API
Résultat des tests au format
Junit/XML
pilotag
e
pilotag
e
Composant
Réel
(Ex: bdd)
49.
TDD / C++/ JENKINS
49
Jenkins
Repository
GIT,
Clearcase,
Subversion,
…
Développeur
d’un composant
trigger
Build du composant: make
Configuration du composant:
./configure –prefix=/usr
Installation dans le stagingdir:
make DESTDIR=<stagingdir> install
Installation dans le rootfs final:
make DESTDIR=<rootfsdir> install
Lancement des tests unitaires:
make check
Mesure de couverture de code:
gcov/lcov
Génération de la doc au format
doxygen: make html
❑ Contexte :Création d’une application Web de gestion
❑ Technologies Projet : J2EE, JSF, Hibernate, Spring
❑ Mode de contractualisation : Forfait Agile/SCRUM
❑ Sprints (itérations) de 4 semaines
❑ Mise en œuvre de pratiques d’eXtreme Programming :
o Test Driven Development : développement orienté par les tests
o Revue de pair
o Intégration Continue
❑ Solution technique
o Tests unitaires sur les couches métiers : JUnit
o Serveur d’intégration continue : Hudson
o Plugins :
o JUnit : non régression
o Duplicate Code : code dupliqué et factorisation possible
o CheckStyle : respect des règles de développement
o PMD : qualité du code
o Cobertura : couverture du code par les tests unitaires
o Java NCSS : commentaires & documentation
REX Viaccess : contexte
51
❑ Itérations courtes: JUnit permet de se protéger contre la régression et évite de
retester exhaustivement l’application à chaque itération
❑ Avantages :
o passage automatique :
o à chaque modification dans SVN
o Régulièrement (toutes les 2 heures ou build de nuit)
o notification des résultats part mail
❑ Inconvénients : se limite aux tests unitaires
❑ développés
REX Viaccess : JUnit
53
54.
❑ Duplicate Code: code dupliqué et factorisation possible
❑ Avantages : reconnait les des bouts de code copiés/collés
mais également des structures de données approchantes
=> conception objet à améliorer
❑ Inconvénients :
❑ ne prend pas en
❑ compte le métier
REX Viaccess : Duplicate Code
54
55.
❑ Respect desrègles de développement
❑ Avantages :
o paramétrable suivant les règles de
développement du client
o Apporte une explication détaillée
sur comment corrigé le problème
❑ Inconvénients : aucun
REX Viaccess : CheckStyle
55
56.
❑ PMD :qualité du code
❑ Avantages :
❑ Inconvénients :
REX Viaccess : PMD
56
57.
❑ Cobertura permetde vérifier la couverture du code par
les tests unitaires
❑ Avantages :
❑ Inconvénients :
REX Viaccess : Cobertura
57
58.
❑ Java NCSS: mensure du taux de commentaires et de documentation
❑ Avantage : donne une indication de l’état du projet
❑ Inconvénient : résultats pas exploitable pour action
REX Viaccess : Java NCSS
58