Voir aussi LangageCppMinUnit pour un exemple en C++ avec le même environnement de tests.
Le pilotage par les tests suppose des séquences très courtes :
Ce cycle devrait prendre moins d'une heure et est repris N fois dans la journée. Au fur et à mesure, les tests deviennent des tests de non-regression et très rapidement le développeur dispose d'une batterie conséquente de tests automatisés. L'autre avantage est le feedback rapide, l'un des principes majeurs de l'Extreme Programming.
Voir aussi: PilotageParLesTestsEtConception et PilotageParLesTestsEtMiseAuPoint
-tc-
Cette formation de mars 2004 a plusieurs objectifs :
25.3.2004 Nous attaquons dans le vif du sujet : création de premiers modules et mise en oeuvre des macros mu_assert et mu_test_run pour les tests unitaires...
Un TP proposé est le calcul de l'impot, sujet d'actualité... Les tranches par rapport au revenu sont les suivantes :
Le système de test proposé repose sur 3 niveaux, un peu comme xUnit mais en beaucoup plus simple:
La macro mu_assert(message, test) reçoit 2 paramètres:
Voici les sources à l'issue de cette journée (le plus simple si vous lisez un peu le C est de consulter directement ces sources pour comprendre le fonctionnement des tests unitaires) :
Note: SourceTestImpot définit plusieurs tests. C'est le résultat à la fin de la journée. Nous avons commencé par écrire uniquement les 2 premiers tests (impot à 0), puis la fonction qui permettait de faire passer ces 2 tests. Ensuite nous avons ajouté les tests 1 à 1 soit tranche par tranche.
Une modification consiste à afficher un "." à chaque "assert" pour visualiser l'avancement des tests:
define mu_assert(message, test) do {printf("."); fflush(stdout); if (!(test)) return message; } while (0)
Les sources dans une version plus évoluée (voir aussi plus bas):
tc
Nous travaillons sur les allocations mémoire et les tableaux. L'appli doit gérer des réductions d'impot. Voici le nouveau test unitaire:
static char * testImpotReduction() {
nouvelleReduction(500); nouvelleReduction(500);
mu_assert("nombre de reductions incorrect", nombreReduction() == 2);
mu_assert("total des reductions incorrect", totalReduction() == 1000);
return 0;
}
Il s'agit de verifier la bonne gestion des reductions avant de voir ce que donne le nouveau calcul qui doit en tenir compte. La conception de cette nouvelle spéc fait apparaitre une fonction: nouvelleReduction(). Afin de tester cette fonction, nous en créons 2 autres : nombreReduction() et totalReduction(). Ces 2 fonctions ne seront peut-être pas utilisées dans l'appli elle-même mais l'objectif est la testabilité.
Le code associé dans impot.c est ensuite ajouté:
static int reduction[100]; static int nreduction = 0;
// le wiki reconnait les crochets, ils sont remplacés par des chevrons.
void nouvelleReduction(int montant) {
reduction <nreduction> = montant; nreduction ++;
}
int nombreReduction(void) {
return nreduction;
}
int totalReduction(void) {
int i = 0, cumul = 0;
for (i = 0; i < nreduction; i++) cumul += reduction <i>;
return cumul;
}
Le nouveau test est:
static char * testImpotReduction() {
nouvelleReduction(500); nouvelleReduction(500);
mu_assert("nombre de reductions incorrect", nombreReduction() == 2);
mu_assert("total des reductions incorrect", totalReduction() == 1000);
// Test de l'impot: 25000 avec 500 + 500 de réduction: 1000 de la 1ere tranche,
// la 2eme tranche est de 5000 -500 -500 soit 4000 à 20% --> 800,
// total impot: 1000 + 800
mu_assert("Impot avec reduction incorrect", calculImpot(25000) == 1800);
return 0;
}
La suite de tests est alors:
static char * all_tests() {
mu_run_test(testImpot0); mu_run_test(testImpot10000); mu_run_test(testImpot15000); mu_run_test(testImpot25000); mu_run_test(testImpot60000); mu_run_test(testImpot100000); mu_run_test(testImpotBoucle); mu_run_test(testImpotTrancheSup); mu_run_test(testImpotReduction);
return 0;
}
Et le nouveau calcul dans impot.c devient:
int calculImpot(int revenu) {
int impot = 0; int revenu_apres_reduction = 0;
revenu_apres_reduction = revenu - totalReduction();
if (revenu_apres_reduction <= 10000) impot = 0;
if ((revenu_apres_reduction >10000) && (revenu_apres_reduction) <= 20000) impot = (revenu_apres_reduction-10000) * 0.1;
if ((revenu_apres_reduction >20000) && (revenu_apres_reduction <= 60000)) impot = 1000 + (revenu_apres_reduction-20000) * 0.2;
if (revenu_apres_reduction >60000) impot = 1000 + 8000 + ((revenu_apres_reduction-60000) * 30 /100 ) ;
//printf("revenu: %d\t %10d\n", revenu, impot);
return impot;
}
Il est temps de s'interesser aux pointeurs. Une première implémentation consiste à créer une fonction qui renvoie les infos "reduction" sous forme de paramètre de retour. Dans un premier temps, le test est :
{ // test de la fonction avec pointeurs
int n = 0, reduc=0;
infosReduction(&n, &reduc);
mu_assert("Fonction avec pointeurs : incorrect", (n == 2) && (reduc == 1000));
}
Ce test est inséré dans la fonction: testImpotReduction().
La fonction infosReduction() est ensuite développée:
void infosReduction(int *nombre, int *reductions) {
ETOILE nombre = nreduction; ETOILE reductions = totalReduction();
}
Nouveaux cas de tests sur des types de calculs de réduction (là encore, les trois tests ne sont pas écrits simultanément, le premier est écrit, le code correspondant est ensuite développé etc).
static char * testCalculReduction() {
mu_assert("calcul reductions type 1 incorrect", calculReduction(1, 1000) == 100);
mu_assert("calcul reductions type 2 incorrect", calculReduction(2, 1000) == 200);
mu_assert("calcul reductions type 3 incorrect", calculReduction(3, 1000) == 2000);
return 0;
}
Et le code de la fonction est:
int calculReduction(int type_reduction, int montant) {
int reduction = 0;
switch(type_reduction)
{
case 1:
reduction = montant / 10;
break;
case 2:
reduction = montant / 5; // 20%
break;
case 3:
reduction = montant * 2;
break;
default:
fprintf(stderr,
"int calculReduction(int type_reduction, int montant) Erreur type");
}
return reduction;
}
L'étape suivante consiste à traiter les cas d'erreur, toujours sous forme de test d'abord: par ex invoquer la fonction avec type reduc = 4 etc.
Création d'un type structuré pour gérer les réductions, ce qui impose une mise à jour des prototypes de fonctions du module.
Une fonction Init() est ensuite créée pour pouvoir enchainer des tests sans effet de bord et être sûr de démarrer d'un état connu.
const int MAX = 5;
static char * testImpotReductionDifferentesTranches() {
int i = 0;
struct s_reduction tab<MAX> = {
{ 1, 5000},
{ 2, 2500},
{ 2, 2500},
{ 2, 2500},
{ 3, 250}
};
Init();
for (i = 0; i<MAX; i++) nouvelleReduction(&tab<i>);
{
int n = 0, total=0;
infosReduction(&n, &total);
mu_assert("Infos reductions incorrect", (n == MAX) && (total == 2500) );
}
mu_assert("Impot avec reduction T0 incorrect", calculImpot(500) == 0);
mu_assert("Impot avec reduction T1 incorrect", calculImpot(18000) == 550);
// 50000 avec 2500 reduc --> 47500 soit 1000 de T1 + 20% de 27500 soit
mu_assert("Impot avec reduction T2 incorrect", calculImpot(50000) == 6500);
return 0;
}
---
Chaînes de caractères
Base de Données et tests unitaires
Nous utilisons pour terminer une base de données. Ce qui se traduit par deux aspects: