Pruebas avanzadas de C++: desde pruebas unitarias hasta end-to-end
En el desarrollo moderno de C++, la calidad del código se define no solo por su rendimiento, sino también por su fiabilidad. Las pruebas avanzadas van mucho más allá de las comprobaciones simples: es un enfoque arquitectónico que incluye pruebas unitarias, de integración y end-to-end (e2e). En este artículo, analizaremos cómo construir correctamente la pirámide de pruebas para proyectos de C++, utilizando Google Test, Google Mock y frameworks propios.
1. Pruebas unitarias: aislamiento y mocks
Las pruebas unitarias verifican módulos individuales (funciones, clases) de forma aislada. A nivel avanzado, la tarea clave es aprender a mockear correctamente las dependencias. Veamos un ejemplo usando Google Mock.
Supongamos que tenemos una clase DatabaseConnector que depende de una API externa. Queremos probar la lógica de procesamiento de datos sin acceder a la base de datos real.
#include <gmock/gmock.h>#include <gtest/gtest.h>
class DatabaseInterface {public: virtual ~DatabaseInterface() = default; virtual std::string fetchData(int id) = 0;};
class MockDatabase : public DatabaseInterface {public: MOCK_METHOD(std::string, fetchData, (int id), (override));};
class DataProcessor { DatabaseInterface& db;public: explicit DataProcessor(DatabaseInterface& db) : db(db) {} std::string process(int id) { auto raw = db.fetchData(id); if (raw.empty()) return "EMPTY"; return "PROCESSED: " + raw; }};
TEST(DataProcessorTest, ProcessWithValidData) { MockDatabase mockDb; EXPECT_CALL(mockDb, fetchData(42)) .WillOnce(testing::Return("Hello")); DataProcessor processor(mockDb); EXPECT_EQ(processor.process(42), "PROCESSED: Hello");}El uso de MOCK_METHOD permite controlar con precisión el comportamiento de las dependencias. Es importante recordar los strict mocks (a través de NiceMock o StrictMock) para detectar llamadas inesperadas.
2. Pruebas de integración: probando interacciones
Las pruebas de integración verifican cómo los módulos trabajan juntos. En C++, esto a menudo incluye probar el trabajo con el sistema de archivos, protocolos de red o bases de datos. Un enfoque avanzado es usar test fixtures y parameterized tests.
Ejemplo: probamos la clase FileManager, que lee y escribe archivos. Usamos directorios temporales.
class FileManagerTest : public ::testing::Test {protected: std::filesystem::path tempDir; void SetUp() override { tempDir = std::filesystem::temp_directory_path() / "test_files"; std::filesystem::create_directories(tempDir); } void TearDown() override { std::filesystem::remove_all(tempDir); }};
TEST_F(FileManagerTest, WriteAndReadFile) { FileManager fm(tempDir.string()); fm.write("test.txt", "data"); auto content = fm.read("test.txt"); EXPECT_EQ(content, "data");}Para pruebas parametrizadas usamos TEST_P con INSTANTIATE_TEST_SUITE_P. Esto permite ejecutar la misma prueba con diferentes datos de entrada.
class MathTest : public ::testing::TestWithParam<std::tuple<int, int, int>> {};TEST_P(MathTest, Add) { auto [a, b, expected] = GetParam(); EXPECT_EQ(add(a, b), expected);}INSTANTIATE_TEST_SUITE_P(AddTests, MathTest, ::testing::Values( std::make_tuple(1, 1, 2), std::make_tuple(-1, 1, 0), std::make_tuple(0, 0, 0) ));3. Pruebas End-to-End: verificación del ciclo completo
Las pruebas e2e simulan el comportamiento del usuario final. Para aplicaciones C++, esto puede significar ejecutar el binario completo, verificar su salida o interactuar a través de una API. Nivel avanzado