Pruebas C sin SMS y registro

Cortador de pantalla Recientemente, zerocost escribió un artículo interesante, "Pruebas en C ++ sin macros y memoria dinámica" , que analiza un marco minimalista para probar el código C ++. El autor (casi) logró evitar el uso de macros para registrar las pruebas, pero en lugar de ellas aparecieron plantillas "mágicas" en el código, que personalmente me parecen, lo siento, inimaginablemente feas. Después de leer el artículo, tuve un vago sentimiento de insatisfacción, ya que sabía lo que podía hacerse mejor. No podía recordar de inmediato dónde, pero definitivamente vi el código de prueba, que no contiene un solo carácter adicional para registrarlos:


void test_object_addition() { ensure_equals("2 + 2 = ?", 2 + 2, 4); } 

Finalmente, recordé que este marco se llama Cutter y utiliza una forma genial para identificar las funciones de prueba a su manera.


(KDPV tomado del sitio web de Cutter bajo CC BY-SA.)


Cual es el truco


El código de prueba se ensambla en una biblioteca compartida separada. Las funciones de prueba se extraen de los símbolos de biblioteca exportados y se identifican por nombres. Las pruebas son realizadas por una utilidad externa especial. Sapienti se sentó.


 $ cat test_addition.c #include <cutter.h> void test_addition() { cut_assert_equal_int(2 + 2, 5); } 

 $ cc -shared -o test_addition.so \ -I/usr/include/cutter -lcutter \ test_addition.c 

 $ cutter . F ========================================================================= Failure: test_addition <2 + 2 == 5> expected: <4> actual: <5> test_addition.c:5: void test_addition(): cut_assert_equal_int(2 + 2, 5, ) ========================================================================= Finished in 0.000943 seconds (total: 0.000615 seconds) 1 test(s), 0 assertion(s), 1 failure(s), 0 error(s), 0 pending(s), 0 omission(s), 0 notification(s) 0% passed 

Aquí hay un ejemplo de la documentación de Cutter . Puede desplazarse de forma segura por todo lo relacionado con Autotools y mirar solo el código. El marco es un poco extraño, sí, como todo japonés.


No voy a entrar en demasiados detalles sobre las características de implementación. Tampoco tengo un código completo (e incluso al menos un borrador), ya que personalmente no lo necesito realmente (en Rust todo está listo para usar). Sin embargo, para las personas interesadas, este puede ser un buen ejercicio.


Detalles y opciones de implementación


Considere algunas de las tareas que necesita resolver al escribir un marco para probar usando el enfoque de Cutter.


Obteniendo funciones exportadas


Primero, necesita llegar a las funciones de prueba de alguna manera. El estándar C ++, por supuesto, no describe las bibliotecas compartidas en absoluto. Windows ha adquirido recientemente un subsistema Linux, que permite que los tres principales sistemas operativos se reduzcan a POSIX. Como sabe, los sistemas POSIX proporcionan las funciones dlopen() , dlsym() , dlclose() , con las que puede obtener la dirección de la función, conocer el nombre de su símbolo y ... eso es todo. POSIX no divulga la lista de funciones contenidas en la biblioteca cargada.


Desafortunadamente (aunque afortunadamente), no existe una forma estándar y portátil de descubrir todas las funciones exportadas desde la biblioteca. Quizás, el hecho de que el concepto de una biblioteca no existe en todas las plataformas (léase: incrustado) está de alguna manera involucrado aquí. Pero ese no es el punto. Lo principal es que tienes que usar funciones específicas de la plataforma.


Como una aproximación inicial, simplemente puede llamar a la utilidad nm :


 $ cat test.cpp void test_object_addition() { } 

 $ clang -shared test.cpp 

 $ nm -gj ./a.out __Z20test_object_additionv dyld_stub_binder 

analizar su salida y usar dlsym() .


Para una introspección más profunda, las bibliotecas como libelf , libMachO , pe-parse son útiles, ya que le permiten analizar mediante programación archivos ejecutables y bibliotecas de plataformas que le interesan. De hecho, nm y la compañía solo los usan.


Función de prueba de filtrado


Como habrás notado, las bibliotecas contienen algunos caracteres extraños:


 __Z20test_object_additionv dyld_stub_binder 

Esto es lo que __Z20test_object_additionv , cuando llamamos a la función solo test_object_addition ? ¿Y qué queda esto dyld_stub_binder ?


Los caracteres " __Z20... " __Z20... es la llamada decoración de nombre (cambio de nombre). Función de compilación C ++, no se puede hacer nada, convivir con ella. Esto es lo que se llaman funciones desde el punto de vista del sistema (y dlsym() ). Para mostrárselos a una persona en su forma normal, puede usar bibliotecas como libdemangle . Por supuesto, la biblioteca que necesita depende del compilador que utilice, pero el formato de decoración suele ser el mismo dentro del marco de la plataforma.


En cuanto a funciones extrañas como dyld_stub_binder , estas también son características de la plataforma que deberán tenerse en cuenta. No necesita llamar a ninguna función al comenzar las pruebas, ya que no hay peces allí.


Una continuación lógica de esta idea es filtrar la función por nombre. Por ejemplo, solo puede ejecutar funciones con test en el nombre. O simplemente funciones del espacio de nombres de tests . Y también use espacios de nombres anidados para agrupar pruebas. No hay límite para tu imaginación.


Pasando el contexto de una prueba ejecutable


Los archivos de objetos con pruebas se recopilan en una biblioteca compartida, cuya ejecución del código está completamente controlada por un controlador de utilidad externo - cutter for Cutter. En consecuencia, las funciones de prueba internas pueden usar esto.


Por ejemplo, el contexto de una prueba ejecutable ( IRuntime en el artículo original) se puede pasar de forma segura a través de una variable global (subproceso local). El conductor es responsable de gestionar y pasar el contexto.


En este caso, las funciones de prueba no requieren argumentos, pero conservan todas las características avanzadas, como la denominación arbitraria de los casos probados:


 void test_vector_add_element() { testing::description("vector size grows after push_back()"); } 

La función description() accede al IRuntime condicional a través de una variable global y, por lo tanto, puede pasar un comentario al marco para una persona. La seguridad del uso del contexto global está garantizada por el marco y no es responsabilidad del autor de la prueba.


Con este enfoque, habrá menos ruido en el código con la transferencia de contexto a las declaraciones de comparación y las funciones de prueba internas que pueden necesitarse desde la principal.


Constructores y destructores


Como la ejecución de las pruebas está completamente controlada por el controlador, puede ejecutar código adicional alrededor de las pruebas.


La biblioteca Cutter utiliza las siguientes funciones para esto:


  • cut_setup() - antes de cada prueba individual
  • cut_teardown() - después de cada prueba individual
  • cut_startup() - antes de ejecutar todas las pruebas
  • cut_shutdown() - después de completar todas las pruebas

Estas funciones solo se invocan si se definen en el archivo de prueba. Puede poner en ellos la preparación y limpieza del entorno de prueba (accesorio): crear los archivos temporales necesarios, la configuración compleja de los objetos probados y otros antipatrones de prueba.


Para C ++, es posible crear una interfaz más idiomática:


  • más orientado a objetos y tipo seguro
  • con mejor soporte de concepto RAII
  • usando lambdas para ejecución diferida
  • involucrando contexto de ejecución de prueba

Pero por ahora estoy pensando en esto nuevamente en detalle ahora.


Ejecutables de prueba independientes


Cutter utiliza un enfoque de biblioteca compartida para mayor comodidad. Se compilan varias pruebas en un conjunto de bibliotecas que una utilidad de prueba separada encuentra y ejecuta. Naturalmente, si lo desea, todo el código del controlador de prueba se puede incrustar directamente en el archivo ejecutable, obteniendo los archivos separados habituales. Sin embargo, esto requerirá la colaboración con el sistema de compilación para organizar el diseño de estos archivos ejecutables de la manera correcta: sin cortar las funciones "no utilizadas", con las dependencias correctas, etc.


Otros


Cutter y otros marcos también tienen muchas otras cosas útiles que pueden facilitar la vida al escribir pruebas:


  • declaraciones de prueba flexibles y extensibles
  • construir y obtener datos de prueba de archivos
  • estudios de seguimiento de pila, excepción y manejo de caída
  • "niveles de desglose" personalizables de pruebas
  • ejecutar pruebas en múltiples procesos

Vale la pena mirar hacia atrás en los marcos existentes al escribir su bicicleta. UX es un tema mucho más profundo.


Conclusión


El enfoque utilizado por el marco Cutter permite la identificación de funciones de prueba con una carga cognitiva mínima en el programador: simplemente escriba las funciones de prueba y eso es todo. El código no requiere el uso de plantillas o macros especiales, lo que aumenta su legibilidad.


Las características de ensamblar y ejecutar pruebas se pueden ocultar en módulos reutilizables para sistemas de ensamblaje como Makefile, CMake, etc. Las preguntas sobre un ensamblaje de pruebas por separado aún tendrán que hacerse de una forma u otra.


Las desventajas de este enfoque incluyen la dificultad de colocar pruebas en el mismo archivo (la misma unidad de traducción) que el código principal. Desafortunadamente, en este caso, sin sugerencias adicionales, ya no es posible determinar qué funciones deben iniciarse y cuáles no. Afortunadamente, en C ++ generalmente se acostumbra distribuir las pruebas y la implementación en diferentes archivos.


En cuanto a la disposición final de las macros, me parece que, en principio, no deberían abandonarse. Las macros permiten, por ejemplo, escribir declaraciones de comparación más cortas, evitando la duplicación de código:


 void test_object_addition() { ensure_equals(2 + 2, 5); } 

pero al mismo tiempo manteniendo el mismo contenido de información del problema en caso de errores:


 Failure: test_object_addition <ensure_equals(2 + 2, 5)> expected: <5> actual: <4> test.c:5: test_object_addition() 

El nombre de la función que se está probando, el nombre del archivo y el número de línea del inicio de la función en teoría se pueden extraer de la información de depuración contenida en la biblioteca que se recopila. La función ensure_equals() conoce el valor esperado y el real de las expresiones comparadas. La macro le permite "restaurar" la ortografía original de la declaración de prueba, de lo que queda más claro por qué se espera el valor 4 .


Sin embargo, esto no es para todos. ¿El beneficio de las macros para el código de prueba termina allí? Todavía no he pensado realmente en este momento, que puede resultar ser un buen campo para más perversiones investigación Una pregunta mucho más interesante: ¿es posible de alguna manera crear un marco simulado para C ++ sin macros?


El atento lector también señaló que realmente no hay SMS ni asbesto en la implementación, lo cual es una ventaja indudable para la ecología y la economía de la Tierra.

Source: https://habr.com/ru/post/es435028/


All Articles