Тестирование, бенчмаркинг и санитарные средства
Напишите единичные тесты с помощью GoogleTest, бенчмарка Google, улавливайте ошибки с дезинфицирующими средствами (ASan, UBSan, TSan) и интегрируйте статический анализ в свой конвейер CI / CD.
Испытательная пирамида для C++
Профессиональная разработка C++ опирается на несколько уровней обеспечения качества: Единичные тесты проверяют отдельные функции и классы, интеграционные тесты проверяют взаимодействие компонентов, санитивизаторы улавливают ошибки памяти и неопределенное поведение во время выполнения, статический анализ находит ошибки без запуска кода и бенчмарки обеспечивают соответствие производительности требованиям. Вместе эти инструменты образуют сеть безопасности, которая ловит ошибки, прежде чем они достигнут производства.
Двумя наиболее популярными фреймворками тестирования C++ являются GoogleTest (от Google, широко используемый в промышленности) и Catch2 (только заголовк, синтаксис в стиле BDD). Обе системы хорошо интегрируются с CMake и CI/CD.
Тестирование с помощью GoogleTest
GoogleTest предоставляет макросы для утверждений (EXPECT_* для несмертельных, ASSERT_* для фатальных), тестовые приспособления для совместной настройки / стирания и параметризованные тесты:
#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 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, но ловят ошибки, которые никакое количество тестирования не может найти без них:
# 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 ../src80%+ покрытие строки по критическим путям кода. 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?