Přeskočit na obsah

Testování, srovnávání a dezinfekce

Pište jednotkové testy pomocí GoogleTest, srovnávejte pomocí Google Benchmark, odhalujte chyby pomocí dezinfekčních prostředků (ASan, UBSan, TSan) a integrujte statickou analýzu do svého kanálu CI/CD.

Testovací pyramida pro C++

Profesionální vývoj C++ se opírá o více vrstev zajištění kvality: jednotkové testy ověřují jednotlivé funkce a třídy, integrační testy kontrolují interakce komponent, sanitizéry zachycují chyby paměti a nedefinované chování za běhu, statická analýza nachází chyby bez spuštění kódu a benchmarky zajišťují, že výkon splňuje požadavky. Tyto nástroje společně tvoří záchrannou síť, která zachytí chyby dříve, než se dostanou do výroby.

Dva nejoblíbenější testovací rámce C++ jsou GoogleTest (od společnosti Google, široce používaný v průmyslu) a Catch2 (pouze záhlaví, syntaxe ve stylu BDD). Oba se dobře integrují se systémy CMake a CI/CD.

Testování jednotek pomocí GoogleTest

GoogleTest provides macros for assertions (EXPECT_*for non-fatal,ASSERT_*for fatal), test fixtures for shared setup/teardown, and parameterized tests:

test_factorial.cpp
#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
}

Integrating Tests with CMake

Set up GoogleTest with CMake and run tests via CTest, the built-in test runner:

CMakeLists.txt
# 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: Runtime Bug Detection

Sanitizers are compiler-instrumented runtime checks that catch entire classes of bugs. Enable them with compiler flags — they add ~2x slowdown but catch bugs that no amount of testing can find without them:

terminal
# 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)

Static Analysis & Code Coverage

Static analysis examines your code without running it, catching bugs, style violations, and security issues:

clang-tidy — The most powerful C++ linter. Checks for modernization opportunities, performance issues, readability, and bug-prone patterns:

clang-tidy src/*.cpp -- -std=c++20 -I include
# Or with CMake:
cmake -B build -DCMAKE_CXX_CLANG_TIDY="clang-tidy;-checks=*,-fuchsia-*"

cppcheck — Complementary to clang-tidy, catches different bug classes:

cppcheck --enable=all --std=c++20 src/

Code Coverage — Measure which lines your tests actually execute:

cmake -B build-cov -DCMAKE_CXX_FLAGS="--coverage -g -O0"
cmake --build build-cov && cd build-cov && ctest
gcovr --html-details coverage.html -r ../src

Aim for 80%+ line coverage on critical code paths. 100% coverage is rarely worth the effort — focus on testing logic, edge cases, and error handling.

Osvědčený postup

Vytvořte komplexní testovací strategii:

1. Pište testy vedle kódu — ne po. Testem řízený vývoj (TDD) funguje dobře pro C++: napište neúspěšný test, implementujte minimální kód, který má projít, refaktorujte.
2. Spouštějte dezinfekční prostředky v CI – mají samostatné úlohy CI pro ASan+UBSan a TSan. Tyto chytají chyby, které samotné testy minou.
3. Použijte testovací zařízení pro nastavení/rozpadnutí sdílených zdrojů (databázová připojení, popisovače souborů, složité objekty).
4. Test okrajových případů a chybových cest — nulové vstupy, prázdné kontejnery, přetečení celého čísla, selhání alokace.
5. Udržujte rychlé testy – testy jednotek by měly probíhat během několika sekund. Přesuňte pomalé testy (síť, disk) do sad testů integrace.
6. Srovnání před optimalizací – nejprve měřte, poté optimalizujte. Nehádejte, kde jsou úzká hrdla.

Úskalí

Testovací implementace, nikoli chování: Testy, které závisí na interních datových strukturách, se při refaktorování přeruší. Testujte veřejné API a pozorovatelné chování, nikoli soukromé metody.

Ignorování nálezů dezinfekce: Hlášení dezinfekce NEJSOU falešně pozitivní (až na velmi vzácné výjimky). Každá zpráva ASan nebo UBSan naznačuje skutečné nedefinované chování, které ZPŮSOBÍ problémy na určité úrovni platformy nebo optimalizace.

Netestuje se s optimalizacemi: Některé chyby se projevují pouze s-O2nebo-O3(zejména UB, které optimalizátor využívá). Spusťte svou testovací sadu se sestavenímiDebugaRelease.

Nepravidelné testy: Testy, které někdy projdou a někdy selžou, jsou horší než žádné testy – narušují důvěru v testovací sadu. Nefunkční testy okamžitě opravte nebo odstraňte.

Klíčové poznatky
  • GoogleTest providesEXPECT_*(non-fatal) andASSERT_*(fatal) macros, fixtures, and parameterized tests
  • Usegtest_discover_tests()in CMake to auto-register tests with CTest
  • ASan zachytí chyby paměti, UBSan zachytí nedefinované chování, TSan zachytí datové závody — vše spusťte v CI
  • Static analysis (clang-tidy, cppcheck) catches bugs without running the code
  • Testujte chování, nikoli implementaci – a vždy testujte okrajové případy a cesty chyb
  • Run tests with both Debug and Release builds to catch optimization-dependent bugs

Kvíz — Otestujte své znalosti

(15 XP)

1. Jaký je rozdíl mezi`EXPECT_EQ`a`ASSERT_EQ`v GoogleTest?

2. Which sanitizer detects use-after-free and buffer overflow bugs?

3. Proč byste měli spouštět testy s laděním i vydáním?