Vous êtes sur la page 1sur 44

Les tests

⬥ Généralités
⬥ Tests d’intégration
⬥ Tests unitaires
⬥ Tactiques de tests
Les tests

Généralités

La stratégie de test la plus répandue consiste à :


⬥ truffer le code de traces temporaires
⬥ lancer l'application en l’exécutant avec quelques
valeurs (au hasard !)
⬥ et enfin vérifier dans les traces le bon comportement
de l'ensemble
Les tests

Généralités

Cette stratégie est malheureusement loin d'être efficace


pour au moins deux raisons :
⬥ La mise en place du contexte de test, l'exécution
proprement dite et l'interprétation des résultats sont
trop coûteux en temps
⬥ Les tests ne portent en général que sur la
fonctionnalité en cours de développement, et trop
peu de régressions sont ainsi détectées
Les tests

Généralités

⬥ Une solution pour améliorer cette situation est de réaliser des


tests unitaires
⬥ Pour chaque module de l'application, les développeurs
écrivent un module chargé de le tester et de vérifier le
résultat de ces tests de manière automatique, c'est-à-dire sans
intervention du développeur
⬥ Par rapport aux tests manuels évoqués précédemment, la
granularité des tests unitaires est extrêmement fine
⬥ Ils portent sur des portions de quelques lignes de code
uniquement
Les tests

Généralités

⬥ La question se pose alors de leur validité : comment


peuvent-ils garantir le comportement d'ensemble de
l'application ?
⬥ En fait, l'intérêt des tests unitaires n'est pas là
⬥ Ce rôle est pris en charge par d'autres types de tests,
appelés tests fonctionnels ou tests d’intégration, qui
valident chaque fonctionnalité de l'application dans
son ensemble et permettent ainsi de mesurer
l'avancement du projet
Les tests d’intégration

⬥ Les tests d’intégration sont un ensemble de scénarios qui


vérifient la validité totale de l’application
⬥ L’ensemble des scénarios construits doivent passer par toutes
les chemins possibles d’exécution du code
⬥ Leur rôle est également de tester la communication entre les
différents composants
⬥ Les tests d’intégration donnent lieu à des traces qui prouvent
la validité de l’application par rapport au cahier des charges
établit avec le client
Les tests

unitaires versus intégration

⬥ Le tableau suivant précise les différences entre ces


deux types de tests :

Tests unitaires Tests d’intégration

Ecrits par... Les développeurs Le client ou son représentant

Portent sur... Des méthodes unitaires L'ensemble de l'application

Approche... Boîte blanche Boîte noire

Concernent le
Non Oui
client
Les tests

Les tests unitaires

⬥ Les tests d’intégration sont clairement plus intéressants dans


l'absolu, mais ils sont généralement assez coûteux à mettre en
œuvre
⬥ A l'inverse, les tests unitaires sont beaucoup plus légers :
■ La mise en place du contexte de test et la vérification du résultat sont le
plus souvent très simples, et le coût d'écriture des tests est très réduit
■ Ils peuvent être lancés à chaque compilation du module (les tests
d’intégration ne peuvent être lancés que lorsque toute l'application est
compilée)
■ Ils peuvent être exécutés très tôt dans le développement d'une tâche
(les tests d’intégration ne peuvent être lancés que lorsque la
fonctionnalité complète est implémentée)
Les tests unitaires

principes

⬥ Les tests unitaires reprennent toujours la même


structure :
■ Mise en place d'une situation
■ Puis liste d'assertions pour vérifier le comportement du
code à tester
■ Ces assertions sont ensuite exécutées et leur résultat est
comparé à un résultat attendu
Les tests unitaires

principes

⬥ Les tests unitaires se font donc par fonction


en testant :
■ un cas nominal
■ les cas aux limites

■ les cas d’erreur

⬥ Pour chaque test, le résultat est comparé à un


résultat attendu
Les tests unitaires

principes

⬥ Une fois écrite une première version, nous


compilons l'ensemble puis exécutons les tests
⬥ Nous corrigeons éventuellement le code jusqu'à ce
que tous les tests passent
⬥ Puis nous passons à la fonctionnalité suivante en
adoptant la même démarche
Les tests unitaires

intérêts

⬥ Au premier abord, on considère en général les tests


unitaires comme un investissement pour l'avenir, un
peu comme si l'on acceptait un mal immédiat pour
un bien futur
⬥ Mais avec un peu de pratique, on se rend compte
que de nombreux bénéfices des tests unitaires sont
assez immédiats
Les tests unitaires

intérêts

⬥ Les tests unitaires apportent un feedback beaucoup


plus rapide que les tests d’intégration, pour un coût
nettement plus réduit
⬥ De plus, ils apportent un sentiment d'avancement
concret
⬥ Le fait d'avoir plusieurs fois par jour l'ensemble des
tests unitaires qui passe à 100% donne confiance
dans le code et permet d'avancer rapidement
Les tests unitaires

intérêts

⬥ Les avantages à long terme ne sont pas en reste pour autant


⬥ En effet, les tests unitaires forment avec le temps une batterie
complète de tests de non-régression qui permet le
remaniement serein du code - et donc une amélioration
continue de l'application
⬥ Ce rôle de détection des régressions peut d'ailleurs être mis à
profit en s'efforçant d'écrire un test unitaire pour chaque
défaut trouvé dans l'application
⬥ Ainsi, un défaut corrigé ne pourra plus jamais réapparaître
sans être détecté
Les tests unitaires

intérêts

⬥ Enfin, les tests unitaires jouent également le rôle de


documentation des services d'un module, une sorte
de mode d'emploi qui facilite l'exploration du code
par d'autres développeurs
⬥ De plus, ils amènent les développeurs à prendre
conscience des dépendances des modules et des
fonctions qu'ils écrivent
Tactiques de tests

⬥ Dans le cas de fonctions très simples, aux entrées et sorties bien


définies, la mise en place de tests est triviale
⬥ Cela ne reflète cependant pas la pratique dans la mesure où les
fonctions les plus intéressantes à tester dépendent souvent d'autres
fonctions ou possèdent un état interne difficile à contrôler
⬥ D'une manière générale, on peut même s'attendre à ce qu'une
application développée sans tests unitaires soit assez difficile à
tester unitairement par la suite
⬥ En particulier les dépendances entre fonctions ont une influence
assez forte sur la facilité de mise en oeuvre des tests
Tactiques de tests

⬥ Il existe différentes tactiques pour aborder certaines


problématiques récurrentes de l'écriture des tests
unitaires :
■ Sélection du code à tester pour optimiser le "retour sur
investissement" des tests
■ Utilisation de bouchons pour s'abstraire des dépendances
vers d'autres fonctions (Self-shunt, Mock Object)
■ Extraction de code testable pour s'abstraire de
dépendances gênantes sans avoir recours aux bouchons de
test
Tactiques de tests

sélection du code

⬥ L'une des premières questions qui se pose lorsque l'on met


ces tests en pratique est : "Que doit-on tester ?"
⬥ Tout d'abord, il est important de noter que l'on ne cherche pas
systématiquement à tester unitairement chacune des
fonctions de chacun des modules du système
⬥ Par exemple, on ne testera pas individuellement des
accesseurs ou des méthodes d'une ou deux lignes qui
consistent en de simples appels à des outils tiers éprouvés
Tactiques de tests

sélection du code

⬥ Ce que l'on cherche à tester avant tout, ce sont les services


rendus par un module
⬥ Ainsi, si certaines méthodes seront suffisamment complexes
pour mériter des tests spécifiques, de nombreux tests auront
pour but de mettre en oeuvre plusieurs méthodes d'un même
module pour vérifier leur cohérence d'ensemble
⬥ Attention toutefois à utiliser cette tactique avec discernement
: plus le nombre de fonctions mises en jeu augmente, moins
les tests sont "unitaires", et donc plus le temps passé à
investiguer d'éventuels problèmes augmente
Tactiques de tests

utilisation de bouchons

⬥ L'écriture de tests unitaires consiste donc à isoler une


fonction ou un ensemble de fonctions afin d'en valider le bon
fonctionnement
⬥ Or, les fonctions complètement indépendantes du reste du
système sont bien rares dans une application
⬥ Il est donc extrêmement fréquent qu'au moment de tester une
fonction donnée on soit amené à mettre en place un contexte
de test qui fasse intervenir un certain nombre d'autres
fonctions de l'application
Tactiques de tests

utilisation de bouchons

⬥ Cela n'est pas gênant en soi, mais plus le nombre de


fonctions ainsi mises en jeu augmente, plus le test devient
lourd à élaborer
⬥ Lorsque la gêne devient trop grande, cela constitue
fréquemment une indication que l'application est en train de
devenir trop complexe, et qu'il faut y remédier en rompant
quelques dépendances
⬥ Outre le nombre, il arrive également que ce soit la nature de
ces dépendances qui amène à envisager leur rupture
Tactiques de tests

utilisation de bouchons

⬥ la rupture d'une dépendance passe en général par l'introduction d'un


module d'interface entre un module donné et le module dont il dépend.
Ainsi :

⬥ devient :
Tactiques de tests

utilisation de bouchons

⬥ Au moment d'écrire le test des fonctions du


module A, il nous faut donc trouver un substitut
au module B utilisé réellement dans le système,
c'est-à-dire un bouchon
⬥ Un bouchon est une fonction qui respecte le
prototype de la fonction réellement utilisée mais
qui se contente de retourner la valeur attendue
Tactiques de tests

utilisation de bouchons

⬥ Dans la technique du self-shunt, c'est la fonction


de test elle même qui joue le rôle de bouchon
⬥ Cette technique est très simple à mettre en oeuvre,
malheureusement, elle peut se montrer mal adaptée
dans certains cas :
■ Lorsque la fonction testée fait appel plusieurs fois à la
même fonction avec des valeurs de paramètres
différents
■ Lorsqu'une autre fonction de test nécessite l'écriture du
même bouchon
Tactiques de tests

utilisation de bouchons

⬥ Dans tous ces cas, l'alternative du Mock object


peut se montrer préférable
⬥ Il s’agit simplement d’écrire une nouvelle
fonction spécifique pour jouer le rôle du bouchon
Tactiques de tests

extraction de code testable

⬥ La technique du bouchonnage est très efficace, mais elle est


également assez coûteuse : il n'est clairement pas pensable
de bouchonner chacune des fonctions de l'application !
⬥ Cela ne signifie pas pour autant que l'on doive se priver de
certains tests à cause de dépendances indésirables
⬥ La solution peut consister à extraire des portions
"sensibles" du code d'une fonction dans des méthodes
spécifiques, qui seront testées indépendamment des
dépendances mises en jeu dans le fonctionnement standard
du module
Automatisation des tests

CppUnit

⬥ cppUnit est l'équivalent C++ de l'outil JUnit créé entre


autres par Kent Beck
⬥ Plateforme de test unitaire, cppUnit sert à organiser ses tests
unitaires.
⬥ Un tel test permet de vérifier le bon fonctionnement de son
code
⬥ Idéalement, le test devrait être écrit avant la fonction à tester
⬥ De plus, une application doit être testée sous toutes ses
coutures, chaque fonction, chaque module ayant un ou
plusieurs tests associés.
Automatisation des tests

CppUnit
CppUnit

La classe Test

⬥ Cette classe est la classe principale du framework.


⬥ Classe virtuelle pure, elle définit entre autres la fonction
runTest() qui est naturellement appelée lors de l'exécution
des tests.
⬥ C'est elle qui sera surchargée par nos tests unitaires.
⬥ Les autres fonctions membres servent à diverses tâches tel
le comptage du nombre de tests, récupérer les résultats des
tests à travers le pattern Visitor, …
CppUnit

La classe TestCase

⬥ Classe la plus élémentaire, elle est dérivée de Test au


travers de TestLeaf.
⬥ Elle peut être considérée comme une feuille de l'arbre des
tests.
CppUnit

La classe TestSuite

⬥ Cette classe implémente le pattern Composite, à savoir


une arborescence dans les tests.
⬥ C'est le moyen de hiérarchiser facilement et efficacement
ses tests.
CppUnit

La classe TestDecorator

⬥ Cette classe est virtuelle mais a 2 filles.


⬥ Dans certains cas, les tests envisagés nécessitent de mettre
en place un cadre, charger des données, mais aussi de
"nettoyer" après les tests.
⬥ 2 fonctions supplémentaires sont à surcharger pour
accomplir cette dernière tâche dans la classe TestSetUp.
⬥ Certains tests doivent être répétés plusieurs fois, ce que la
classe RepeatedTest permet d'accomplir.
CppUnit

Exemple simple

class PseudoTest : public CppUnit::TestCase


{
public: PseudoTest(std::string name) : CppUnit::TestCase(name) {}

void runTest() {
CPPUNIT_ASSERT( 1 == 1 );
CPPUNIT_ASSERT( !(1 == 2) );
}
};
CppUnit

Exemple simple

class ComplexNumberTest : public CppUnit::TestCase {


public:
ComplexNumberTest( std::string name ) : CppUnit::TestCase( name )
{}

void runTest() {
CPPUNIT_ASSERT( Complex (10, 1) == Complex (10, 1) );
CPPUNIT_ASSERT( !(Complex (1, 1) == Complex (2, 2)) );
}
};
CppUnit

tests plus élaborés

⬥ On va maintenant essayer d'aller un peu plus loin


en utilisant une autre classe dérivant de Test,
TestFixture.
⬥ Cette classe propose simplement de gérer
plusieurs fonctions de test.
⬥ Elle permet également de stocker des éléments
pour les utiliser d’un test à l’autre.
CppUnit

TestFixture (.hpp)

class ComplexNumberTest : public CppUnit::TestFixture {

private:
Complex *m_10_1, *m_1_1, *m_11_2;

public:
void setUp();
void tearDown();

void testEquality();
void testAddition();

};
CppUnit

TestFixture (.cpp)

void ComplexNumberTest::setUp() {
m_10_1 = new Complex( 10, 1 );
m_1_1 = new Complex( 1, 1 );
m_11_2 = new Complex( 11, 2 );
}

void ComplexNumberTest::tearDown() {
delete m_10_1;
delete m_1_1;
delete m_11_2;
}

void ComplexNumberTest::testEquality() {
CPPUNIT_ASSERT( *m_10_1 == *m_10_1 );
CPPUNIT_ASSERT( !(*m_10_1 == *m_11_2) );
}

void ComplexNumberTest::testAddition() {
CPPUNIT_ASSERT( *m_10_1 + *m_1_1 == *m_11_2 );
}
CppUnit

Lancement de tests

⬥ On peut alors créer et lancer des instances pour


chaque test :
CppUnit::TestCaller<ComplexNumberTest> test("testEquality",
&ComplexNumberTest::testEquality);

CppUnit::TestResult result;

test.run( &result );
CppUnit

TestSuites

⬥ Pour créer une suite de un ou plusieurs tests :


CppUnit::TestSuite suite;
CppUnit::TestResult result;

suite.addTest( new CppUnit::TestCaller<ComplexNumberTest>(


"testEquality", &ComplexNumberTest::testEquality ) );

suite.addTest( new CppUnit::TestCaller<ComplexNumberTest>(


"testAddition", &ComplexNumberTest::testAddition ) );

suite.run( &result );
CppUnit

TestRunner

⬥ CppUnit fournit les outils pour définir la suite à lancer et afficher


les résultats
⬥ On ajoute à la classe de test (ComplexNumberTest) une méthode
statique publique qui retourne une suite de tests.
⬥ Cette méthode sera utilisée par TestRunner
public: static CppUnit::Test *suite() {
CppUnit::TestSuite *suiteOfTests = new CppUnit::TestSuite( "ComplexNumberTest" );

suiteOfTests->addTest( new CppUnit::TestCaller<ComplexNumberTest>("testEquality",


&ComplexNumberTest::testEquality ) );
suiteOfTests->addTest( new CppUnit::TestCaller<ComplexNumberTest>("testAddition",
&ComplexNumberTest::testAddition ) );

return suiteOfTests;
}
CppUnit

TestRunner

⬥ Il ne reste plus qu’à créer un programme principal de test


qui fera une suite de addTest(CppUnit::Test *)
#include <cppunit/ui/text/TestRunner.h>
#include "ComplexNumberTest.h"

int main( int argc, char **argv) {

CppUnit::TextUi::TestRunner runner;

runner.addTest( ExampleTestCase::suite() );
runner.addTest( ComplexNumberTest::suite() );
runner.run();
return 0;
}
CppUnit

Utilisation des macros

⬥ L’implémentation de la méthode statique est répétitive et


donc sujette à erreurs.
⬥ Pour faciliter son écriture, des macros existent dans CppUnit.
#include <cppunit/extensions/HelperMacros.h>

class ComplexNumberTest : public CppUnit::TestFixture {

CPPUNIT_TEST_SUITE( ComplexNumberTest );
CPPUNIT_TEST( testEquality );
CPPUNIT_TEST( testAddition );
CPPUNIT_TEST_SUITE_END();

}
CppUnit

Utilisation des macros

⬥ Les macros permettent également de vérifier la validité des


exceptions.
⬥ Par exemple, pour vérifier que ComplexNumber lève bien
l’exception division par zéro :
CPPUNIT_TEST_SUITE( ComplexNumberTest );
// [...]
CPPUNIT_TEST_EXCEPTION(testDivideByZeroThrows, MathException );
CPPUNIT_TEST_SUITE_END();
// [...]
void testDivideByZeroThrows()
{
// The following line should throw a MathException.
*m_10_1 / ComplexNumber(0);
}
CppUnit


http://cppunit.sourceforge.net/doc/1.11.6/cppunit_cookbook.html

Vous aimerez peut-être aussi