Tests, benchmarks et sanitizers
Écrivez des tests unitaires avec GoogleTest, mesurez les performances avec Google Benchmark, traquez les bugs avec les sanitizers (ASan, UBSan, TSan) et intégrez l'analyse statique à votre pipeline CI/CD.
La pyramide des tests en C++
Le développement C++ professionnel repose sur plusieurs niveaux d'assurance qualité : les tests unitaires vérifient les fonctions et classes individuelles, les tests d'intégration contrôlent les interactions entre composants, les sanitizers détectent à l'exécution les erreurs mémoire et les comportements indéfinis, l'analyse statique trouve des bugs sans exécuter le code, et les benchmarks garantissent que les performances répondent aux exigences. Ensemble, ces outils forment un filet de sécurité qui intercepte les bugs avant qu'ils n'atteignent la production.
Les deux frameworks de test C++ les plus populaires sont GoogleTest (de Google, très utilisé dans l'industrie) et Catch2 (header-only, syntaxe de style BDD). Tous deux s'intègrent bien à CMake et aux systèmes CI/CD.
Tests unitaires avec GoogleTest
GoogleTest fournit des macros d'assertion (EXPECT_* pour les échecs non fatals, ASSERT_* pour les échecs fatals), des fixtures de test pour la préparation et le nettoyage partagés (setup/teardown), et des tests paramétrés :
#include <gtest/gtest.h>
#include <vector>
#include <stdexcept>
// Function under test
int factorial(int n) {
if (n < 0) throw std::invalid_argument("negative input");
if (n <= 1) return 1;
return n * factorial(n - 1);
}
// Basic test — TEST(TestSuite, TestName)
TEST(FactorialTest, HandlesZero) {
EXPECT_EQ(factorial(0), 1);
}
TEST(FactorialTest, HandlesPositive) {
EXPECT_EQ(factorial(1), 1);
EXPECT_EQ(factorial(5), 120);
EXPECT_EQ(factorial(10), 3628800);
}
TEST(FactorialTest, ThrowsOnNegative) {
EXPECT_THROW(factorial(-1), std::invalid_argument);
}
// Test fixture — shared setup/teardown
class StackTest : public ::testing::Test {
protected:
std::vector<int> stack;
void SetUp() override {
stack.push_back(1);
stack.push_back(2);
stack.push_back(3);
}
void TearDown() override {
stack.clear();
}
};
TEST_F(StackTest, TopElement) {
EXPECT_EQ(stack.back(), 3);
}
TEST_F(StackTest, PopReducesSize) {
stack.pop_back();
EXPECT_EQ(stack.size(), 2);
EXPECT_EQ(stack.back(), 2);
}
TEST_F(StackTest, IsNotEmpty) {
ASSERT_FALSE(stack.empty()); // fatal — stops test if false
EXPECT_GT(stack.size(), 0u); // non-fatal — continues on failure
}Intégrer les tests à CMake
Configurez GoogleTest avec CMake et exécutez les tests via CTest, le lanceur de tests intégré :
# CMakeLists.txt for testing
cmake_minimum_required(VERSION 3.20)
project(MyProject LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Fetch GoogleTest
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)
# Main library
add_library(mylib src/math.cpp src/utils.cpp)
target_include_directories(mylib PUBLIC include)
# Test executable
enable_testing()
add_executable(unit_tests
tests/test_math.cpp
tests/test_utils.cpp
)
target_link_libraries(unit_tests PRIVATE mylib GTest::gtest_main)
# Register tests with CTest
include(GoogleTest)
gtest_discover_tests(unit_tests)
# Run tests:
# cmake -B build && cmake --build build
# cd build && ctest --output-on-failure --parallel $(nproc)Sanitizers : détecter les bugs à l'exécution
Les sanitizers sont des vérifications à l'exécution, insérées par le compilateur, qui détectent des catégories entières de bugs. Activez-les avec des options du compilateur — ils ralentissent l'exécution d'environ 2x, mais détectent des bugs qu'aucune quantité de tests ne permettrait de trouver sans eux :
# AddressSanitizer (ASan) — catches memory errors
# Buffer overflows, use-after-free, double-free, memory leaks
cmake -B build-asan \
-DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
# UndefinedBehaviorSanitizer (UBSan) — catches UB
# Signed overflow, null dereference, misaligned access, shift errors
cmake -B build-ubsan \
-DCMAKE_CXX_FLAGS="-fsanitize=undefined -fno-omit-frame-pointer -g" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=undefined"
# ThreadSanitizer (TSan) — catches data races
# Data races, lock-order inversions, deadlocks
cmake -B build-tsan \
-DCMAKE_CXX_FLAGS="-fsanitize=thread -g" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread"
# MemorySanitizer (MSan) — catches uninitialized reads (Clang only)
cmake -B build-msan \
-DCMAKE_CXX_FLAGS="-fsanitize=memory -fno-omit-frame-pointer -g" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=memory"
# Combine ASan + UBSan (commonly done together)
cmake -B build-asan-ubsan \
-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"
# IMPORTANT: ASan and TSan cannot be combined — use separate builds
# IMPORTANT: Always compile ALL code with sanitizer flags (including libraries)Analyse statique et couverture de code
L'analyse statique examine votre code sans l'exécuter, et détecte les bugs, les écarts de style et les problèmes de sécurité :
clang-tidy — Le linter C++ le plus puissant. Il repère les possibilités de modernisation, les problèmes de performance, de lisibilité et les motifs propices aux bugs :
clang-tidy src/*.cpp -- -std=c++20 -I include
# Or with CMake:
cmake -B build -DCMAKE_CXX_CLANG_TIDY="clang-tidy;-checks=*,-fuchsia-*"cppcheck — Complémentaire de clang-tidy, il détecte d'autres catégories de bugs :
cppcheck --enable=all --std=c++20 src/Couverture de code — Mesurez quelles lignes vos tests exécutent réellement :
cmake -B build-cov -DCMAKE_CXX_FLAGS="--coverage -g -O0"
cmake --build build-cov && cd build-cov && ctest
gcovr --html-details coverage.html -r ../srcVisez 80 %+ de couverture de lignes sur les chemins de code critiques. Une couverture de 100 % vaut rarement l'effort — concentrez-vous sur les tests de la logique, des cas limites et de la gestion des erreurs.
Construisez une stratégie de test complète :
1. Écrivez les tests en même temps que le code — pas après. Le développement piloté par les tests (TDD) fonctionne bien en C++ : écrivez un test qui échoue, implémentez le minimum de code pour le faire passer, puis refactorisez.
2. Exécutez les sanitizers en CI — prévoyez des jobs CI distincts pour ASan+UBSan et pour TSan. Ils détectent des bugs que les tests seuls ne voient pas.
3. Utilisez des fixtures de test pour la préparation et le nettoyage des ressources partagées (connexions à une base de données, descripteurs de fichiers, objets complexes).
4. Testez les cas limites et les chemins d'erreur — entrées nulles, conteneurs vides, dépassement d'entier, échec d'allocation.
5. Gardez des tests rapides — les tests unitaires doivent s'exécuter en quelques secondes. Déplacez les tests lents (réseau, disque) vers des suites de tests d'intégration.
6. Mesurez avant d'optimiser — mesurez d'abord, optimisez ensuite. Ne devinez pas où se trouvent les goulots d'étranglement.
Tester l'implémentation plutôt que le comportement : les tests qui dépendent des structures de données internes cassent dès que vous refactorisez. Testez l'API publique et le comportement observable, pas les méthodes privées.
Ignorer les signalements des sanitizers : les rapports des sanitizers ne sont PAS des faux positifs (à de très rares exceptions près). Chaque rapport d'ASan ou d'UBSan signale un véritable comportement indéfini qui CAUSERA des problèmes sur une plateforme ou à un niveau d'optimisation donné.
Ne pas tester avec les optimisations : certains bugs ne se manifestent qu'avec -O2 ou -O3 (notamment les UB que l'optimiseur exploite). Exécutez votre suite de tests avec des builds Debug et Release.
Tests instables (flaky) : les tests qui passent parfois et échouent parfois sont pires que l'absence de tests — ils sapent la confiance dans la suite de tests. Corrigez ou supprimez immédiatement les tests instables.
- GoogleTest fournit les macros
EXPECT_*(non fatales) etASSERT_*(fatales), des fixtures et des tests paramétrés - Utilisez
gtest_discover_tests()dans CMake pour enregistrer automatiquement les tests auprès de CTest - ASan détecte les erreurs mémoire, UBSan les comportements indéfinis, TSan les data races — exécutez-les tous en CI
- L'analyse statique (clang-tidy, cppcheck) détecte des bugs sans exécuter le code
- Testez le comportement, pas l'implémentation — et testez toujours les cas limites et les chemins d'erreur
- Exécutez les tests avec des builds Debug et Release pour détecter les bugs qui dépendent des optimisations
Quiz — Testez vos connaissances
(15 XP)1. Quelle est la différence entre `EXPECT_EQ` et `ASSERT_EQ` dans GoogleTest ?
2. Quel sanitizer détecte les bugs de type use-after-free et les débordements de tampon ?
3. Pourquoi faut-il exécuter les tests avec des builds Debug et Release ?