Introducción: Por qué las pruebas son críticas para proyectos C++
Probar el código en C++ no es solo una formalidad, sino una práctica fundamental que garantiza la estabilidad, seguridad y mantenibilidad del software. A diferencia de los lenguajes interpretados, donde un error puede manifestarse instantáneamente, en C++ los bugs a menudo permanecen ocultos hasta el momento de la compilación o la ejecución en condiciones específicas. Una fuga de memoria pasada por alto, un comportamiento indefinido o un manejo incorrecto de punteros pueden provocar fallos fatales.
La industria del desarrollo moderna destaca tres niveles principales de pruebas: Unit (pruebas unitarias), Integration (pruebas de integración) y E2E (pruebas de extremo a extremo). Cada uno resuelve su propia tarea y juntos forman la "pirámide de pruebas", permitiendo detectar hasta el 90% de los errores antes del lanzamiento a producción.
En este artículo, analizaremos cada tipo de prueba con ejemplos prácticos en C++, utilizando los frameworks populares Google Test y Catch2, y también discutiremos las mejores prácticas para su aplicación.
1. Pruebas Unitarias: Verificación de módulos atómicos
Las pruebas unitarias son el nivel más bajo y el más rápido. Verifican funciones, clases o métodos individuales de forma aislada del resto del sistema. El objetivo es asegurarse de que cada "bloque" de código funcione correctamente.
Ejemplo de prueba unitaria con Google Test
Consideremos una función matemática simple:
// math_utils.h#pragma once
namespace MathUtils { int add(int a, int b); int divide(int a, int b);}// math_utils.cpp#include "math_utils.h"
namespace MathUtils { int add(int a, int b) { return a + b; }
int divide(int a, int b) { if (b == 0) { throw std::invalid_argument("Division by zero"); } return a / b; }}Ahora escribamos pruebas unitarias con Google Test:
// test_math_utils.cpp#include <gtest/gtest.h>#include "math_utils.h"
TEST(MathUtilsTest, AddPositiveNumbers) { EXPECT_EQ(MathUtils::add(2, 3), 5); EXPECT_EQ(MathUtils::add(0, 0), 0);}
TEST(MathUtilsTest, AddNegativeNumbers) { EXPECT_EQ(MathUtils::add(-1, -1), -2); EXPECT_EQ(MathUtils::add(-5, 3), -2);}
TEST(MathUtilsTest, DivideNormal) { EXPECT_EQ(MathUtils::divide(10, 2), 5); EXPECT_EQ(MathUtils::divide(7, 3), 2); // División de enteros}
TEST(MathUtilsTest, DivideByZeroThrows) { EXPECT_THROW(MathUtils::divide(5, 0), std::invalid_argument);}Uso de objetos Mock
Para aislar un módulo, a menudo es necesario simular dependencias. Google Mock (parte de Google Test) permite crear stubs:
// database_interface.hclass DatabaseInterface {public: virtual ~DatabaseInterface() = default; virtual bool save(const std::string& data) = 0;};
// user_service.cppclass UserService { DatabaseInterface& db;public: explicit UserService(DatabaseInterface& db) : db(db) {} bool registerUser(const std::string& name) { return db.save("user:" + name); }};
// test_user_service.cpp#include <gmock/gmock.h>
class MockDatabase : public DatabaseInterface {public: MOCK_METHOD(bool, save, (const std::string&), (override));};
TEST(UserServiceTest, RegisterUserCallsDb) { MockDatabase mockDb; EXPECT_CALL(mockDb, save("user:Alice")).WillOnce(testing::Return(true));
UserService service(mockDb); EXPECT_TRUE(service.registerUser("Alice"));}Ventajas: Velocidad de ejecución (milisegundos), localización precisa de errores, posibilidad de ejecutar sin dependencias externas.
Desventajas: No verifican la interacción entre componentes reales.
2. Pruebas de Integración: Verificación de la combinación de componentes
Las pruebas de integración verifican cómo varios mó