Tests C++ (Compétence pour Agent)
Workflow de test orienté agent pour le C++ moderne (C++17/20) utilisant GoogleTest/GoogleMock avec CMake/CTest.
Quand utiliser
- Écrire de nouveaux tests C++ ou corriger des tests existants
- Concevoir la couverture de tests unitaires/d'intégration pour des composants C++
- Ajouter de la couverture de test, un gating CI ou une protection contre les régressions
- Configurer des workflows CMake/CTest pour une exécution cohérente
- Enquêter sur des échecs de test ou des comportements instables
- Activer les sanitizers pour le diagnostic mémoire/race
Quand NE PAS utiliser
- Implémenter de nouvelles fonctionnalités produit sans modifications de test
- Refontes à grande échelle non liées à la couverture ou aux échecs de test
- Réglage des performances sans régressions de test à valider
- Projets non C++ ou tâches non liées aux tests
Concepts clés
- Boucle TDD : rouge → vert → refactor (tests d'abord, correction minimale, puis nettoyage).
- Isolation : privilégier l'injection de dépendances et les fakes plutôt que l'état global.
- Disposition des tests :
tests/unit,tests/integration,tests/testdata. - Mocks vs fakes : mock pour les interactions, fake pour le comportement avec état.
- Découverte CTest : utiliser
gtest_discover_tests()pour une découverte stable des tests. - Signal CI : exécuter d'abord un sous-ensemble, puis la suite complète avec
--output-on-failure.
Workflow TDD
Suivez la boucle ROUGE → VERT → REFACTOR :
- ROUGE : écrire un test échouant qui capture le nouveau comportement
- VERT : implémenter le plus petit changement pour réussir
- REFACTOR : nettoyer pendant que les tests restent verts
// tests/add_test.cpp
#include <gtest/gtest.h>
int Add(int a, int b); // Fourni par le code de production.
TEST(AddTest, AddsTwoNumbers) { // ROUGE
EXPECT_EQ(Add(2, 3), 5);
}
// src/add.cpp
int Add(int a, int b) { // VERT
return a + b;
}
// REFACTOR : simplifier/renommer une fois les tests passés
Exemples de code
Test unitaire basique (gtest)
// tests/calculator_test.cpp
#include <gtest/gtest.h>
int Add(int a, int b); // Fourni par le code de production.
TEST(CalculatorTest, AddsTwoNumbers) {
EXPECT_EQ(Add(2, 3), 5);
}
Fixture (gtest)
// tests/user_store_test.cpp
// Stub de pseudocode : remplacer UserStore/User par les types du projet.
#include <gtest/gtest.h>
#include <memory>
#include <optional>
#include <string>
struct User { std::string name; };
class UserStore {
public:
explicit UserStore(std::string /*path*/) {}
void Seed(std::initializer_list<User> /*users*/) {}
std::optional<User> Find(const std::string &/*name*/) { return User{"alice"}; }
};
class UserStoreTest : public ::testing::Test {
protected:
void SetUp() override {
store = std::make_unique<UserStore>(":memory:");
store->Seed({{"alice"}, {"bob"}});
}
std::unique_ptr<UserStore> store;
};
TEST_F(UserStoreTest, FindsExistingUser) {
auto user = store->Find("alice");
ASSERT_TRUE(user.has_value());
EXPECT_EQ(user->name, "alice");
}
Mock (gmock)
// tests/notifier_test.cpp
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <string>
class Notifier {
public:
virtual ~Notifier() = default;
virtual void Send(const std::string &message) = 0;
};
class MockNotifier : public Notifier {
public:
MOCK_METHOD(void, Send, (const std::string &message), (override));
};
class Service {
public:
explicit Service(Notifier ¬ifier) : notifier_(notifier) {}
void Publish(const std::string &message) { notifier_.Send(message); }
private:
Notifier ¬ifier_;
};
TEST(ServiceTest, SendsNotifications) {
MockNotifier notifier;
Service service(notifier);
EXPECT_CALL(notifier, Send("hello")).Times(1);
service.Publish("hello");
}
Démarrage rapide CMake/CTest
# CMakeLists.txt (extrait)
cmake_minimum_required(VERSION 3.20)
project(example LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
# Privilégier les versions verrouillées par projet. Si une balise est utilisée,
# utiliser une version figée conformément à la politique du projet.
set(GTEST_VERSION v1.17.0) # Ajuster à la politique du projet.
FetchContent_Declare(
googletest
# Framework de test Google (dépôt officiel)
URL https://github.com/google/googletest/archive/refs/tags/${GTEST_VERSION}.zip
)
FetchContent_MakeAvailable(googletest)
add_executable(example_tests
tests/calculator_test.cpp
src/calculator.cpp
)
target_link_libraries(example_tests GTest::gtest GTest::gmock GTest::gtest_main)
enable_testing()
include(GoogleTest)
gtest_discover_tests(example_tests)
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j
ctest --test-dir build --output-on-failure
Exécuter les tests
ctest --test-dir build --output-on-failure
ctest --test-dir build -R ClampTest
ctest --test-dir build -R "UserStoreTest.*" --output-on-failure
./build/example_tests --gtest_filter=ClampTest.*
./build/example_tests --gtest_filter=UserStoreTest.FindsExistingUser
Déboguer les échecs
- Réexécuter le seul test qui échoue avec le filtre gtest.
- Ajouter des logs ciblés autour de l'assertion qui échoue.
- Réexécuter avec les sanitizers activés.
- Étendre à la suite complète une fois la cause racine corrigée.
Couverture
Privilégier les paramètres au niveau de la cible plutôt que les drapeaux globaux.
option(ENABLE_COVERAGE "Activer les drapeaux de couverture" OFF)
if(ENABLE_COVERAGE)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
target_compile_options(example_tests PRIVATE --coverage)
target_link_options(example_tests PRIVATE --coverage)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
target_compile_options(example_tests PRIVATE -fprofile-instr-generate -fcoverage-mapping)
target_link_options(example_tests PRIVATE -fprofile-instr-generate)
endif()
endif()
GCC + gcov + lcov :
cmake -S . -B build-cov -DENABLE_COVERAGE=ON
cmake --build build-cov -j
ctest --test-dir build-cov
lcov --capture --directory build-cov --output-file coverage.info
lcov --remove coverage.info '/usr/*' --output-file coverage.info
genhtml coverage.info --output-directory coverage
Clang + llvm-cov :
cmake -S . -B build-llvm -DENABLE_COVERAGE=ON -DCMAKE_CXX_COMPILER=clang++
cmake --build build-llvm -j
LLVM_PROFILE_FILE="build-llvm/default.profraw" ctest --test-dir build-llvm
llvm-profdata merge -sparse build-llvm/default.profraw -o build-llvm/default.profdata
llvm-cov report build-llvm/example_tests -instr-profile=build-llvm/default.profdata
Sanitizers
option(ENABLE_ASAN "Activer AddressSanitizer" OFF)
option(ENABLE_UBSAN "Activer UndefinedBehaviorSanitizer" OFF)
option(ENABLE_TSAN "Activer ThreadSanitizer" OFF)
if(ENABLE_ASAN)
add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
add_link_options(-fsanitize=address)
endif()
if(ENABLE_UBSAN)
add_compile_options(-fsanitize=undefined -fno-omit-frame-pointer)
add_link_options(-fsanitize=undefined)
endif()
if(ENABLE_TSAN)
add_compile_options(-fsanitize=thread)
add_link_options(-fsanitize=thread)
endif()
Garde-fous pour les tests instables
- Ne jamais utiliser
sleeppour la synchronisation ; utiliser des variables de condition ou des barrières. - Rendre les répertoires temporaires uniques par test et toujours les nettoyer.
- Éviter le temps réel, le réseau ou les dépendances système de fichiers dans les tests unitaires.
- Utiliser des graines déterministes pour les entrées aléatoires.
Bonnes pratiques
À FAIRE
- Maintenir les tests déterministes et isolés
- Privilégier l'injection de dépendances plutôt que les variables globales
- Utiliser
ASSERT_*pour les préconditions,EXPECT_*pour les vérifications multiples - Séparer les tests unitaires et d'intégration dans les étiquettes ou répertoires CTest
- Exécuter les sanitizers dans le CI pour la détection mémoire et de race
À NE PAS FAIRE
- Ne pas dépendre du temps réel ou du réseau dans les tests unitaires
- Ne pas utiliser de
sleepcomme synchronisation quand une variable de condition peut être utilisée - Ne pas sur-mocker des objets de valeur simples
- Ne pas utiliser de correspondance de chaîne fragile pour des logs non critiques
Pièges courants
- Utilisation de chemins temporaires fixes → Générer des répertoires temporaires uniques par test et les nettoyer.
- Dépendance à l'horloge murale → Injecter une horloge ou utiliser des sources de temps factices.
- Tests de concurrence instables → Utiliser des variables de condition/barrières et des attentes bornées.
- État global caché → Réinitialiser l'état global dans les fixtures ou supprimer les globaux.
- Sur-mock → Privilégier les fakes pour le comportement avec état et ne mocker que les interactions.
- Absence d'exécution des sanitizers → Ajouter des builds ASan/UBSan/TSan dans le CI.
- Couverture sur des builds debug uniquement → S'assurer que les cibles de couverture utilisent des drapeaux cohérents.
Annexe optionnelle : Fuzzing / Tests par propriétés
À utiliser uniquement si le projet supporte déjà LLVM/libFuzzer ou une bibliothèque de tests par propriétés.
- libFuzzer : meilleur pour les fonctions pures avec un minimum d'E/S.
- RapidCheck : tests basés sur les propriétés pour valider les invariants.
Harness minimal libFuzzer (pseudocode : remplacer ParseConfig) :
#include <cstddef>
#include <cstdint>
#include <string>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
std::string input(reinterpret_cast<const char *>(data), size);
// ParseConfig(input); // fonction du projet
return 0;
}
Alternatives à GoogleTest
- Catch2 : header-only, matchers expressifs
- doctest : léger, surcharge de compilation minimale