Testen, Benchmarking & Sanitizer
Schreibe Unit-Tests mit GoogleTest, miss Performance mit Google Benchmark, finde Bugs mit Sanitizern (ASan, UBSan, TSan) und integriere statische Analyse in deine CI/CD-Pipeline.
Die Testpyramide für C++
Professionelle C++-Entwicklung stützt sich auf mehrere Ebenen der Qualitätssicherung: Unit-Tests prüfen einzelne Funktionen und Klassen, Integrationstests kontrollieren das Zusammenspiel der Komponenten, Sanitizer fangen Speicherfehler und undefiniertes Verhalten zur Laufzeit ab, statische Analyse findet Bugs, ohne den Code auszuführen, und Benchmarks stellen sicher, dass die Performance den Anforderungen genügt. Zusammen bilden diese Werkzeuge ein Sicherheitsnetz, das Fehler abfängt, bevor sie in Produktion gelangen.
Die beiden beliebtesten C++-Testframeworks sind GoogleTest (von Google, in der Industrie weit verbreitet) und Catch2 (Header-only, Syntax im BDD-Stil). Beide lassen sich gut in CMake und CI/CD-Systeme integrieren.
Unit-Tests mit GoogleTest
GoogleTest bietet Makros für Zusicherungen (EXPECT_* für nicht-fatale, ASSERT_* für fatale), Test-Fixtures für gemeinsames Setup/Teardown und parametrisierte Tests:
#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
}Tests in CMake einbinden
Richte GoogleTest mit CMake ein und führe die Tests über CTest aus, den eingebauten Test-Runner:
# 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: Bugs zur Laufzeit aufspüren
Sanitizer sind vom Compiler instrumentierte Laufzeitprüfungen, die ganze Fehlerklassen abfangen. Aktiviere sie über Compiler-Flags — sie verlangsamen das Programm etwa um den Faktor 2, finden aber Bugs, die ohne sie auch das gründlichste Testen nicht aufdeckt:
# 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)Statische Analyse & Code Coverage
Statische Analyse untersucht deinen Code, ohne ihn auszuführen, und findet dabei Bugs, Stilverstöße und Sicherheitsprobleme:
clang-tidy — der mächtigste C++-Linter. Er prüft auf Modernisierungsmöglichkeiten, Performanceprobleme, Lesbarkeit und fehleranfällige Muster:
clang-tidy src/*.cpp -- -std=c++20 -I include
# Or with CMake:
cmake -B build -DCMAKE_CXX_CLANG_TIDY="clang-tidy;-checks=*,-fuchsia-*"cppcheck — eine Ergänzung zu clang-tidy, die andere Fehlerklassen findet:
cppcheck --enable=all --std=c++20 src/Code Coverage — miss, welche Zeilen deine Tests tatsächlich ausführen:
cmake -B build-cov -DCMAKE_CXX_FLAGS="--coverage -g -O0"
cmake --build build-cov && cd build-cov && ctest
gcovr --html-details coverage.html -r ../srcStrebe auf kritischen Codepfaden mehr als 80 % Zeilenabdeckung an. 100 % Abdeckung lohnt den Aufwand selten — konzentriere dich darauf, Logik, Randfälle und Fehlerbehandlung zu testen.
Baue eine umfassende Teststrategie auf:
1. Schreibe Tests parallel zum Code — nicht hinterher. Test-Driven Development (TDD) funktioniert in C++ gut: einen fehlschlagenden Test schreiben, den minimalen Code zum Bestehen implementieren, refaktorieren.
2. Lass Sanitizer in der CI laufen — richte getrennte CI-Jobs für ASan+UBSan und TSan ein. Sie finden Bugs, die Tests allein übersehen.
3. Nutze Test-Fixtures für Setup/Teardown gemeinsam genutzter Ressourcen (Datenbankverbindungen, Datei-Handles, komplexe Objekte).
4. Teste Randfälle und Fehlerpfade — Null-Eingaben, leere Container, Integer-Überlauf, fehlgeschlagene Allokation.
5. Halte Tests schnell — Unit-Tests sollten in Sekunden durchlaufen. Verschiebe langsame Tests (Netzwerk, Festplatte) in Integrationstest-Suites.
6. Miss, bevor du optimierst — erst messen, dann optimieren. Rate nicht, wo die Engpässe liegen.
Die Implementierung testen statt das Verhalten: Tests, die von internen Datenstrukturen abhängen, brechen beim Refactoring. Teste die öffentliche API und das beobachtbare Verhalten, nicht private Methoden.
Sanitizer-Befunde ignorieren: Sanitizer-Meldungen sind KEINE False Positives (von sehr seltenen Ausnahmen abgesehen). Jede ASan- oder UBSan-Meldung weist auf echtes undefiniertes Verhalten hin, das auf irgendeiner Plattform oder Optimierungsstufe Probleme verursachen WIRD.
Nicht mit Optimierungen testen: Manche Bugs zeigen sich erst mit -O2 oder -O3 (insbesondere UB, das der Optimierer ausnutzt). Lass deine Test-Suite sowohl im Debug- als auch im Release-Build laufen.
Flaky Tests: Tests, die mal durchlaufen und mal fehlschlagen, sind schlimmer als gar keine Tests — sie zerstören das Vertrauen in die Test-Suite. Repariere oder lösche flaky Tests sofort.
- GoogleTest bietet die Makros
EXPECT_*(nicht-fatal) undASSERT_*(fatal), Fixtures und parametrisierte Tests - Verwende
gtest_discover_tests()in CMake, um Tests automatisch bei CTest zu registrieren - ASan findet Speicherfehler, UBSan undefiniertes Verhalten, TSan Data Races — lass alle in der CI laufen
- Statische Analyse (clang-tidy, cppcheck) findet Bugs, ohne den Code auszuführen
- Teste Verhalten, nicht Implementierung — und teste immer Randfälle und Fehlerpfade
- Lass Tests sowohl im Debug- als auch im Release-Build laufen, um optimierungsabhängige Bugs zu finden
Quiz — Teste dein Wissen
(15 XP)1. Was ist der Unterschied zwischen `EXPECT_EQ` und `ASSERT_EQ` in GoogleTest?
2. Welcher Sanitizer erkennt Use-after-free- und Pufferüberlauf-Fehler?
3. Warum solltest du Tests sowohl im Debug- als auch im Release-Build laufen lassen?