Testing, Benchmarking e Sanitizer
Scrivi unit test con GoogleTest, misura le prestazioni con Google Benchmark, individua i bug con i sanitizer (ASan, UBSan, TSan) e integra l'analisi statica nella tua pipeline CI/CD.
La Piramide del Testing per il C++
Lo sviluppo C++ professionale si basa su più livelli di controllo della qualità: gli unit test verificano singole funzioni e classi, i test di integrazione controllano le interazioni tra componenti, i sanitizer individuano a runtime errori di memoria e comportamento indefinito, l'analisi statica trova bug senza eseguire il codice e i benchmark garantiscono che le prestazioni rispettino i requisiti. Insieme, questi strumenti formano una rete di sicurezza che intercetta i bug prima che arrivino in produzione.
I due framework di testing più diffusi per il C++ sono GoogleTest (di Google, molto usato nell'industria) e Catch2 (header-only, con sintassi in stile BDD). Entrambi si integrano bene con CMake e con i sistemi CI/CD.
Unit Test con GoogleTest
GoogleTest offre macro per le asserzioni (EXPECT_* per quelle non fatali, ASSERT_* per quelle fatali), fixture per condividere setup e teardown e test parametrizzati:
#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
}Integrare i Test con CMake
Configura GoogleTest con CMake ed esegui i test tramite CTest, il test runner integrato:
# 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)Sanitizer: Rilevamento dei Bug a Runtime
I sanitizer sono controlli a runtime inseriti dal compilatore che intercettano intere classi di bug. Si abilitano con i flag del compilatore — rallentano l'esecuzione di circa 2 volte, ma individuano bug che nessuna quantità di test riuscirebbe a trovare senza di essi:
# 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)Analisi Statica e Code Coverage
L'analisi statica esamina il tuo codice senza eseguirlo, individuando bug, violazioni di stile e problemi di sicurezza:
clang-tidy — Il linter C++ più potente. Segnala opportunità di modernizzazione, problemi di prestazioni, questioni di leggibilità e pattern soggetti a bug:
clang-tidy src/*.cpp -- -std=c++20 -I include
# Or with CMake:
cmake -B build -DCMAKE_CXX_CLANG_TIDY="clang-tidy;-checks=*,-fuchsia-*"cppcheck — Complementare a clang-tidy, intercetta classi di bug diverse:
cppcheck --enable=all --std=c++20 src/Code Coverage — Misura quali righe vengono effettivamente eseguite dai tuoi test:
cmake -B build-cov -DCMAKE_CXX_FLAGS="--coverage -g -O0"
cmake --build build-cov && cd build-cov && ctest
gcovr --html-details coverage.html -r ../srcPunta a una copertura delle righe dell'80% o più sui percorsi di codice critici. Il 100% raramente vale lo sforzo — concentrati sul testare la logica, i casi limite e la gestione degli errori.
Costruisci una strategia di testing completa:
1. Scrivi i test insieme al codice — non dopo. Il test-driven development (TDD) funziona bene in C++: scrivi un test che fallisce, implementa il minimo codice per farlo passare, poi rifattorizza.
2. Esegui i sanitizer in CI — predisponi job di CI separati per ASan+UBSan e per TSan. Individuano bug che i soli test si lasciano sfuggire.
3. Usa le fixture per il setup e il teardown delle risorse condivise (connessioni al database, handle di file, oggetti complessi).
4. Testa i casi limite e i percorsi di errore — input nulli, contenitori vuoti, overflow di interi, fallimenti di allocazione.
5. Mantieni i test veloci — gli unit test dovrebbero girare in pochi secondi. Sposta i test lenti (rete, disco) nelle suite di test di integrazione.
6. Misura prima di ottimizzare — prima misura, poi ottimizza. Non tirare a indovinare dove sono i colli di bottiglia.
Testare l'implementazione invece del comportamento: i test che dipendono dalle strutture dati interne si rompono quando rifattorizzi. Testa l'API pubblica e il comportamento osservabile, non i metodi privati.
Ignorare le segnalazioni dei sanitizer: i report dei sanitizer NON sono falsi positivi (salvo rarissime eccezioni). Ogni report di ASan o UBSan indica comportamento indefinito reale che PRIMA O POI causerà problemi su qualche piattaforma o a qualche livello di ottimizzazione.
Non testare con le ottimizzazioni attive: alcuni bug si manifestano solo con -O2 o -O3 (soprattutto quelli da UB sfruttato dall'ottimizzatore). Esegui la tua suite di test sia con build Debug sia con build Release.
Test instabili: i test che a volte passano e a volte falliscono sono peggio di nessun test — erodono la fiducia nella suite. Correggi o elimina immediatamente i test instabili.
- GoogleTest offre le macro
EXPECT_*(non fatali) eASSERT_*(fatali), le fixture e i test parametrizzati - Usa
gtest_discover_tests()in CMake per registrare automaticamente i test in CTest - ASan individua gli errori di memoria, UBSan il comportamento indefinito, TSan le data race — eseguili tutti in CI
- L'analisi statica (clang-tidy, cppcheck) trova bug senza eseguire il codice
- Testa il comportamento, non l'implementazione — e testa sempre i casi limite e i percorsi di errore
- Esegui i test sia con build Debug sia con build Release per scoprire i bug che dipendono dalle ottimizzazioni
Quiz — Metti alla prova le tue conoscenze
(15 XP)1. Qual è la differenza tra `EXPECT_EQ` e `ASSERT_EQ` in GoogleTest?
2. Quale sanitizer rileva i bug di use-after-free e di buffer overflow?
3. Perché dovresti eseguire i test sia con build Debug sia con build Release?