Ir al contenido

Testing, Benchmarking y Sanitizadores

Escribe pruebas unitarias con GoogleTest, haz benchmarking con Google Benchmark, detecta bugs con sanitizadores (ASan, UBSan, TSan) e integra analisis estatico en tu pipeline de CI/CD.

La Piramide de Testing para C++

El desarrollo profesional de C++ se apoya en multiples capas de aseguramiento de calidad: las pruebas unitarias verifican funciones y clases individuales, las pruebas de integracion comprueban las interacciones entre componentes, los sanitizadores detectan errores de memoria y comportamiento indefinido en tiempo de ejecucion, el analisis estatico encuentra bugs sin ejecutar el codigo, y los benchmarks aseguran que el rendimiento cumpla los requisitos. Juntas, estas herramientas forman una red de seguridad que detecta bugs antes de que lleguen a produccion.

Los dos frameworks de testing de C++ mas populares son GoogleTest (de Google, ampliamente usado en la industria) y Catch2 (solo headers, sintaxis estilo BDD). Ambos se integran bien con CMake y sistemas de CI/CD.

Pruebas Unitarias con GoogleTest

GoogleTest proporciona macros para aserciones (EXPECT_* para no fatales, ASSERT_* para fatales), fixtures de prueba para setup/teardown compartido, y pruebas parametrizadas:

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
}

Integrando Pruebas con CMake

Configura GoogleTest con CMake y ejecuta las pruebas a traves de CTest, el ejecutor de pruebas integrado:

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)

Sanitizadores: Deteccion de Bugs en Tiempo de Ejecucion

Los sanitizadores son verificaciones en tiempo de ejecucion instrumentadas por el compilador que detectan clases enteras de bugs. Habilitalos con flags del compilador — agregan aproximadamente 2x de ralentizacion pero detectan bugs que ninguna cantidad de testing puede encontrar sin ellos:

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)

Analisis Estatico y Cobertura de Codigo

El analisis estatico examina tu codigo sin ejecutarlo, detectando bugs, violaciones de estilo y problemas de seguridad:

clang-tidy — El linter de C++ mas poderoso. Verifica oportunidades de modernizacion, problemas de rendimiento, legibilidad y patrones propensos a errores:

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

cppcheck — Complementario a clang-tidy, detecta diferentes clases de bugs:

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

Cobertura de Codigo — Mide que lineas realmente ejecutan tus pruebas:

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

Apunta a 80%+ de cobertura de lineas en rutas de codigo criticas. 100% de cobertura rara vez vale el esfuerzo — enfocate en probar logica, casos limite y manejo de errores.

Buena práctica

Construye una estrategia de testing integral:

1. Escribe pruebas junto con el codigo — no despues. El desarrollo guiado por pruebas (TDD) funciona bien para C++: escribe una prueba que falle, implementa el codigo minimo para pasar, refactoriza.
2. Ejecuta sanitizadores en CI — ten trabajos de CI separados para ASan+UBSan y TSan. Estos detectan bugs que las pruebas solas no encuentran.
3. Usa fixtures de prueba para setup/teardown de recursos compartidos (conexiones a bases de datos, handles de archivos, objetos complejos).
4. Prueba casos limite y rutas de error — entradas nulas, contenedores vacios, desbordamiento de enteros, fallo de asignacion.
5. Manten las pruebas rapidas — las pruebas unitarias deben ejecutarse en segundos. Mueve las pruebas lentas (red, disco) a suites de pruebas de integracion.
6. Haz benchmark antes de optimizar — mide primero, luego optimiza. No adivines donde estan los cuellos de botella.

Error común

Probar la implementacion, no el comportamiento: Las pruebas que dependen de estructuras de datos internas se rompen cuando refactorizas. Prueba la API publica y el comportamiento observable, no los metodos privados.

Ignorar hallazgos de sanitizadores: Los reportes de sanitizadores NO son falsos positivos (con excepciones muy raras). Cada reporte de ASan o UBSan indica comportamiento indefinido real que CAUSARA problemas en alguna plataforma o nivel de optimizacion.

No probar con optimizaciones: Algunos bugs solo se manifiestan con -O2 o -O3 (especialmente UB que el optimizador explota). Ejecuta tu suite de pruebas con compilaciones tanto Debug como Release.

Pruebas inestables (flaky): Las pruebas que a veces pasan y a veces fallan son peores que no tener pruebas — erosionan la confianza en la suite de pruebas. Corrige o elimina las pruebas inestables inmediatamente.

Puntos clave
  • GoogleTest proporciona macros EXPECT_* (no fatales) y ASSERT_* (fatales), fixtures y pruebas parametrizadas
  • Usa gtest_discover_tests() en CMake para auto-registrar pruebas con CTest
  • ASan detecta errores de memoria, UBSan detecta comportamiento indefinido, TSan detecta carreras de datos — ejecuta todos en CI
  • El analisis estatico (clang-tidy, cppcheck) detecta bugs sin ejecutar el codigo
  • Prueba el comportamiento, no la implementacion — y siempre prueba casos limite y rutas de error
  • Ejecuta las pruebas con compilaciones tanto Debug como Release para detectar bugs dependientes de la optimizacion

Quiz — Pon a prueba tus conocimientos

(15 XP)

1. Cual es la diferencia entre `EXPECT_EQ` y `ASSERT_EQ` en GoogleTest?

2. Que sanitizador detecta bugs de uso despues de liberar (use-after-free) y desbordamiento de buffer?

3. Por que deberias ejecutar las pruebas con compilaciones tanto Debug como Release?