La lisibilité et la maintenabilité du code de test sont aussi cruciales que celles du code applicatif. Le code de test agit souvent comme une documentation vivante, illustrant le fonctionnement et l'utilisation des composants. Lorsque les tests deviennent un dédale illisible, les développeurs hésitent à modifier le code existant ou à écrire de nouveaux tests, ce qui érode la confiance dans la base de test.
1. Exemple de code de test problématique
void VerificationTriParScores() { vector<DocumentNote> documents; documents.resize(5); documents[0].adresse = "http://exemple.fr"; documents[0].valeur = -5.0; documents[1].adresse = "http://exemple.fr"; documents[1].valeur = 1; documents[2].adresse = "http://exemple.fr"; documents[2].valeur = 4; documents[3].adresse = "http://exemple.fr"; documents[3].valeur = -99998.7; documents[4].adresse = "http://exemple.fr"; documents[4].valeur = 3.0;
TrierEtFiltrerDocs(&documents);
assert(documents.size() == 3);
assert(documents[0].valeur == 4);
assert(documents[1].valeur == 3.0);
assert(documents[2].valeur == 1);
}
</div>### 2. Masquer les détails d'initialisation
Pour rendre le test plus lisible, encapsulez la création des objets. Cela permet de dissimuler les attributs non pertinents et de mettre en évidence les valeurs significatives.
<div>```
DocumentNote CreerDocumentNote(double score, const string& lien) {
DocumentNote doc;
doc.valeur = score;
doc.adresse = lien;
return doc;
}
void VerificationTriParScores() {
vector<DocumentNote> documents;
documents.push_back(CreerDocumentNote(-5.0, "http://exemple.fr"));
documents.push_back(CreerDocumentNote(1, "http://exemple.fr"));
documents.push_back(CreerDocumentNote(4, "http://exemple.fr"));
documents.push_back(CreerDocumentNote(-99998.7, "http://exemple.fr"));
// ...
}
Une abstraction supérieure permet de réduire encore la verbosité. On se concentre uniquement sur les scores qui caractérisent le scénario du test.
void AjouterDocument(vector<DocumentNote>& collection, double score) { DocumentNote nouveau; nouveau.valeur = score; nouveau.adresse = "http://exemple.fr"; collection.push_back(nouveau); }
void VerificationTriParScores() { vector<DocumentNote> docs; AjouterDocument(docs, -5.0); AjouterDocument(docs, 1); AjouterDocument(docs, 4); AjouterDocument(docs, -99998.7); // ... }
</div>### 4. Exprimer les cas de test de manière concise
Un idéal est de pouvoir exprimer l'intégralité d'un scénario de test en une seule ligne descriptive, décrivant l'entrée et la sortie attendue.
`VerifierTriScores("-5, 1, 4, -99998.7, 3", "4, 3, 1");`### 5. Mettre en œuvre un mini-langage pour les tests
L'utilisation de littéraux de liste peut simplifier la syntaxe dans les versions modernes de C++. Une autre approche consiste à créer des fonctions de parsing pour convertir des chaînes en données de test.
<div>```
vector<DocumentNote> DocumentsDepuisListe(const string& listeScores) {
vector<DocumentNote> collection;
istringstream flux(listeScores);
string element;
while (getline(flux, element, ',')) {
if (!element.empty()) {
try {
double score = stod(element);
AjouterDocument(collection, score);
} catch (const exception& e) {
// Gérer l'erreur de parsing
}
}
}
return collection;
}
string ListeDepuisDocuments(const vector<DocumentNote>& docs) {
ostringstream resultat;
for (size_t i = 0; i < docs.size(); ++i) {
if (i > 0) resultat << ", ";
resultat << docs[i].valeur;
}
return resultat.str();
}
void VerifierTriScores(const string& entree, const string& sortieAttendue) {
vector<DocumentNote> donnees = DocumentsDepuisListe(entree);
TrierEtFiltrerDocs(&donnees);
string resultatObtenu = ListeDepuisDocuments(donnees);
assert(resultatObtenu == sortieAttendue);
}
6. Fournir des messages d'échec clairs
Lorsqu'un test échoue, le message d'erreur doit immédiatement indiquer la cause. Des assertions simples ne sont pas suffisantes.
void VerifierTriScores(const string& entree, const string& attendue) { vector<DocumentNote> donnees = DocumentsDepuisListe(entree); TrierEtFiltrerDocs(&donnees); string obtenue = ListeDepuisDocuments(donnees); if (obtenue != attendue) { cerr << "ÉCHEC du test de tri des scores:" << endl; cerr << " Entrée: "" << entree << """ << endl; cerr << " Sortie attendue: "" << attendue << """ << endl; cerr << " Sortie obtenue: "" << obtenue << """ << endl; abort(); } }
</div>### 7. Choisir des données de test significatives
Sélectionnez le jeu d'entrées le plus simple qui couvre le chemin d'exécution testé. Évitez les valeurs arbitraires ou excessivement complexes.
<div>```
// Couvre les cas de base: tri, filtrage des négatifs, doublons et entrée vide
VerifierTriScores("2, 1, 3", "3, 2, 1");
VerifierTriScores("0, -0.1, -1", "0");
VerifierTriScores("1, -2, 1", "1, 1");
VerifierTriScores("", "");
8. Nommage explicite des fonctions de test
Le nom d'une fonction de test doit décrire ce qu'elle vérifie. Un schéma courant est Tester_NomDeLaFonction_Scenario.
Tester_TrierEtFiltrerDocs_TriDeBase()Tester_TrierEtFiltrerDocs_ValeursNegatives()Tester_TrierEtFiltrerDocs_EntreeVide()
Ces fonctions ne sont généralement pas appelées en dehors du code de test, donc des noms longs et descriptifs sont parfaitement acceptables.
9. Principes pour un code testable
Concevoir du code facilement testable favorise naturellement une architecture propre et modulaire.
À rechercher :
- Minimal ou aucun état interne mutable dans les classes.
- Principe de responsabilité unique pour chaque classe/fonction.
- Faible couplage entre les composants (réduction des dépendances).
- Interfaces de fonctions claires, bien définies et à effet de bord minimal.
À éviter :
- Dégrader la lisibilité du code de production pour des besoins de test.
- Poursuivre une couverture de code de 100% à tout prix.
- Laisser la maintenance des tests ralentir excessivement le développement de nouvelles fonctionnalités.