Effectuer des tests unitaires à l’aide de GoogleTest et CTest

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 construction 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 déjà fait, clonez le dépôt, ouvrez-le dans VSCodium et vérifiez le tag devops_2 en cliquant sur le principale-symbole de la branche (marqueur rouge) et choix de la branche (marqueur jaune) :
Sinon, ouvrez la ligne de commande et tapez :
$ git checkout tags/devops_2
Contents
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 les tests unitaires pour le Générateur bibliothèque. En général, un test unitaire doit vérifier le comportement d’une seule unité logique. le Générateur la bibliothèque est une unité, donc je vais écrire quelques tests significatifs pour assurer le bon fonctionnement.
Avec 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 une défaillance fatale d’une défaillance non fatale :
ASSERT_*
défaillance fatale, la fonction est annulée.EXPECT_*
échec non fatal, la fonction n’est pas interrompue.
Google recommande d’utiliser EXPECT_*
macros 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 test (une chaîne librement sélectionnable) et le second argument est le nom du test lui-même. le Générateur bibliothèque définit juste la fonction produire(…), donc les tests de cet article appartiennent au même groupe : GénérateurTest.
Les tests unitaires suivants pour le produire(…) fonction se trouve dans GeneratorTest.cpp.
Vérification des références
le produire(…) fonction prend une référence à un std ::stringstream en argument et renvoie la même référence. Le premier test consiste donc à vérifier si la référence passée 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 :: adressede 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 le produire fonction 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 le même que la somme de la série de sortie mélangée. Je m’attends à ce que la somme corresponde à la produire(…) La fonction 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 dans le code, le CTest L’utilitaire me permet de définir des tests pouvant être effectués sur des exécutables. En un mot, j’appelle l’exécutable avec certains arguments et fais correspondre la sortie avec expressions régulières. Cela me permet simplement de vérifier le comportement de l’exécutable avec des arguments de ligne de commande incorrects. Les tests sont définis au niveau supérieur CMakeLists.txt. 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, je m’attends à ce 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 ]+"
)
Pas de dispute
Si aucun argument n’est fourni, le programme doit se fermer immédiatement et afficher la raison :
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 fait appel à la Producteur exécutable 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 épreuves
Pour exécuter un seul test et voir comment il est traité, appelez ctest
depuis la ligne de commande en fournissant les arguments suivants :
- Exécutez un seul tst :
-R <test-name>
- 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 appelé 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 la sortie ne correspond 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 construire répertoire et exécuter ctest
:
Dans VSCodium, cliquez sur la zone marquée en jaune dans la barre d’informations pour appeler CTest. Si tous les tests réussissent, la sortie suivante s’affiche :
Automatisez les tests avec Git Hooks
Désormais, l’exécution des tests est une étape supplémentaire pour le développeur. Le développeur peut également valider et envoyer du code qui ne réussit pas les tests. Grâce à Crochets Git, 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.
Aller vers .git/hooks
, créez un fichier vide nommé pré-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 invoque CTest lors de la tentative d’exécution d’un commit. Si un test échoue, comme dans la capture d’écran ci-dessous, le commit est abandonné :
Si les tests réussissent, le commit est traité et le résultat ressemble à ceci :
Le mécanisme décrit n’est qu’une barrière souple : un développeur peut toujours commettre un code défectueux en utilisant git commit --no-verify
. Je peux m’assurer que seul le code fonctionnel est poussé 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 des bogues dans votre code. L’utilisation de tests unitaires améliorera probablement la qualité de votre code et, comme je l’ai montré, le fera 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 veux également mentionner le Introduction à GoogleTest, qui vous donne un aperçu des idées, des opportunités et des fonctionnalités du framework.