Effectuer des tests unitaires à l'aide de GoogleTest et CTest
L'utilisation de tests unitaires améliorera probablement la qualité de votre code et ce, sans perturber votre flux de travail.
Cet article fait suite à mon dernier article Configurer un système de build avec CMake et VSCodium.
Dans le dernier article, j'ai montré comment configurer un système de build basé sur VSCodium et CMake. Cet article affine cette configuration en intégrant des tests unitaires significatifs à l'aide de GoogleTest et CTest.
Si ce n'est pas déjà fait, clonez le référentiel, ouvrez-le dans VSCodium et extrayez la balise devops_2 en cliquant sur le symbole de branche main (marqueur rouge) et en choisissant la branche (jaune). marqueur):
Stephan Avenwedde (CC BY-SA 4.0)
Alternativement, ouvrez la ligne de commande et tapez :
$ git checkout tags/devops_2
GoogleTest
GoogleTest est un framework de test C++ open source indépendant de la plate-forme. Même si GoogleTest n'est pas destiné exclusivement aux tests unitaires, je vais l'utiliser pour définir des tests unitaires pour la bibliothèque Generator. En général, un test unitaire doit vérifier le comportement d’une seule unité logique. La bibliothèque Generator est une unité, je vais donc écrire quelques tests significatifs pour garantir son bon fonctionnement.
À l'aide de GoogleTest, les cas de test sont définis par des macros d'assertions. Le traitement d'une assertion génère l'un des résultats suivants :
- Succès : test réussi.
- Échec non fatal : le test a échoué, mais la fonction de test continuera.
- Échec fatal : le test a échoué et la fonction de test sera abandonnée.
Les macros d'assertions suivent ce schéma pour distinguer un échec fatal d'un échec non fatal :
ASSERT_*
échec fatal, la fonction est abandonnée.EXPECT_*
échec non fatal, la fonction n'est pas abandonnée.
Google recommande d'utiliser les macros EXPECT_*
car elles permettent au test de continuer lorsque les tests définissent plusieurs assertions. Une macro d'assertion prend deux arguments : le premier argument est le nom du groupe de tests (une chaîne librement sélectionnable) et le deuxième argument est le nom du test lui-même. La bibliothèque Generator définit simplement la fonction generate(...), donc les tests de cet article appartiennent au même groupe : GeneratorTest.
Les tests unitaires suivants pour la fonction generate(...) peuvent être trouvés dans GeneratorTest.cpp.
Vérification des références
La fonction generate(...) prend une référence à un std::stringstream comme argument et renvoie la même référence. Le premier test consiste donc à vérifier si la référence transmise est la même référence que celle renvoyée par la fonction.
TEST(GeneratorTest, ReferenceCheck){
const int NumberOfElements = 10;
std::stringstream buffer;
EXPECT_EQ(
std::addressof(buffer),
std::addressof(Generator::generate(buffer, NumberOfElements))
);
}
Ici, j'utilise std::addressof pour vérifier si l'adresse de l'objet renvoyé fait référence au même objet que j'ai fourni en entrée.
Nombre d'éléments
Ce test vérifie si le nombre d'éléments dans la référence stringstream correspond au nombre donné en argument.
TEST(GeneratorTest, NumberOfElements){
const int NumberOfElements = 50;
int nCalcNoElements = 0;
std::stringstream buffer;
Generator::generate(buffer, NumberOfElements);
std::string s_no;
while(std::getline(buffer, s_no, ' ')) {
nCalcNoElements++;
}
EXPECT_EQ(nCalcNoElements, NumberOfElements);
}
Mélanger
Ce test vérifie le bon fonctionnement du moteur aléatoire. Si j'invoque la fonction generate deux fois de suite, je m'attends à ne pas obtenir le même résultat.
TEST(GeneratorTest, Shuffle){
const int NumberOfElements = 50;
std::stringstream buffer_A;
std::stringstream buffer_B;
Generator::generate(buffer_A, NumberOfElements);
Generator::generate(buffer_B, NumberOfElements);
EXPECT_NE(buffer_A.str(), buffer_B.str());
}
Somme de contrôle
C'est le plus grand test. Il vérifie si la somme des chiffres d'une série numérique de 1 à n est la même que la somme de la série de sortie mélangée. Je m'attends à ce que la somme corresponde à la fonction generate(...) qui devrait simplement créer une variante mélangée d'une telle série.
TEST(GeneratorTest, CheckSum){
const int NumberOfElements = 50;
int nChecksum_in = 0;
int nChecksum_out = 0;
std::vector<int> vNumbersRef(NumberOfElements); // Input vector
std::iota(vNumbersRef.begin(), vNumbersRef.end(), 1); // Populate vector
// Calculate reference checksum
for(const int n : vNumbersRef){
nChecksum_in += n;
}
std::stringstream buffer;
Generator::generate(buffer, NumberOfElements);
std::vector<int> vNumbersGen; // Output vector
std::string s_no;
// Read the buffer back back to the output vector
while(std::getline(buffer, s_no, ' ')) {
vNumbersGen.push_back(std::stoi(s_no));
}
// Calculate output checksum
for(const int n : vNumbersGen){
nChecksum_out += n;
}
EXPECT_EQ(nChecksum_in, nChecksum_out);
}
Les tests ci-dessus peuvent également être débogués comme une application C++ ordinaire.
CTest
En plus du test unitaire in-code, l'utilitaire CTest me permet de définir des tests pouvant être effectués sur les exécutables. En un mot, j'appelle l'exécutable avec certains arguments et je fais correspondre la sortie avec des expressions régulières. Cela me permet simplement de vérifier comment l'exécutable se comporte avec des arguments de ligne de commande incorrects. Les tests sont définis dans le fichier CMakeLists.txt de niveau supérieur. Voici un examen plus approfondi de trois cas de test :
Utilisation régulière
Si un entier positif est fourni comme argument de ligne de commande, j'attends que l'exécutable produise une série de nombres séparés par des espaces :
add_test(NAME RegularUsage COMMAND Producer 10)
set_tests_properties(RegularUsage
PROPERTIES PASS_REGULAR_EXPRESSION "^[0-9 ]+"
)
Aucun argument
Si aucun argument n'est fourni, le programme doit se terminer immédiatement et afficher la raison pour laquelle :
add_test(NAME NoArg COMMAND Producer)
set_tests_properties(NoArg
PROPERTIES PASS_REGULAR_EXPRESSION "^Enter the number of elements as argument"
)
Mauvais argument
Fournir un argument qui ne peut pas être converti en entier devrait également provoquer une sortie immédiate avec un message d'erreur. Ce test appelle l'exécutable Producer avec le paramètre de ligne de commande "ABC" :
add_test(NAME WrongArg COMMAND Producer ABC)
set_tests_properties(WrongArg
PROPERTIES PASS_REGULAR_EXPRESSION "^Error: Cannot parse"
)
Tester les tests
Pour exécuter un seul test et voir comment il est traité, appelez ctest
à partir de la ligne de commande en fournissant les arguments suivants :
- Exécuter un seul test :
-R
- Activer la sortie détaillée :
-VV
Voici la commande ctest -R Usage -VV :
$ ctest -R Usage -VV
UpdatecTest Configuration from :/home/stephan/Documents/cpp_testing sample/build/DartConfiguration.tcl
UpdateCTestConfiguration from :/home/stephan/Documents/cpp_testing sample/build/DartConfiguration.tcl
Test project /home/stephan/Documents/cpp_testing sample/build
Constructing a list of tests
Done constructing a list of tests
Updating test list for fixtures
Added 0 tests to meet fixture requirements
Checking test dependency graph...
Checking test dependency graph end
Dans ce bloc de code, j'ai invoqué un test nommé Usage.
Cela a exécuté l'exécutable sans arguments de ligne de commande :
test 3
Start 3: Usage
3: Test command: /home/stephan/Documents/cpp testing sample/build/Producer
Le test a échoué car le résultat ne correspondait pas à l'expression régulière [^[0-9]+]
.
3: Enter the number of elements as argument
1/1 test #3. Usage ................
Failed Required regular expression not found.
Regex=[^[0-9]+]
0.00 sec round.
0% tests passed, 1 tests failed out of 1
Total Test time (real) =
0.00 sec
The following tests FAILED:
3 - Usage (Failed)
Errors while running CTest
$
Pour exécuter tous les tests (y compris celui défini avec GoogleTest), accédez au répertoire build et exécutez ctest
:
Stephan Avenwedde (CC BY-SA 4.0)
Dans VSCodium, cliquez sur la zone marquée en jaune dans la barre d'informations pour appeler CTest. Si tous les tests réussissent, le résultat suivant s'affiche :
Stephan Avenwedde (CC BY-SA 4.0)
Automatisez les tests avec Git Hooks
Désormais, exécuter les tests est une étape supplémentaire pour le développeur. Le développeur peut également valider et diffuser du code qui ne réussit pas les tests. Grâce à Git Hooks, je peux implémenter un mécanisme qui exécute automatiquement les tests et empêche le développeur de commettre accidentellement du code défectueux.
Accédez à .git/hooks
, créez un fichier vide nommé pre-commit, puis copiez et collez le code suivant :
#!/usr/bin/sh
(cd build; ctest --output-on-failure -j6)
Après cela, rendez ce fichier exécutable :
$ chmod +x pre-commit
Ce script appelle CTest lors d'une tentative d'exécution d'une validation. Si un test échoue, comme dans la capture d'écran ci-dessous, la validation est annulée :
Stephan Avenwedde (CC BY-SA 4.0)
Si les tests réussissent, la validation est traitée et le résultat ressemble à ceci :
Stephan Avenwedde (CC BY-SA 4.0)
Le mécanisme décrit n'est qu'une barrière souple : un développeur peut toujours valider du code défectueux en utilisant git commit --no-verify
. Je peux m'assurer que seul le code fonctionnel est transmis en configurant un serveur de build. Ce sujet fera partie d'un article séparé.
Résumé
Les techniques mentionnées dans cet article sont faciles à mettre en œuvre et vous aident à trouver rapidement les bugs dans votre code. L'utilisation de tests unitaires améliorera probablement la qualité de votre code et, comme je l'ai montré, sans perturber votre flux de travail. Le framework GoogleTest fournit des fonctionnalités pour tous les scénarios imaginables ; Je n'ai utilisé qu'un sous-ensemble de ses fonctionnalités. À ce stade, je souhaite également mentionner le GoogleTest Primer, qui vous donne un aperçu des idées, des opportunités et des fonctionnalités du framework.