Inicio: https://github.com/mity/acutest
Acutest es una instalación de pruebas unitarias de C/C++ que pretende ser lo más simple posible, no interponerse en el camino del desarrollador y minimizar cualquier dependencia externa.
Para lograrlo, la implementación completa reside en un único archivo de encabezado C, y su núcleo depende sólo de unas pocas funciones estándar de la biblioteca C.
Características principales:
acutest.h
.main()
).--tap
).--xml-output=FILE
).Características específicas de C++:
std::exception
, what()
se escribe en el mensaje de error.Características específicas de Unix/Posix:
clock_gettime()
), el usuario puede medir los tiempos de ejecución de las pruebas con --time=real
(igual que --time
) y --time=cpu
.Características específicas de Linux:
Características específicas de Windows:
--time
.Características específicas de macOS:
Cualquier módulo C/C++ que implemente una o más pruebas unitarias e incluya acutest.h
se puede crear como un programa independiente. Al binario resultante lo denominamos "conjunto de pruebas" para los fines de este documento. Luego se ejecuta la suite para ejecutar las pruebas, según lo especificado en sus opciones de línea de comando.
Decimos que cualquier prueba unitaria tiene éxito si y sólo si:
Para usar Acutest, simplemente incluya el archivo de encabezado acutest.h
al comienzo del archivo fuente C/C++ que implementa una o más pruebas unitarias. Tenga en cuenta que el encabezado proporciona la implementación de la función main()
.
#include "acutest.h"
Se supone que cada prueba se implementa como una función con el siguiente prototipo:
void test_example ( void );
Las pruebas pueden utilizar algunas macros de preprocesador para validar las condiciones de la prueba. Se pueden utilizar varias veces y, si alguna de esas condiciones falla, se considera que la prueba en particular falla.
TEST_CHECK
es la macro de prueba más utilizada que simplemente prueba una condición booleana y falla si la condición se evalúa como falsa (o cero).
Por ejemplo:
void test_example ( void )
{
void * mem ;
int a , b ;
mem = malloc ( 10 );
TEST_CHECK ( mem != NULL );
mem = realloc ( mem , 20 );
TEST_CHECK ( mem != NULL );
}
Tenga en cuenta que las pruebas deben ser completamente independientes entre sí. Siempre que se invoca el conjunto de pruebas, el usuario puede ejecutar cualquier cantidad de pruebas en el conjunto, en cualquier orden. Además, de forma predeterminada, en las plataformas donde se admite, cada prueba unitaria se ejecuta como un (sub)proceso independiente.
Finalmente, el archivo fuente del conjunto de pruebas debe enumerar las pruebas unitarias, utilizando la macro TEST_LIST
. La lista especifica el nombre de cada prueba (debe ser única) y un puntero a una función que implementa la prueba. Recomiendo nombres que sean fáciles de usar en la línea de comandos: evite especialmente los espacios y otros caracteres especiales que puedan requerir escape en el shell; También evite el guión ( -
) como primer carácter del nombre, ya que podría interpretarse como una opción de línea de comando y no como un nombre de prueba.
TEST_LIST = {
{ "example" , test_example },
...
{ NULL, NULL } /* zeroed record marking the end of the list */
};
Tenga en cuenta que la lista de pruebas debe finalizar con un registro puesto a cero.
Para un conjunto de pruebas básico, esto es más o menos todo lo que necesita saber. Sin embargo, Acutest proporciona algunas macros más que pueden resultar útiles en algunas situaciones específicas. Los cubrimos en las siguientes subsecciones.
Hay una macro TEST_ASSERT
que es muy similar a TEST_CHECK
pero, si falla, aborta instantáneamente la ejecución de la prueba unitaria actual.
Por ejemplo:
void test_example ( void )
{
void * mem ;
int a , b ;
mem = malloc ( 10 );
TEST_ASSERT ( mem != NULL );
mem = realloc ( mem , 20 );
TEST_ASSERT ( mem != NULL );
}
El aborto en caso de falla se realiza llamando abort()
(si la prueba se ejecuta como un proceso hijo) o mediante longjmp()
(si no lo es).
Por lo tanto, sólo debe utilizarse si se comprenden los costes relacionados con un aborto tan brutal de la prueba. Dependiendo de lo que haga su prueba unitaria, puede incluir descriptores de archivos no eliminados, pérdidas de memoria, objetos C++ destruidos sin que se llame a sus destructores y más.
En general, se debe preferir TEST_CHECK
a TEST_ASSERT
, a menos que sepa exactamente qué hace y por qué eligió TEST_ASSERT
en alguna situación particular.
Para C++, hay una macro adicional TEST_EXCEPTION
para verificar que el código dado (normalmente solo una función o una llamada a un método) arroja el tipo de excepción esperado.
La verificación falla si la función no arroja ninguna excepción o si arroja algo incompatible.
Por ejemplo:
void test_example ( void )
{
TEST_EXCEPTION ( CallSomeFunction (), std:: exception );
}
Si falla una verificación de condición, suele ser útil proporcionar información adicional sobre la situación para que el problema sea más fácil de depurar. Acutest proporciona las macros TEST_MSG
y TEST_DUMP
para este propósito.
El primero genera cualquier mensaje tipo printf
, el otro genera un volcado hexadecimal de un bloque de memoria proporcionado.
Así por ejemplo:
void test_example ( void )
{
char produced [ 100 ];
char expected [] = "Hello World!" ;
SomeSprintfLikeFunction ( produced , "Hello %s!" , "world" );
TEST_CHECK ( strcmp ( produced , expected ) == 0 );
TEST_MSG ( "Expected: %s" , expected );
TEST_MSG ( "Produced: %s" , produced );
/* Or, if the function could provide some binary stuff, we might rather
* use TEST_DUMP instead in order to output a hexadecimal dump of the data.
*/
TEST_DUMP ( "Expected:" , expected , strlen ( expected ));
TEST_DUMP ( "Produced:" , produced , strlen ( produced ));
}
Tenga en cuenta que ambas macros generan algo sólo cuando la macro de comprobación más reciente ha fallado. En otras palabras, estos dos son equivalentes:
if (! TEST_CHECK ( some_condition != 0 ))
TEST_MSG ( "some message" );
TEST_CHECK ( some_condition != 0 );
TEST_MSG ( "some message" );
(Tenga en cuenta que TEST_MSG
requiere el compilador con soporte para macros variadas).
A veces, resulta útil diseñar la función de prueba como un bucle sobre datos que proporciona una colección de vectores de prueba y sus respectivos resultados esperados. Por ejemplo, imagine que se supone que nuestra prueba unitaria verifica algún tipo de implementación de función hash y tenemos una colección de vectores de prueba para ello en la especificación hash.
En tales casos, es muy útil obtener algún nombre asociado con cada vector de prueba y generar el nombre en el registro de salida para que, si falla alguna verificación, sea fácil identificar el vector de prueba culpable. Sin embargo, el cuerpo del bucle puede ejecutar docenas de macros de verificación, por lo que puede resultar poco práctico agregar dicho nombre para personalizar cada mensaje de verificación en el bucle.
Para solucionar esto, Acutest proporciona la macro TEST_CASE
. La macro especifica una cadena que sirve como nombre del vector de prueba. Cuando se utiliza, Acutest se asegura de que en el registro de salida el nombre proporcionado preceda a cualquier mensaje de verificaciones de condición posteriores.
Por ejemplo, supongamos que estamos probando SomeFunction()
que se supone, para una matriz de bytes determinada de cierto tamaño, devuelve otra matriz de bytes en un búfer recién editado malloc
. Entonces podemos hacer algo como esto:
struct TestVector {
const char * name ;
const uint8_t * input ;
size_t input_size ;
const uint8_t * expected_output ;
size_t expected_output_size ;
};
struct TestVector test_vectors [] = {
/* some data */
};
void test_example ( void )
{
int i ;
const uint8_t * output ;
size_t output_size ;
for ( i = 0 ; i < sizeof ( test_vectors ) / sizeof ( test_vectors [ 0 ]); i ++ ) {
struct TestVector * vec = & test_vectors [ i ];
/* Output the name of the tested test vector. */
TEST_CASE ( vec . name );
/* Now, we can check the function produces what it should for the
* current test vector. If any of the following checking macros
* produces any output (either because the check fails, or because
* high `--verbose` level is used), Acutest also outputs the currently
* tested vector's name. */
output = SomeFunction ( vec -> input , vec -> input_size , & output_size );
if ( TEST_CHECK ( output != NULL )) {
TEST_CHECK ( output_size == vec -> expected_output_size );
TEST_CHECK ( memcmp ( output , vec -> expected_output , output_size ) == 0 );
free ( output );
}
}
}
El nombre especificado se aplica a todas las comprobaciones ejecutadas después del uso de TEST_CASE
TEST_CASE
se use nuevamente para especificar otro nombre; oTEST_CASE
con NULL
como argumento.Muchas de las macros mencionadas en las secciones anteriores tienen una contraparte que permite generar mensajes personalizados en lugar de algunos predeterminados.
Todas ellas tienen el mismo nombre que las macros antes mencionadas, sólo que con el sufijo de subrayado añadido. Con el sufijo, esperan un formato de cadena tipo printf
y los argumentos adicionales correspondientes.
Así, por ejemplo, en lugar de las simples macros de comprobación
TEST_CHECK (a == b);
TEST_ASSERT (x < y);
TEST_EXCEPTION (SomeFunction(), std::exception);
Podemos usar sus respectivas contrapartes con mensajes personalizados:
TEST_CHECK_ (a == b, " %d is equal to %d " , a, b);
TEST_ASSERT_ (x < y, " %d is lower than %d " , x, y);
TEST_EXCEPTION_ (SomeFunction(), std::exception, "SomeFunction() throws std::exception");
Debería utilizar alguna redacción neutral para ellos porque, con la opción de línea de comando --verbose
, los mensajes se cierran incluso si la verificación respectiva pasa con éxito.
(Si necesita generar alguna información de diagnóstico en caso de que la verificación falle, use la macro TEST_MSG
. Ese es exactamente su propósito).
De manera similar, en lugar de
TEST_CASE ( "name" );
podemos usar más rico
TEST_CASE_ ( "iteration #%d" , 42 );
Sin embargo, tenga en cuenta que todos estos sólo se pueden utilizar si su compilador admite macros de preprocesador variadas. Las macros variadas se convirtieron en una parte estándar del lenguaje C con C99.
Cuando hayamos terminado con la implementación de las pruebas, simplemente podemos compilarlo como un programa C/C++ simple. Por ejemplo, suponiendo que cc
es su compilador de C:
$ cc test_example.c -o test_example
Cuando se compila el conjunto de pruebas, el binario de prueba resultante se puede utilizar para ejecutar las pruebas.
El código de salida del conjunto de pruebas es 0 si todas las pruebas unitarias ejecutadas pasan, 1 si alguna de ellas falla o cualquier otro número si ocurre un error interno.
De forma predeterminada (sin opciones de línea de comando), ejecuta todas las pruebas unitarias implementadas. También puede ejecutar solo un subconjunto de pruebas unitarias como se especifica en la línea de comando:
$ ./test_example # Runs all tests in the suite
$ ./test_example test1 test2 # Runs only tests specified
$ ./test_example --exclude test3 # Runs all tests but those specified
Tenga en cuenta que un único argumento de línea de comando puede seleccionar un grupo completo de unidades de prueba porque Acutest implementa varios niveles de selección de pruebas unitarias (se utiliza el primero que coincida con al menos una unidad de prueba):
Coincidencia exacta : cuando el argumento coincide exactamente con el nombre completo de una prueba unitaria, solo se selecciona la prueba dada.
Coincidencia de palabras : cuando el argumento no coincide con ningún nombre de prueba completo, pero sí coincide con la palabra completa en uno o más nombres de prueba, se seleccionan todas esas pruebas.
Los siguientes caracteres se reconocen como delimitadores de palabras: espacio
, tabulador t
, guión -
, guión bajo _
, barra diagonal /
, punto .
, coma ,
, dos puntos :
, punto y coma ;
.
Coincidencia de subcadena : si ni siquiera la palabra coincidencia pudo seleccionar ninguna prueba, se seleccionan todas las pruebas con un nombre que contenga el argumento como subcadena.
Al adoptar una estrategia de nomenclatura de pruebas adecuada, esto permite al usuario ejecutar (u omitir si se usa --exclude
) fácilmente toda una familia de pruebas relacionadas con un único argumento de línea de comando.
Por ejemplo, considere el conjunto de pruebas test_example
que implementa las pruebas foo-1
, foo-2
, foomatic
, bar-1
y bar-10
:
$ ./test_example bar-1 # Runs only the test 'bar-1' (exact match)
$ ./test_example foo # Runs 'foo-1' and 'foo-2' (word match)
$ ./test_example oo # Runs 'foo-1', 'foo-2' and 'foomatic' (substring match)
$ ./test_example 1 # Runs 'foo-1' and 'bar-1' (word match)
Puede usar --list
o -l
para enumerar simplemente todas las pruebas unitarias implementadas por el conjunto de pruebas determinado:
$ ./test_example --list
Para ver la descripción de todas las opciones de línea de comando admitidas, ejecute el binario con la opción --help
:
$ ./test_example --help
P: ¿No se conocía este proyecto como "CUTest"?
R: Sí. Se le cambió el nombre porque se descubrió que el nombre original estaba demasiado sobrecargado.
P: ¿Necesito distribuir el archivo README.md
y/o LICENSE.md
?
R: No. El encabezado acutest.h
incluye la URL de nuestro repositorio, una nota de derechos de autor y los términos de la licencia del MIT en su interior. Siempre y cuando los dejes intactos, estamos completamente de acuerdo si solo agregas el encabezado a tu proyecto. Después de todo, nuestro objetivo principal es su uso simple y su naturaleza todo en uno.
Acutest está cubierto con una licencia MIT; consulte el archivo LICENSE.md
o el comienzo de acutest.h
para ver el texto completo.
El proyecto reside en github:
Puede encontrar la última versión de Acutest allí, contribuir con mejoras o informar errores.