Estoy empezando un pequeño proyecto en C++ y antes de nada me han surgido un par de preguntas:
-
¿Cómo voy a construirlo?
-
¿Qué framework para pruebas unitarias utilizar?
Tip
|
Si simplemente quieres comenzar un proyecto en C++, fácil de construir, con una librería y pruebas unitarias listas, simplemente visita el repositorio del proyecto de ejemplo https://github.com/carlosvin/uuid-cpp y sigue las instrucciones en el README.md. |
Elegir un Sistema de Construcción (Meson)
Ya he utilizado antes Make, Maven, Scons, Gradle y Autotools, pero tengo algunas razones para probar algo diferente, hay algunas cosas que no me gustan:
- Autotools
-
No es fácil de configurar y mantener: hay distintos ficheros de configuración y distintos pasos de configuración.
- Gradle
-
La construcción de proyectos CPP está todavía en desarrollo, los modelos y APIs están cambiando. No es muy rápido. Puedes ver un ejemplo en este artículo Construir un proyecto Cpp con Gradle.
- Make
-
A medida que el proyecto crece los archivos de configuración se van complicando y volviendo poco manejables. La sintáxis no me parece clara (esto es una custión de gustos).
- Scons
-
Es más lento y un poco más difícil de comprender que Meson.
- Maven
-
Es lento y puedes terminar "Javatizando" la estructura del proyecto.
Note
|
He nombrado solo las cosas que no gustan, pero estos sistemas de construcción tienen otras grandes virtudes, personalmente me encantan Gradle, Autotools y Maven (solo para projectos Java). |
CMake vs Meson
Después de descartar los anteriores, estoy considerando Meson y CMake. Los dos son bastante rápidos:
Aunque Meson está hecho en Python, simplemente genera projectos Ninja. La primera vez tenemos que ejecutar Meson para configurar el proyecto, el resto de ejecuciones para compilar o ejecutar pruebas, realmente estaremos ejecutando directamente Ninja.
CMake también puede generar proyectos Ninja entre otros formatos, mira la documentación "CMake generators".
- CMake
-
Tiene una gran ventaja sobre Meson, es mucho más maduro y es mucho más usado, lo que significa que podrás encontrar muy fácilmente ejemplos, documentación y ayuda en Internet. No importa el tipo de proyecto que estés empezando, lo más seguro es que CMake sea una buena elección.
- Meson
-
Es un proyecto jóven comparado con CMake, pero está creciendo rápido y ya ha sido adoptado por algunos proyectos importantes como Gnome, donde han comenzado una iniciativa para migrar desde Autotools a Meson.
Finalmente he elegido Meson porque:
-
La sintáxis es muy clara para mí, cuando leo un archivo meson.build entiendo rápidamente lo ue está pasando durante el proceso de construcción.
-
Es rápido, aunque está escrito en Python utiliza Ninja para construir el proyecto. La primera vez tienes que utilizar Meson para configurar el proyecto, pero para construir y probar el proyecto relmente estamos ejecutando Ninja.
$ meson build . (1)
$ cd build
$ ninja build (2)
$ ninja test (3)
-
Primera vez, configuración del proyecto
-
Cada vez que construyes el projecto
-
Cada vez que ejecutas tests
Framework the Pruebas Unitarias
Anteriorment he utilizado algunas librerías del tipo xUnit como UnitTest++, CppUTest o Google Test que encaja perfectamente con Google Mock.
Si quires una apuesta segura que cumpla tus expectativas, te recomiendo Google Test.
Pero hace algún tiempo encontré un framework de pruebas con algunas características no tan comunes en librerías de pruebas C++ y que resultaba realmente fácil de utilizar, estoy hablando de Catch:
-
Es simplemente un fichero de cabeceras C++ sin dependencias adicionales, por lo que resulta realmente rápido comenzar (wget y utilizar el fichero descargado desde tus pruebas).
-
Puedes utilizar el estilo normal de pruebas unitarias o el estilo BDD.
Si quieres saber más sobre Catch, te recomiendo que directamente lo pruebes, el siguiente ejemplo, es cuestión de dos minutos simple example up and running. Puedes también leer algunos artículos como Why do we need yet another C test framework?] o https://blog.coldflake.com/posts/Testing-C-with-a-new-Catch/[Testing C++ With A New Catch.
doctest: Una alternativa a Catch
Hay otro framework de pruebas llamado doctest, con los mismos principios que Catch, pero promete ser más rápido y ligero (resultados de las comparaciones de rendimiento) que Catch.
doctest fue diseñado basándose en los puntos fuertes de Catch, pero hay algunas diferencias.
No es fácil decidirse por uno, los dos son muy parecidos, puedes comprobar las diferencias a continuación:
@@ -1,12 +1,12 @@
-#define CATCH_CONFIG_MAIN // It tells Catch to provide a main() - only do this in one cpp file
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
-#include "catch.hpp"
+#include "doctest.h"
#include "Uuid.h"
#include <string>
constexpr int MAX_ITERS = 100;
-TEST_CASE("Uuid", "[uuid]")
+TEST_CASE("Uuid")
{
for (int i = 0; i < MAX_ITERS; i++)
{
@@ -26,7 +26,7 @@ TEST_CASE("Uuid", "[uuid]")
// BDD style
-SCENARIO("UUID creation", "[Uuid]")
+SCENARIO("UUID creation")
{
GIVEN("A random UUID ")
Finalmente he elegido doctest simplemente porque es más rápido: resultados de las comparaciones de rendimiento.
Note
|
He creado el proyecto de ejemplo utilizando ambos frameworks, puedes encontrarlos en diferentes ramas del repositorio: rama doctest or rama catch. |
Ejemplo
He creado un ejemplo para ilustrar este artículo: https://github.com/carlosvin/uuid-cpp.
Consiste en una implementación básica de un generador pseudo-aleatorio de UUID, está basado en mt19937 que no es criptográficamente seguro.
Artefactos del Proyecto
Cuando instalemos el proyecto, Meson (Ninja realmente) generará una serie de artefactos en nuestro sistema.
-
Librería compartida:
libuuid
. -
Fichero de cabeceras para que los desarrolladores puedan usar la librería:
include/Uuid.h
. -
Fichero ejecutable
uuidgen
(Generador de UUID). -
Ejecutable de las pruebas unitarias (no será instalado).
Si ejecutamos ninja install
en Linux obtendremos los siguientes ficheros:
/usr/local/lib/libuuid.so
/usr/local/include/Uuid.h
/usr/local/bin/uuidgen
Estructura del Proyecto (Fork project)
- meson.build
-
Fichero principal de configuración para construir el proyecto. Lo utilizamos para especificar las propiedades y subdirectorios del proyecto.
project(
'cpp-meson-example', # project name
'cpp', # C++ project, e.g: for C project
version : '1.0.0',
license : 'MIT',
default_options : ['cpp_std=c++11']) # compile for C++
# it will be referred from subdir projects
inc = include_directories('include')
# meson will try to find a meson.build file inside following directories
subdir('include')
subdir('src')
subdir('test')
- include
-
- meson.build
-
Archivo de configuración para construir este directorio, no hay mucho que hacer aquí, simplemente indicamos qué ficheros de cabeceras han de ser instalados
# Select header files to be installed
install_headers('Uuid.h')
- Uuid.h
-
Archivos de cabeceras, es el interfaz que expone la librería y que será incluido por los usuarios de la misma.
namespace ids {
class Uuid {
private:
// ...
- src
-
- meson.build (src)
-
Declara 2 artefactos de salida: La librería
libuuid
y el ejecutableuuidgen
.
libuuid = shared_library(
'uuid', (1)
'Uuid.cpp', (2)
include_directories : inc, (3)
install : true) (4)
uuidgen = executable(
'uuidgen', (5)
'main.cpp', (6)
include_directories : inc, (7)
link_with : libuuid, (8)
install : true) (9)
-
library name
-
source files to be compile
-
previously declared include directories in root
meson.build
-
libuuid
will be part of project installation -
executable name
-
source files to compile
-
previously declared include directories in root
meson.build
-
linking executable with shared previously declared shared library
libuuid
-
uuidgen
executable be part of project installation- main.cpp
-
Código fuente del ejecutable de la aplicación:
uuidgen
#include "Uuid.h"
#include <iostream>
int main()
{
ids::Uuid uuid;
std::cout << uuid.to_str() << std::endl;
return 0;
}
- Uuid.cpp
-
Implementación de la clase declarada en el fichero de cabeceras
Uuid.h
.
#include "Uuid.h"
Uuid::Uuid()
{ // ...
- test
-
- meson.build (test)
-
Archivo de configuración para construir y ejecutar las pruebas unitarias.
testexe = executable(
'testexe', (1)
'uuid_test.cpp', (2)
include_directories : inc, (3)
link_with : libuuid) (4)
test('Uuid test', testexe) (5)
test('Uuid test with args and env', testexe, args : ['arg1', 'arg2'], env : ['FOO=bar']) (6)
-
test executable name
-
tests source files to be compiled
-
declared include directories in root
meson.build
-
link test executable with previously declared shared library
libuuid
-
test execution
-
we can specify other test execution passing arguments or environment variables
- doctest.h
-
Librería doctest en un único fichero de cabeceras. Puedes tratar de automatizar el proceso de instalación de la librería, yo por el momento la he instalado manualmente, ya que es un proceso muy sencillo:
cd test
wget https://raw.githubusercontent.com/onqtam/doctest/master/doctest/doctest.h
- uuid_test.cpp
-
Implementación de las pruebas unitarias.
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"
#include "Uuid.h"
#include <string>
constexpr int MAX_ITERS = 100;
TEST_CASE("Uuid")
{
for (int i = 0; i < MAX_ITERS; i++)
{
ids::Uuid uuid;
std::string uuid_str{uuid.to_str()};
INFO(uuid_str);
// If assertion fails test execution is stopped
REQUIRE(uuid_str.size() == 36);
// If assertion fails test execution continues
CHECK(uuid.most > 0);
CHECK(uuid.least > 0);
}
}
// BDD style
SCENARIO("UUID creation")
{
GIVEN("A random UUID ")
{
ids::Uuid uuid;
std::string uuid_str{uuid.to_str()};
REQUIRE(uuid_str.size() == 36);
WHEN("get the most and least")
{
THEN("should be more than 0")
{
CHECK(uuid.most > 0);
CHECK(uuid.least > 0);
}
}
}
}
Tip
|
Puedes encontrar las instrucciones para construir y ejecutar el proyecto de ejemplo en: https://github.com/carlosvin/uuid-cpp#how-to-build-the-example |