Introducción: Por qué las pruebas en C++ no son un lujo, sino una necesidad
En el mundo de C++, la calidad del código es crucial. El lenguaje le da al desarrollador un enorme poder sobre la memoria y el rendimiento, pero este poder conlleva una gran responsabilidad. Un error en la gestión de memoria, el uso incorrecto de punteros o un manejo inadecuado de la concurrencia pueden provocar bugs difíciles de detectar que solo se manifiestan en producción.
Las pruebas son la única forma confiable de garantizar que su código C++ se comporte como se espera. En este artículo, recorreremos el camino desde pequeñas pruebas unitarias hasta verificaciones completas de extremo a extremo (E2E). Aprenderá cómo construir una estrategia de pruebas adecuada, qué herramientas utilizar y cómo escribir pruebas que realmente aporten valor.
Analizaremos tres niveles de la pirámide de pruebas: unit (unitarias), integration (integración) y e2e (extremas a extremo). Cada uno resuelve su propio problema, y solo su combinación brinda total confianza en la confiabilidad del programa.
Pruebas unitarias: El fundamento de la calidad
Las pruebas unitarias son el nivel más bajo y más rápido. Verifican funciones, clases o métodos individuales de forma aislada de las dependencias externas (bases de datos, sistema de archivos, red). El objetivo principal es asegurarse de que cada unidad lógica de código funcione correctamente.
Elección del framework para C++
En el ecosistema de C++, existen varios frameworks populares para pruebas unitarias:
- Google Test (gtest) — el framework más común de Google. Soporta assertions, fixtures, pruebas parametrizadas y mocks (a través de Google Mock).
- Catch2 — un framework ligero y moderno con una sintaxis excelente. Es especialmente bueno para la creación rápida de prototipos de pruebas.
- Boost.Test — parte de la biblioteca Boost. Potente, pero más pesado.
Para el ejemplo, usaremos Google Test, ya que es el estándar de facto en muchos proyectos C++.
Ejemplo de prueba unitaria en C++
Supongamos que tenemos una función simple para calcular el factorial:
// math_utils.h#ifndef MATH_UTILS_H#define MATH_UTILS_H
long long factorial(int n);
#endif
// math_utils.cpp#include "math_utils.h"
long long factorial(int n) { if (n < 0) throw std::invalid_argument("Negative input"); if (n == 0) return 1; long long result = 1; for (int i = 1; i <= n; ++i) { result *= i; } return result;}Ahora escribamos una prueba unitaria usando Google Test:
// test_math_utils.cpp#include <gtest/gtest.h>#include "math_utils.h"
TEST(FactorialTest, HandlesZeroInput) { EXPECT_EQ(factorial(0), 1);}
TEST(FactorialTest, HandlesPositiveInput) { 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);}Esta prueba verifica tres escenarios: el caso límite (0), el funcionamiento normal y el manejo de errores. Observe las macros EXPECT_EQ y EXPECT_THROW — no interrumpen la prueba en caso de error, solo lo registran.
Objetos Mock para el aislamiento
Para las pruebas unitarias, es fundamental aislar el código bajo prueba de las dependencias externas. Para ello se utilizan objetos mock. Google Mock (parte de Google Test) permite crear stubs fácilmente:
// database_connector.hclass DatabaseConnector {public: virtual ~DatabaseConnector() = default; virtual bool connect(const std::string& url) = 0; virtual std::vector<int> fetchData() = 0;};
// user_service.cppclass UserService { DatabaseConnector& db;public: UserService(DatabaseConnector& db) : db(db) {} int ge