Перейти к содержимому

Тестирование, бенчмаркинг и санитарные средства

Напишите единичные тесты с помощью GoogleTest, бенчмарка Google, улавливайте ошибки с дезинфицирующими средствами (ASan, UBSan, TSan) и интегрируйте статический анализ в свой конвейер CI / CD.

Испытательная пирамида для C++

Профессиональная разработка C++ опирается на несколько уровней обеспечения качества: Единичные тесты проверяют отдельные функции и классы, интеграционные тесты проверяют взаимодействие компонентов, санитивизаторы улавливают ошибки памяти и неопределенное поведение во время выполнения, статический анализ находит ошибки без запуска кода и бенчмарки обеспечивают соответствие производительности требованиям. Вместе эти инструменты образуют сеть безопасности, которая ловит ошибки, прежде чем они достигнут производства.

Двумя наиболее популярными фреймворками тестирования C++ являются GoogleTest (от Google, широко используемый в промышленности) и Catch2 (только заголовк, синтаксис в стиле BDD). Обе системы хорошо интегрируются с CMake и CI/CD.

Тестирование с помощью GoogleTest

GoogleTest предоставляет макросы для утверждений (EXPECT_* для несмертельных, ASSERT_* для фатальных), тестовые приспособления для совместной настройки / стирания и параметризованные тесты:

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
}

Интеграция тестов с CMake

Настройте GoogleTest с помощью CMake и запустите тесты через CTest, встроенный тестовый бегун:

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)

Системные требования Runtime Bug Detection

Санитаризаторы - это встроенные в компилятор проверки времени выполнения, которые ловят целые классы ошибок. Включите их с флагами компилятора — они добавляют замедление ~2x, но ловят ошибки, которые никакое количество тестирования не может найти без них:

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)

Статический анализ и кодовое покрытие

Статический анализ проверяет ваш код без его запуска , улавливает ошибки, нарушения стиля и проблемы безопасности:

clang-tidy Самый мощный C++ linter. Проверка возможностей модернизации, проблем производительности, читаемости и подверженных ошибкам шаблонов:

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

cppcheck — дополнение к clang-tidy, ловит различные классы ошибок:

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

Кодовое покрытие — Измерение, которое выстраивает ваши тесты:

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

80%+ покрытие строки по критическим путям кода. 100% покрытие редко стоит усилий — сосредоточьтесь на проверке логики, краевых случаях и обработке ошибок.

Лучшая практика

Создайте комплексную стратегию тестирования:

1. Письменные тесты вместе с кодом — не после. Тест-управляемая разработка (TDD) хорошо работает для C++: записать неудачный тест, реализовать минимальный код для прохождения, рефакторировать.
2. Беговые дезинфицирующие средства в CI — имеют отдельные рабочие места CI для ASan+UBSan и TSan. Они ловят ошибки, которые пропускают только тесты.
3. Использовать тестовые приспособления для настройки/вывода общих ресурсов (соединений базы данных, файловых ручек, сложных объектов).
4. Испытываем краевые случаи и пути ошибок — нулевые входы, пустые контейнеры, переполнение целых чисел, отказ распределения.
5.Продолжайте тесты быстро — единичные тесты должны выполняться в считанные секунды. Переместить медленные тесты (сетевые, дисковые) в пакеты тестирования интеграции.
6.Бенчмарк перед оптимизацией — сначала измерить, затем оптимизировать. Не угадайте, где узкие места.

Подводный камень

Тестирование реализации, а не поведения: Тесты, которые зависят от внутренних структур данных, ломаются при рефакторе. Проверяйте общедоступный API и наблюдаемое поведение, а не частные методы.

Игнорирование результатов дезинфицирующего средства: Сообщения Sanitizer не являются ложноположительными (за очень редкими исключениями). Каждый отчет ASan или UBSan указывает на реальное неопределенное поведение, которое вызовет проблемы на каком-либо уровне платформы или оптимизации.

Не тестировать с оптимизацией: Некоторые ошибки проявляются только в -O2 или -O3 (особенно UB, который использует оптимизатор). Запустите свой тестовый пакет с Debug и Release сборки.

Неудачное тестирование: Тесты, которые иногда проходят и терпят неудачу, иногда хуже, чем отсутствие тестов — они подрывают доверие к набору тестов. Немедленно исправьте или удалите тесты.

Ключевые выводы
  • GoogleTest предоставляет EXPECT_* (нефатальный) и ASSERT_* (фатальный) макросы, светильники и параметризованные тесты
  • Использование gtest_discover_tests() в CMake для автоматической регистрации тестов с помощью CTest
  • ASan улавливает ошибки памяти, UBSan улавливает неопределенное поведение, TSan улавливает гонки данных - запустите все в CI
  • Статический анализ (clang-tidy, cppcheck) улавливает ошибки без запуска кода
  • Поведение теста, а не реализация — и всегда тестируйте крайние случаи и пути ошибок
  • Запустите тесты как с Debug, так и с Release, чтобы поймать ошибки, зависящие от оптимизации

Тест — проверьте свои знания

(15 XP)

1. В чем разница между `EXPECT_EQ` и `ASSERT_EQ` в GoogleTest?

2. Какой дезинфицирующее средство обнаруживает ошибки переполнения буфера после использования?

3. Почему вы должны проводить тесты с Debug и Release?