Guía de pruebas unitarias en C++

23-07-2021

Por

Pivotar de lenguajes de alto nivel como Java o C# a lenguajes de más bajo nivel como C++, es un tema divertido, y se pone aún más divertido cuando no sólo quieres escribir código, sino también quieres probarlo de forma automática con pruebas unitarias.

Este artículo explica algunas cosas que me hubiera gustado saber antes de empezar a hacer pruebas unitarias en proyectos con C++. No pretendo profundizar en las pruebas unitarias en sí, ni en conceptos básicos como la creación de dobles de prueba con gMock, ya que estos temas están bien documentados en la web oficial y hablar de ellos podría hacer que la guía sea muy extensa.

Los ejemplos están hechos en Windows usando el compilador del MSBuild, por lo que algunas cosas pueden variar respecto a otros entornos. No obstante, los temas tratados son agnósticos al entorno.

Polimorfismo sin interfaces

El polimorfismo es necesario en las pruebas unitarias. El polimorfismo aplicado con la inyección de dependencias, nos permite hacer pruebas unitarias sobre un artefacto que depende de otros, sin acoplarnos a las implementaciones reales de sus dependencias.

Aunque C++ sea un lenguaje orientado a objetos, no tiene el concepto de interfaz tal cual lo conocemos en lenguajes de más alto nivel como C# o Java, por lo que una de las primeras dudas que surgen cuando empiezas a trabajar con este lenguaje es ¿cómo hago polimorfismo en C++?

C++ no tiene interfaces, pero tiene clases abstractas y nos podemos aprovechar de ellas para aplicar polimorfismo en nuestro código:

polimorfismo-diagrama|407x361

UserRepository.h

#include "User.h" #include <memory> class UserRepository { public: virtual void Save(const std::shared_ptr<User>& user) = 0; };

PostgreSqlUserRepository.h

#pragma once #include "UserRepository.h" class PostgreSqlUserRepository : public UserRepository { public: PostgreSqlUserRepository() = default; ~PostgreSqlUserRepository() = default; PostgreSqlUserRepository& operator=(const PostgreSqlUserRepository&) = delete; PostgreSqlUserRepository& operator=(const PostgreSqlUserRepository&&) = delete; PostgreSqlUserRepository(const PostgreSqlUserRepository&) = delete; PostgreSqlUserRepository(const PostgreSqlUserRepository&&) = delete; void Save(const std::shared_ptr<User>& user) override; };

PostgreSqlUserRepository.cpp

#include "PostgreSqlUserRepository.h" void PostgreSqlUserRepository::Save(const std::shared_ptr<User>& user) { //your postgresql impl }

UserService.h

#pragma once #include <memory> #include "UserRepository.h" class UserService { public: UserService() = delete; ~UserService() = default; UserService& operator=(const UserService&) = delete; UserService& operator=(const UserService&&) = delete; UserService(const UserService&) = delete; UserService(const UserService&&) = delete; UserService(std::shared_ptr<UserRepository> userRepository); void Create(const std::shared_ptr<User>& user); private: std::shared_ptr<UserRepository> _userRepository = nullptr; };

UserService.cpp

#include "pch.h" #include "UserService.h" UserService::UserService(std::shared_ptr<UserRepository> userRepository) : _userRepository(userRepository) { } void UserService::Create(const std::shared_ptr<User>& user) { //... }

Con un simple método de factoría, podemos crear instancias del UserService, usando diferentes implementaciones del UserRepository. Esto es exactamente lo que buscamos, ya que necesitaremos instanciar el servicio usando un repositorio falso para nuestras pruebas unitarias.

Cabe destacar que las clases abstractas no son la única vía en C++ para crear interfaces. Podríamos conseguir un comportamiento similar usando estructuras. La mayoría de los resultados que aparecen en Google cuando buscas cómo hacer interfaces en C++ usan struct en sus ejemplos, pero bajo mi punto de vista, las clases abstractas son más apropiadas para usarlas como interfaces porque no puedes instanciarlas.

GoogleTests como marco de pruebas unitarias

Si buscamos en internet marcos para pruebas unitarias para proyecto con C++, encontraremos bastantes opciones, pero la mayoría de ellas con una documentación muy pobre y poca comunidad. De todas las opciones que hay, la que más brilla en mi opinión es GoogleTests (GTests).

Las ventajas que le veo respecto a los demás marcos de pruebas son:

  • Una sintaxis clara: Si vienes de lenguajes de más alto nivel, quizá pensarás que estoy loco, pero GTests en comparación con otros marcos de pruebas tiene una sintaxis decente.

  • Soporte integrado para Mocks: GTests se apoya en otra librería llamada gMock para ayudarnos con los dobles de pruebas. Hacer dobles de pruebas en C++ no es tan trivial como en lenguajes de más alto nivel, pero gMock resuelve esta necesidad bastante bien.

  • Buen marco de aserciones: GTests nos provee una buena API de aserciones para nuestras pruebas unitarias. Además, nos permite crear nuestras propias funciones de aserciones personalizadas de manera muy sencilla. Algo muy útil en C++ debido a su gran variedad de tipos.

  • Integración con distintos sistemas de compilación: GTests te sirve para proyectos con Visual Studio, CMake o Makefile

  • Integración con CI: Los reportes generados automáticamente por GTests, usan el formato JUnit-style XML, esto es bastante importante porque es el formato que usan la mayoría de los sistemas de CI, como Gitlab Pipelines o Github actions para interpretar los reportes.

Como crear y configurar un proyecto de test en Visual Studio

Para crear un proyecto de pruebas con GTests en Visual Studio, basta con crear un proyecto de tipo “Google Tests” usando el asistente de proyecto.

gtest-como-marco-de-pruebas-1|690x411

Una vez creado el proyecto, estará listo para empezar a crear pruebas unitarias. Este tipo de proyecto incluye la librería de GTests, (enlazada de forma estática o dinámica según tu elección), por lo que no hay que invertir tiempo en incluirla a mano.

gtest-como-marco-de-pruebas-2|690x187

Desgraciadamente, el proyecto no incluye gMock. Esto es una tarea que debemos hacer a mano si queremos usar dobles de pruebas. Para hacerlo, podemos instalar el Nuget gmock, un paquete creado directamente por Google, que incluye GTests y gMock perfectamente integrados.

Una vez instalado el paquete gMock, podemos desinstalar el Nuget que viene en el proyecto por defecto: Microsoft.googletest.v140.windesktop.msvcstl.static.rt-static, ya no es necesario.

El siguiente paso es incluir en el proyecto los siguientes tres archivos, como elementos existentes:

  • gmock-all.cc

  • gtest-all.cc

  • gtest_main.cc

Estos archivos los encontraremos en la ruta de instalación de los paquetes Nuget en nuestro proyecto, generalmente en la carpeta packages que se encuentra en la raíz de nuestra solución.

El último paso para terminar de configurar nuestro proyecto de pruebas, será desactivar el uso de las cabeceras precompiladas (precompiled headers - pch.h). Esto lo podemos hacer en la configuración del proyecto, (click derecho sobre el proyecto > Propiedades > C / C++ > Precompiled Headers).

gtest-como-marco-de-pruebas-3|547x97

Una vez cambiada la configuración del proyecto, podremos eliminar los archivos pch.h y pch.cpp. Debemos tener en cuenta que, a partir de ahora, vamos a tener que incluir GTest y GMock a mano en archivo de pruebas.

#include "gtest/gtest.h" #include "gmock/gmock.h"

Given-When-Then VS Arrange-Expect-Act

Given-When-Then o *Arrange-Act-Assert, (*no confundir con Arrange-Expect-Act del título), **son patrones muy conocidos en el ámbito de las pruebas unitarias para estructurar los tests. Básicamente definen tres bloques principales en el test: la preparación, el código que se quiere probar y las aserciones.

#include "gtest/gtest.h" #include "gmock/gmock.h" TEST(TestCaseName, TestName) { //GIVEN int num1 = 1; int num2 = 1; //WHEN int result = num1 + num2; //THEN ASSERT_EQ(result, 2); }

Hasta aquí todo muy bien, siempre y cuando no estemos probando un artefacto donde tengamos que usar dobles de pruebas.

Volvamos al ejemplo anterior, el que usamos para hablar del polimorfismo sin interfaces. Supongamos que tenemos la siguiente implementación del servicio de usuarios:

#include "pch.h" #include "UserService.h" UserService::UserService(std::shared_ptr<UserRepository> userRepository) : _userRepository(userRepository) { } void UserService::Create(const std::shared_ptr<User>& user) { if(_userRepository->Exist(user)) return; _userRepository->Save(user); }

La primera aproximación a una prueba unitaria podría ser la siguiente:

#include "gtest/gtest.h" #include "gmock/gmock.h" #include "FakeUserRepository.h" #include "UserService.h" class UserServiceTests : public ::testing::Test { protected: std::shared_ptr<FakeUserRepository> userRepository = std::make_shared<FakeUserRepository>(); UserService* userService; void SetUp() override { userService = new UserService(userRepository); } void TearDown() override { delete userService; } }; TEST_F(UserServiceTests, Saves_User) { //GIVEN std::shared_ptr<User> user = std::make_shared<User>(1); ON_CALL(*userRepository, Exist(user)) .WillByDefault(testing::Return(false)); //WHEN userService->Create(user); //THEN EXPECT_CALL(*userRepository, Save(user)) .Times(testing::Exactly(1)); }

Sin embargo, si ejecutamos el test anterior veremos un rojo en la pantalla, debido a que el test no considera que el método “Save” del repositorio se haya llamado una vez.

Debemos tener en cuenta que, toda configuración que hagamos sobre el doble de prueba, debe de hacerse antes de ejecutar el código que lo ejercita (en este caso userService->Create(user)).

TEST_F(UserServiceTests, Saves_User) { //ARRANGE std::shared_ptr<User> user = std::make_shared<User>(1); ON_CALL(*userRepository, Exist(user)) .WillByDefault(testing::Return(false)); //EXPECT EXPECT_CALL(*userRepository, Save(user)) .Times(testing::Exactly(1)); //ACT userService->Create(user); }

Por esta razón la convección Arrange-Expect-Act, encaja mejor con el diseño de pruebas unitarias en este lenguaje, o por lo menos, con GTests y gMock.

ON_CALL VS EXPECT_CALL

En el contexto de gMock, ON_CALL es poco utilizado en comparación con EXPECT_CALL. Ambos se utilizan para definir el comportamiento de un doble de pruebas, pero hay diferencias clave:

  • ON_CALL define el comportamiento de un método sin establecer expectativas en su invocación.

  • EXPECT_CALL define el comportamiento y además, establece expectativas sobre la cantidad, orden de las invocaciones y parámetros del método.

Aunque EXPECT_CALL parece más completo, puede ser contraproducente si se usa en exceso, ya que añade restricciones innecesarias al test. Esto puede dificultar el mantenimiento y la flexibilidad del código, ya que cualquier cambio en la implementación podría romper los tests.

Se recomienda usar ON_CALL por defecto y recurrir a EXPECT_CALL, sólo cuando sea necesario verificar que una llamada específica se realiza.

Comparadores personalizados (Custom Matchers)

Los comparadores (Matchers) se usan para probar si dos valores son iguales a la hora de llamar a un doble de pruebas. Es una práctica necesaria cuando escribimos pruebas unitarias.

EXPECT_CALL(*mockObj, Foo(::testing::Eq(42))) .WillOnce(::testing::Return(false));

En el ejemplo anterior, estamos comprobando que el método Foo se llama pasándole un parámetro que es exactamente 42, si esto se cumple, la función devolverá false cuando se le llame.

gMock define una serie de comparadores predefinidos en la librería que son muy útiles, pero en ocasiones no son lo suficientemente potentes.

En C++, es una práctica común asignar nombres alternativos a tipos existentes, usando la palabra reservada typedef, como por ejemplo el siguiente tipo definido en una librería de Windows:

typedef unsigned char BYTE;

Nótese que el tipo BYTE no deja de ser un tipo entero de 8 bits, que no puede contener valores negativos, pero la librería en lugar de usar unsigned char directamente usa BYTE.

Por esta razón y porque cada librería puede definir sus propios tipos de datos, debemos crear nuestros propios comparadores personalizados si queremos tener unas pruebas unitarias sólidas.

gMock nos permite definir nuestros propios comparadores de forma sencilla, lo único que debemos tener claro, es qué es lo que tenemos que comparar para afirmar que dos tipos de datos sean iguales.

MATCHER_P(IsOptionalEqualTo, expected_value, "") { return arg.has_value() && arg.value() == expected_value; }

El comparador personalizado anterior, nos permite afirmar que dos valores opcionales (std::optional) son iguales. Un ejemplo de uso podría ser el siguiente:

std::optional<std::string> expectedParam = "param_value"; EXPECT_CALL(*mockObj, Foo(IsOptionalEqualTo(expectedParam)))

Las palabras reservadas que nos permiten crear comparadores personalizados (MATCHER, MATCHER_P o MATCHER_P2) son macros, por lo que no se pueden usar dentro de funciones o clases. También debemos tener en cuenta que el cuerpo de nuestros comparadores deben ser funciones puras, que no dependan de nada externo para hacer las comparaciones y no produzcan efectos colaterales en su ejecución.

Acciones personalizadas (Custom Actions)

Las acciones (Actions), se usan para definir qué debe hacer un doble de pruebas cuando es ejecutado. Volviendo al ejemplo anterior donde hablábamos de los comparadores:

EXPECT_CALL(*mockObj, Foo(::testing::Eq(42))) .WillOnce(::testing::Return(false));

::testing::Eq es el comparador y ::testing::Return es la acción.

De igual forma que gMock define una serie de comparadores integrados en la librería, también define una serie de acciones que nos permiten cubrir la mayoría de escenarios en nuestros tests.

Sin embargo, en ocasiones esas acciones integradas en la librería, no son lo suficientemente potentes y deberemos crear nuestras propias acciones personalizadas.

Un caso común donde esta práctica es necesaria, ocurre cuando intentamos crear dobles de prueba para funciones que utilizan parámetros de salida (output parameters). En estos casos, al igual que con los comparadores, dichos parámetros no son tipos soportados por las acciones de la librería.

Supongamos que tenemos el siguiente doble de pruebas:

class FakeDataEncryptionService : public DataEncryptionService { public: MOCK_METHOD(bool, Decrypt, (const std::string& encryptedData, BYTE* decryptedData, DWORD* decryptedDataLen), (override)); };

El método Decrypt recibe tres parámetros, de los cuales, el primero es de entrada y los dos últimos son de salida (donde se almacenará la salida de la función).

Para trabajar con dobles de pruebas que usan parámetros de salida, gMock define una serie de acciones como pueden ser: SaveArg<N>(pointer), SaveArgPointee<N>(pointer), SetArgReferee<N>(value) o SetArgPointee<N>(value) que nos permite asignar o guardar valores de los parámetros de una función.

Por lo que podríamos interpretar que, para trabajar con nuestro doble de pruebas deberíamos de hacer algo como lo siguiente:

std::wstring encryptedData = L"enctypted_data"; BYTE* decryptedData = (BYTE*)("{id: 1}"); EXPECT_CALL(*encryptionService, Decrypt(encryptedData, ::testing::_, ::testing::_)) .WillOnce(testing::DoAll( ::testing::SetArgPointee<1>(decryptedData), ::testing::Return(true)));

El código anterior asigna el valor de la variable decryptedData al parámetro 1 (partiendo del índice 0), pero de igual forma que pasa con los comparadores, el tipo BYTE* no es un tipo que soporte directamente ::testing::SetArgPointee<N> y al ejecutar el test veríamos un error en la pantalla.

Error C2440 '=': cannot convert from 'const A' to 'BYTE'

El error nos dice que el tipo A no puede ser convertido a un tipo BYTE , el compilador no es capaz de inferir el tipo del argumento 1 de la función Decrypt, porque es un tipo que no conoce.

La acción SetArgPointee es equivalente a escribir:

*arg = param;

Siendo arg , un puntero al parámetro 1 de la función Decrypt , arg es un tipo de dato no inferible para el compilador.

La solución, al igual que con los comparadores, está en crear nuestra acción personalizada para trabajar con este tipo de dato:

ACTION_P(AssignDecryptedDataParam, param) { BYTE* destPtr = static_cast<BYTE*>(arg1); BYTE* sourcePtr = static_cast<BYTE*>(param); auto byteArrayLen = wcslen(reinterpret_cast<const wchar_t*>(sourcePtr)) * sizeof(wchar_t); memcpy(destPtr, sourcePtr, byteArrayLen); }

Nos aseguramos que tanto el argumento de la función con la que estamos trabajando, como el valor que queremos asignarle sean de tipo BYTE*, y finalmente copia los datos de una región de memoria a otra.

Este caso específico es algo más complejo de lo habitual, ya que un BYTE* no se puede asignar a otro BYTE* directamente, pero en resumidas cuentas debemos convertir el tipo manualmente para el compilador:

ACTION_P(AssignMyType, param) { *static_cast<MyType*>(arg1) = param; }

Finalmente, se reemplaza el uso de la acción integrada de gMock por nuestra acción personalizada:

std::wstring encryptedSerializedUser = L"enctypted_data"; BYTE* serializedUser = (BYTE*)("{id: 1}"); EXPECT_CALL(*encryptionService, Decrypt(encryptedSerializedUser, ::testing::_, ::testing::_)) .WillOnce(testing::DoAll( AssignDecryptedDataParam(serializedUser), ::testing::Return(true)));

Cómo probar código que usa callbacks

En su momento me costó encontrar la forma de hacer pruebas unitarias sobre código que usa callbacks, por esa razón, dejo este pequeño espacio reservado para ello.

Para ejemplificar esto vamos a suponer el siguiente caso práctico: una clase que actúa como centro de eventos. Esta clase se subscribe a diferentes eventos. Cuando un evento es recibido se envía una notificación push con el mensaje del evento. Cabe destacar que los eventos se reciben de forma asíncrona y descontrolada. Para capturar los eventos se usa un callback.

EventListener.h

#pragma once #include <string> class EventListener { public: virtual void ListenMessages( void (*handler)(void* context, const std::string& eventMessage), void* context) = 0; };

PushNotificationService.h

#pragma once #include <string> class PushNotificationService { public: virtual void Push(const std::string& message) = 0; };

EventsCenter.cpp (he omitido el archivo de cabeceras de esta clase)

#include "pch.h" #include "EventsCenter.h" static void MessageReceivedHandler(void* context, const std::string& message) { EventsCenter* self = static_cast<EventsCenter*>(context); self->PushMessage(message); }; EventsCenter::EventsCenter( std::shared_ptr<PushNotificationService> pushNotificationService, std::shared_ptr<EventListener> systemEventListener): _pushNotificationService(pushNotificationService), _systemEventListener(systemEventListener) {} void EventsCenter::SubscribeToEvents() { _systemEventListener->ListenMessages(MessageReceivedHandler, this); //_serverEventListener->Listen... //... } void EventsCenter::PushMessage(const std::string& message) { _pushNotificationService->Push(message); }

Para poder hacer una prueba unitaria para el código anterior, debemos capturar el primer argumento del método _systemEventListener->ListenMessages. Una vez lo tengamos, podemos usarlo en nuestro test para probar el flujo completo del código. Para poder hacer esto, gMock nos provee una acción llamada Invoke

Aquí el test:

//includes omitted. class EventsCenterTests : public ::testing::Test { protected: std::shared_ptr<FakePushNotificationService> pushNotificationService = std::make_shared<FakePushNotificationService>(); std::shared_ptr<FakeEventListener> systemEventListener = std::make_shared<FakeEventListener>(); EventsCenter* eventsCenter; void (*callBackHandler)(void* context, const std::string& notification) = nullptr; void* callBackContext = nullptr; void SetUp() override { eventsCenter = new EventsCenter(pushNotificationService, systemEventListener); } void TearDown() override { delete eventsCenter; } public: void captureListenMessagesCallBack( void (*handler)(void* context, const std::string& notification), void* context) { callBackHandler = handler; callBackContext = context; } }; TEST_F(EventsCenterTests, Sends_Push_Notification_When_System_Event_Received) { EXPECT_CALL(*systemEventListener, ListenMessages(testing::_, testing::_)) .WillOnce(testing::Invoke(this, &EventsCenterTests::captureListenMessagesCallBack)); eventsCenter->SubscribeToEvents(); std::string systemEventMessage = "Successfully scheduled Software Protection service for re-start at 2124-04-29T13:20:44Z. Reason: RulesEngine."; EXPECT_CALL(*pushNotificationService, Push(systemEventMessage)) .Times(::testing::Exactly(1)); callBackHandler(callBackContext, systemEventMessage); }

Reportes e integración en CI

Si hemos decidido incluir los test unitarios como parte de nuestra estrategia de testing, lo más probable es que queramos ejecutar las pruebas unitarias en un entorno de integración continua.

Los proyectos de tests de GTests se compilan a un binario (.exe en caso de Windows), este binario nos expone una CLI y es tan sencillo como ejecutar este binario desde la línea de comandos, para obtener el resultado de las pruebas.

MyTestBinary.exe --gtest_output="xml:report.xml"

El comando anterior ejecutará nuestro proyecto de pruebas unitarias y guardará un reporte en formato XML. Es importante mencionar que el reporte XML que se genera usa el formato JUnit-style XML, esto es bastante importante porque es el formato que usan la mayoría de los sistemas de CI como Gitlab Pipelines o Github actions para interpretar los reportes.

Un aspecto negativo de GTests es que no nos provee una manera de ejecutar de forma simultánea varios proyectos de pruebas. Es bastante común que nuestras pruebas unitarias se dividan en diferentes proyectos, lo que genera varios binarios. Esto lo podemos solventar fácilmente con un pequeño script que luego usemos en nuestro Pipeline. Aquí un ejemplo con Batch:

@echo off setlocal enabledelayedexpansion set test_folder=..\src\x64\Release\Testing\Unit set report_folder=reports if not exist %report_folder% mkdir %report_folder% for %%f in ("%test_folder%\*.exe") do ( echo Executing: %%f set "project_name=%%~nf" %%f --gtest_output="xml:%report_folder%\!project_name!-test-report.xml" ) endlocal

El directorio de reportes generado por el script se podría incluir en los reportes de nuestro Pipeline. Aquí un ejemplo con Gitlab Pipelines:

unit-tests-job: stage: tests script: - .\run-unit-tests.bat artifacts: when: always expire_in: 7 days paths: - reports reports: junit: reports\*.xml

Reportes de cobertura de test

Los reportes de cobertura de tests son métricas que calculan cuánto porcentaje de nuestro código tenemos cubierto por pruebas unitarias. Sistemas como SonarQube, usan estos reportes para determinar si una Merge Request pasa los estándares de calidad del equipo, o si simplemente podríamos necesitarlos para ser conscientes de la cantidad de código que tenemos sin probar.

GTests no ofrece ninguna herramienta para crear reportes de cobertura de tests, lo que es normal ya que esta funcionalidad escapa un poco del ámbito de las pruebas unitarias. Necesitamos una herramienta que sea capaz de analizar los archivos que conforman nuestro proyecto, y en base a nuestras pruebas unitarias, calcular el porcentaje de cobertura.

Herramientas hay varias, sobre todo para sistemas UNIX o proyectos que usen CMake. En este artículo, como los ejemplos los he hecho en Windows, hablaré de una herramienta para este entorno que casualmente es el menos documentado en internet.

OpenCppCoverage es una herramienta de código abierto, que nos genera reportes de cobertura de tests usando nuestras pruebas escritas con GTests. Usarla es tan sencillo como instalar en el sistema operativo y ejecutar el siguiente comando:

OpenCppCoverage.exe --sources src ^ --excluded_sources **\packages ^ --export_type cobertura:MyTestBinary.xml -- MyTestBinary.exe

El comando anterior ejecuta nuestro proyecto de pruebas unitarias (MyTestBinary.exe), y utiliza la carpeta src (la que hemos especificado en —sources), para analizar los archivos que confirman nuestro proyecto, y calcular el porcentaje de código que está cubierto por pruebas unitarias. Excluye de este análisis todos los archivos que estén dentro de packages, esto es muy importante. Debemos ser muy meticulosos con los archivos que usamos para calcular el porcentaje de cobertura, si incluimos en el análisis archivos que no forman parte de nuestra base de código (cómo dependencias de terceros), el porcentaje de cobertura calculado no será real.

Finalmente el reporte se guarda en un archivo XML.

Al igual que pasa con los binarios de GTests, OpenCppCoverage no es capaz de ejecutar múltiples binarios de pruebas, por lo que tendremos que generar un reporte de cobertura por cada proyecto de pruebas que tengamos. Para la integración con CI se puede hacer un script sencillo, parecido al que vimos cuando hablamos de reportes e integración en CI.

SonarQube soporta el formato del reporte que genera OpenCppCoverage desde la versión 10.2, lo que es genial si usamos este escáner en nuestro proyecto.

Como hacer pruebas unitarias sobre artefactos que NO están en librerías

Una duda común cuando empezamos a trabajar con C++ es: ¿Cómo hacer pruebas unitarias sobre artefactos que están en tipos de proyecto que no son librerías? Por ejemplo, una aplicación de consola.

Los proyectos de pruebas unitarias están pensados para hacer pruebas sobre librerías. Piezas de software que expongan el código de su interior a un consumidor. Sin embargo, las aplicaciones de tipo consola, por ejemplo, se compilan a un ejecutable (.exe en caso de Windows) y no es posible consumir el código de su interior.

Para solucionar este problema existen varias alternativas, pero la más adoptada es separar el código que se quiere probar a una librería que luego se enlace a la aplicación, la librería de la aplicación, podríamos llamarlo. De esta forma, tanto la propia aplicación como el proyecto de pruebas usarían la librería.

pruebas-unitarias-no-libs|690x339

Este hilo de StackOverflow explica muy bien este tema.

Gracias por leer. Un saludo.