Philip Withnall
philip.withnall@collabora.co.uk 2015
Diseñar software para ser probado y escribir pruebas unitarias para él
Daniel Mustieles
daniel.mustieles@gmail.com 2016-2020
Javier Mazorra
mazi.debian@gmail.com 2016, 2020

# Pruebas unitarias

#

        Resumen

        Las pruebas unitarias deben ser el método principal para probar la mayor parte del código escrito, porque una prueba unitaria puede escribirse una vez y ejecutarse muchas veces; las pruebas manuales deben planificarse una vez y luego ejecutarse manualmente cada vez.

        El desarrollo de las pruebas unitarias comienza con la arquitectura y el diseño de la API del código que se va a probar: el código debe diseñarse para que se pueda probar fácilmente, o será potencialmente muy difícil de probar.

          Escriba pruebas unitarias para que sean lo más pequeñas posible, pero no más pequeñas. ()
          Utilice herramientas de cobertura de código para escribir pruebas para obtener una cobertura de código alta. ()
          Ejecute todas las pruebas unitarias en Valgrind para comprobar si hay fugas y otros problemas. ()
          Utilice herramientas apropiadas para generar pruebas unitarias automáticamente donde sea posible. ()
          Diseñe el código para ser comprobable desde el principio. ()

# Escribir pruebas unitarias

Las pruebas unitarias deben escribirse junto con la observación de la información de cobertura obtenida de la ejecución de las pruebas. Por lo general, esto significa escribir un conjunto inicial de pruebas unitarias, ejecutarlas para obtener datos de cobertura, luego volver a trabajar y expandirlas para aumentar los niveles de cobertura del código. La cobertura debe aumentarse primero asegurándose de que todas las funciones están cubiertas (al menos en parte) y luego asegurándose de que todas las líneas de código están cubiertas. Al cubrir las funciones primero, los problemas de la API que evitarán la realización de pruebas efectivas se pueden encontrar rápidamente. Normalmente, se manifiestan como funciones internas a las que no se puede llamar fácilmente desde las pruebas unitarias. Habitualmente, se debe apuntar a niveles de cobertura superiores al 90%; no pruebe sólo los casos cubiertos por los requisitos del proyecto, pruebe todo.

Al igual que con git commits, cada prueba unitaria debería ser “tan pequeña como sea posible, pero no más pequeña”, probando una sola API o comportamiento. Cada caso de prueba debe ser capaz de ejecutarse individualmente, sin depender del estado de otros casos de prueba. Esto es importante para permitir la depuración de una prueba errónea sin tener que recorrer también todos los demás códigos de prueba. También significa que una única prueba errónea se puede rastrear fácilmente a una API específica, en lugar de un mensaje genérico de “las pruebas unitarias fallaron en algún lugar”.

GLib tiene soporte para pruebas unitarias con su GTest framework, que permite organizar las pruebas en grupos y jerarquías. Esto significa que los grupos de pruebas relacionadas también se pueden ejecutar juntos para una mejor depuración, ejecutando el binario de prueba con el argumento -p : ./test-suite-name -p /path/to/test/group.

# Pruebas instaladas

Todas las pruebas unitarias deberían instalarse en todo el sistema, siguiendo el estándar de pruebas instaladas.

Mediante la instalación de pruebas unitarias, la integración continua (IC) se hace más fácil, ya que las pruebas para un proyecto se pueden volver a ejecutar después de cambios en otros proyectos en el entorno IC, probando así las interfaces entre módulos. Esto es útil para un conjunto de proyecto altamente acoplados como GNOME.

Para añadir compatibilidad con las pruebas instaladas, añada lo siguiente a configure.ac:

# Installed tests AC_ARG_ENABLE([modular_tests], AS_HELP_STRING([--disable-modular-tests], [Disable build of test programs (default: no)]),, [enable_modular_tests=yes]) AC_ARG_ENABLE([installed_tests], AS_HELP_STRING([--enable-installed-tests], [Install test programs (default: no)]),, [enable_installed_tests=no]) AM_CONDITIONAL([BUILD_MODULAR_TESTS], [test "$enable_modular_tests" = "yes" || test "$enable_installed_tests" = "yes"]) AM_CONDITIONAL([BUILDOPT_INSTALL_TESTS],[test "$enable_installed_tests" = "yes"])

En el tests/Makefile.am:

insttestdir = $(libexecdir)/installed-tests/[project]

all_test_programs = \ test-program1 \ test-program2 \ test-program3 \ $(NULL) if BUILD_MODULAR_TESTS TESTS = $(all_test_programs) noinst_PROGRAMS = $(TESTS) endif

if BUILDOPT_INSTALL_TESTS insttest_PROGRAMS = $(all_test_programs)

testmetadir = $(datadir)/installed-tests/[project] testmeta_DATA = $(all_test_programs:=.test)

testdatadir = $(insttestdir) testdata_DATA = $(test_files)

testdata_SCRIPTS = $(test_script_files) endif

EXTRA_DIST = $(test_files)

%.test: % Makefile $(AM_V_GEN) (echo '[Test]' > $@.tmp; \ echo 'Type=session' >> $@.tmp; \ echo 'Exec=$(insttestdir)/$<' >> $@.tmp; \ mv $@.tmp $@)

# Comprobación de fugas

Una vez se han escrito pruebas unitarias con alta cobertura de código, se pueden ejecutar bajo varias herramientas de análisis dinámico, como Valgrind para comprobar si hay fugas, errores de ejecución de hilos, problemas de asignación, etc. en toda la base del código. Cuanto mayor sea la cobertura del código de las pruebas unitarias, más fiables serás los resultados de Valgrind. Véase para más información, incluyendo las instrucciones de integración de sistemas de compilación.

Críticamente, esto significa que las pruebas unitarias no deberían perder memoria u otros recursos, y de manera similar no deberían tener problemas de ejecución de hilos. Todos estos problemas serían efectivamente falsos positivos en el análisis del código real del proyecto. (Los falsos positivos que deben corregirse arreglando las pruebas unitarias).

# Generación de pruebas

Ciertos tipos de código son bastante repetitivos y requieren muchas pruebas unitarias para obtener una buena cobertura; pero son apropiados para la generación de datos de prueba, donde se utiliza una herramienta para generar automáticamente vectores de prueba para el código. Esto puede reducir drásticamente el tiempo necesario para escribir pruebas unitarias para el código en esto dominios específicos.

## JSON

Un ejemplo de dominio dispuesto para probar la generación de datos es el análisis, donde los datos que se van a analizar deben seguir un esquema estricto — este es el caso de los documentos XML y JSON. Para JSON una herramienta como Walbottle se puede utilizar para generar vectores de prueba para todo tipo de entradas válidas y no válidas de acuerdo con el esquema.

Cada tipo de documento JSON debe tener un esquema JSON definido para él, que luego puede pasarse a Walbottle para generar vectores de prueba:

json-schema-generate --valid-only schema.json json-schema-generate --invalid-only schema.json

Estos vectores de prueba pueden usarse al código que se está probando en sus pruebas unitarias. Las instancias JSON generadas por —valid-only se deben aceptar; las de —invalid-only se deben rechazar.

# Escribir código comprobable

El código se debe escribir con la capacidad de prueba en mente desde la fase de diseño, ya que afecta al diseño y a la arquitectura de la API de manera fundamental. Algunos principios clave:

No utilice el estado global. Los objetos Singleton generalmente son una mala idea, ya que no pueden instanciarse por separado ni controlarse en las pruebas unitarias.

Separe el uso del estado externo, como bases de datos, redes o el sistema de archivos. Las pruebas unitarias pueden reemplazar los accesos al estado externo con objetos simulados. Un enfoque común a esto es usar la inyección de dependencias para pasar un objeto contenedor del sistema de archivos al código que se está probando. Por ejemplo, una clase no debería cargar una base de datos global (desde una ubicación fija en el sistema de archivos) porque las pruebas unitarias podrían sobrescribir la copia de la base de datos del sistema en ejecución y nunca podrían ejecutarse en paralelo. Se les debe pasar un objeto que proporcione una interfaz a la base de datos: en un sistema en producción, esto sería una envoltura delgada alrededor de la API de la base de datos; para las pruebas, sería un objeto simulado que verifica las solicitudes que recibe y devuelve respuestas codificadas para varias pruebas.

Exponga las funciones de utilidad donde puedan ser generalmente útiles.

Divida los proyectos en colecciones de pequeñas bibliotecas privadas que luego se unen con una cantidad mínima de código de pegamento en el ejecutable general. Cada una se puede probar por separado.

# Enlaces externos

El tema de la comprobabilidad se cubre en los siguientes artículos:

Diseño para la comprobabilidad

Verificación de software

Inyección de dependencias

Diseño de software para pruebas