integración continua para software científico

22

No soy ingeniero de software. Soy un estudiante de doctorado en el campo de la geociencia.

Hace casi dos años comencé a programar un software científico. Nunca utilicé la integración continua (CI), principalmente porque al principio no sabía que existía y era la única persona que trabajaba en este software.

Ahora, dado que la base del software se está ejecutando, otras personas comienzan a interesarse en él y desean contribuir al software. El plan es que otras personas en otras universidades estén implementando adiciones al software central. (Tengo miedo de que puedan introducir errores). Además, el software se volvió bastante complejo y se volvió cada vez más difícil de probar y también planeo continuar trabajando en él.

Debido a estas dos razones, ahora estoy cada vez más pensando en usar CI. Como nunca tuve una educación en ingeniería de software y nadie a mi alrededor ha oído hablar de CI (somos científicos, no programadores), me resulta difícil comenzar mi proyecto.

Tengo un par de preguntas en las que me gustaría obtener algunos consejos:

En primer lugar, una breve explicación de cómo funciona el software:

  • El software está controlado por un archivo .xml que contiene todas las configuraciones requeridas. Inicia el software simplemente pasando la ruta al archivo .xml como argumento de entrada y se ejecuta y crea un par de archivos con los resultados. Una sola carrera puede tomar ~ 30 segundos.

  • Es un software científico. Casi todas las funciones tienen múltiples parámetros de entrada, cuyos tipos son principalmente clases que son bastante complejas. Tengo varios archivos .txt con grandes catálogos que se utilizan para crear instancias de estas clases.

Ahora pasemos a mis preguntas:

  1. pruebas unitarias, pruebas de integración, pruebas de extremo a extremo? : Mi software ahora tiene alrededor de 30,000 líneas de código con cientos de funciones y ~ 80 clases. Me parece un poco extraño comenzar a escribir pruebas unitarias para cientos de funciones que ya están implementadas. Entonces pensé en simplemente crear algunos casos de prueba. Prepare 10-20 archivos .xml diferentes y deje que se ejecute el software. Supongo que esto es lo que se llama pruebas de extremo a extremo. A menudo leo que no deberías hacer esto, pero ¿tal vez está bien para empezar si ya tienes un software que funciona? ¿O es simplemente una tonta idea intentar agregar CI a un software que ya funciona?

  2. ¿Cómo se escriben las pruebas unitarias si los parámetros de la función son difíciles de crear? Supongo que tengo una función double fun(vector<Class_A> a, vector<Class_B>)y, por lo general, primero tendría que leer en varios archivos de texto para crear objetos de tipo Class_Ay Class_B. Pensé en crear algunas funciones ficticias, como Class_A create_dummy_object()sin leer en los archivos de texto. También pensé en implementar algún tipo de serialización . (No planeo probar la creación de los objetos de clase ya que solo dependen de múltiples archivos de texto)

  3. ¿Cómo escribir pruebas si los resultados son muy variables? Mi software utiliza grandes simulaciones monte-carlo y funciona de forma iterativa. Por lo general, tiene ~ 1000 iteraciones y en cada iteración, está creando ~ 500-20,000 instancias de objetos basados ​​en simulaciones monte-carlo. Si solo un resultado de una iteración es un poco diferente, las próximas iteraciones completas son completamente diferentes. ¿Cómo lidias con esta situación? Supongo que este es un gran punto en contra de las pruebas de extremo a extremo, ya que el resultado final es muy variable.

Cualquier otro consejo con CI es muy apreciado.

usuario7431005
fuente
1
¿Cómo sabe que su software funciona correctamente? ¿Puedes encontrar una manera de automatizar esa verificación para que puedas ejecutarla en cada cambio? Ese debería ser su primer paso al presentar CI a un proyecto existente.
Bart van Ingen Schenau
¿Cómo se aseguró de que su software produzca resultados aceptables en primer lugar? ¿Qué te asegura que realmente "funcione"? Las respuestas a ambas preguntas le darán mucho material para probar su software ahora y en el futuro.
Polygnome

Respuestas:

23

Probar el software científico es difícil, tanto por el tema complejo como por los procesos típicos de desarrollo científico (también conocido como hackearlo hasta que funcione, lo que generalmente no resulta en un diseño comprobable). Esto es un poco irónico considerando que la ciencia debería ser reproducible. Lo que cambia en comparación con el software "normal" no es si las pruebas son útiles (¡sí!), Sino qué tipos de pruebas son apropiadas.

Manejo de aleatoriedad: todas las ejecuciones de su software DEBEN ser reproducibles. Si utiliza las técnicas de Monte Carlo, debe permitir proporcionar una semilla específica para el generador de números aleatorios.

  • Es fácil olvidar esto, por ejemplo, cuando se usa la rand()función de C que depende del estado global.
  • Idealmente, un generador de números aleatorios se pasa como un objeto explícito a través de sus funciones. El randomencabezado de la biblioteca estándar de C ++ 11 hace esto mucho más fácil.
  • En lugar de compartir el estado aleatorio entre los módulos del software, he encontrado útil crear un segundo RNG que se siembra mediante un número aleatorio del primer RNG. Luego, si el número de solicitudes al RNG por parte del otro módulo cambia, la secuencia generada por el primer RNG permanece igual.

Las pruebas de integración están perfectamente bien. Son buenos para verificar que diferentes partes de su software se unen correctamente y para ejecutar escenarios concretos.

  • Como nivel de calidad mínimo "no se bloquea" ya puede ser un buen resultado de prueba.
  • Para obtener resultados más sólidos, también deberá verificar los resultados con respecto a alguna línea de base. Sin embargo, estos controles tendrán que ser algo tolerantes, por ejemplo, tener en cuenta los errores de redondeo. También puede ser útil comparar estadísticas de resumen en lugar de filas de datos completos.
  • Si comparar con una línea de base sería demasiado frágil, verifique que las salidas sean válidas y satisfagan algunas propiedades generales. Estos pueden ser generales ("las ubicaciones seleccionadas deben estar separadas por al menos 2 km") o específicas del escenario, por ejemplo, "una ubicación seleccionada debe estar dentro de esta área".

Al ejecutar pruebas de integración, es una buena idea escribir un corredor de prueba como un programa o script separado. Este corredor de prueba realiza la configuración necesaria, ejecuta el ejecutable que se va a probar, verifica los resultados y luego lo limpia.

Las comprobaciones de estilo de prueba de unidad pueden ser bastante difíciles de insertar en un software científico porque el software no ha sido diseñado para eso. En particular, las pruebas unitarias se vuelven difíciles cuando el sistema bajo prueba tiene muchas dependencias / interacciones externas. Si el software no está puramente orientado a objetos, generalmente no es posible burlarse de esas dependencias. He descubierto que es mejor evitar en gran medida las pruebas unitarias para dicho software, excepto las funciones matemáticas y las funciones de utilidad.

Incluso unas pocas pruebas son mejores que ninguna. Combinado con el cheque "tiene que compilar" que ya es un buen comienzo en la integración continua. Siempre puede volver y agregar más pruebas más tarde. Luego puede priorizar las áreas del código que tienen más probabilidades de romperse, por ejemplo, porque obtienen más actividad de desarrollo. Para ver qué partes de su código no están cubiertas por las pruebas unitarias, puede usar las herramientas de cobertura de código.

Pruebas manuales: especialmente para dominios con problemas complejos, no podrá probar todo automáticamente. Por ejemplo, actualmente estoy trabajando en un problema de búsqueda estocástica. Si pruebo que mi software siempre produce el mismo resultado, no puedo mejorarlo sin romper las pruebas. En cambio, he hecho que sea más fácil hacer pruebas manuales : ejecuto el software con una semilla fija y obtengo una visualizacióndel resultado (según sus preferencias, R, Python / Pyplot y Matlab facilitan la obtención de visualizaciones de alta calidad de sus conjuntos de datos). Puedo usar esta visualización para verificar que las cosas no salieron terriblemente mal. Del mismo modo, el seguimiento del progreso de su software a través de la salida de registro puede ser una técnica de prueba manual viable, al menos si puedo seleccionar el tipo de eventos que se registrarán.

amon
fuente
7

Me parece un poco extraño comenzar a escribir pruebas unitarias para cientos de funciones que ya están implementadas.

Querrá (típicamente) escribir las pruebas a medida que cambia dichas funciones. No necesita sentarse y escribir cientos de pruebas unitarias para las funciones existentes, eso sería (en gran medida) una pérdida de tiempo. El software (probablemente) funciona bien tal como está. El objetivo de estas pruebas es garantizar que los cambios futuros no rompan el comportamiento anterior. Si nunca cambia de nuevo una función en particular, probablemente nunca valga la pena tomarse el tiempo para probarla (ya que actualmente está funcionando, siempre ha funcionado y probablemente continuará funcionando). Recomiendo leer Trabajar eficazmente con código heredadopor Michael Feathers en este frente. Tiene algunas estrategias generales excelentes para probar cosas que ya existen, incluidas técnicas de ruptura de dependencia, pruebas de caracterización (copiar / pegar la función de salida en el conjunto de pruebas para garantizar que mantenga el comportamiento de regresión) y mucho más.

¿Cómo se escriben las pruebas unitarias si los parámetros de la función son difíciles de crear?

Idealmente, no lo haces. En cambio, hace que los parámetros sean más fáciles de crear (y, por lo tanto, hace que su diseño sea más fácil de probar). Es cierto que los cambios de diseño llevan tiempo, y estas refactorizaciones pueden ser difíciles en proyectos heredados como el suyo. TDD (Test Driven Development) puede ayudar con esto. Si los parámetros son muy difíciles de crear, tendrá muchos problemas para escribir pruebas en un estilo de prueba primero.

A corto plazo, usa simulacros, pero ten cuidado con el infierno burlón y los problemas que vienen con ellos a largo plazo. Sin embargo, a medida que crecí como ingeniero de software, me di cuenta de que los simulacros son casi siempre un mini olor que intentan resolver un problema mayor y no abordan el problema central. Me gusta referirme a él como "envoltura de turd", porque si pones un trozo de papel de aluminio en un poco de popó de perro en tu alfombra, todavía apesta. Lo que tienes que hacer es levantarte, recoger la caca, tirarla a la basura y luego sacar la basura. Obviamente, esto es más trabajo, y corre el riesgo de tener algo de materia fecal en sus manos, pero a la larga es mejor para usted y su salud. Si sigues envolviendo esas cacas, no querrás vivir mucho más en tu casa. Los simulacros son similares en naturaleza.

Por ejemplo, si tienes tu Class_Aque es difícil de crear instancias porque tienes que leer 700 archivos, entonces podrías burlarte de él. Lo siguiente que sabes es que tu simulacro se desactualiza, y lo real Class_A hace algo muy diferente al simulacro, y tus pruebas aún pasan, aunque deberían estar fallando. Una mejor solución es dividir Class_Aen componentes más fáciles de usar / probar , y probar esos componentes en su lugar. Tal vez escriba una prueba de integración que realmente llegue al disco y asegúrese de que Class_Afuncione como un todo. O tal vez solo tenga un constructor para Class_Aque pueda instanciar con una cadena simple (que representa sus datos) en lugar de tener que leer desde el disco.

¿Cómo escribir pruebas si los resultados son muy variables?

Un par de consejos:

1) Use inversas (o más generalmente, pruebas basadas en propiedades). ¿De qué se trata [1,2,3,4,5]? Ni idea. ¿Qué es ifft(fft([1,2,3,4,5]))? Debería estar [1,2,3,4,5](o cerca de él, pueden aparecer errores de coma flotante).

2) Utilice afirmaciones "conocidas". Si escribe una función determinante, puede ser difícil decir cuál es el determinante de una matriz de 100x100. Pero sí sabe que el determinante de la matriz de identidad es 1, incluso si es 100x100. También sabe que la función debería devolver 0 en una matriz no invertible (como un 100x100 lleno de todos los 0).

3) Use afirmaciones aproximadas en lugar de afirmaciones exactas . Hace un tiempo escribí un código que registraba dos imágenes al generar puntos de enlace que crean un mapeo entre las imágenes y hacen una deformación entre ellas para que coincidan. Podría registrarse en un nivel de subpíxel. ¿Cómo puedes probarlo? Cosas como:

EXPECT_TRUE(reg(img1, img2).size() < min(img1.size(), img2.size()))

Como solo puede registrarse en partes superpuestas, la imagen registrada debe ser más pequeña o igual a su imagen más pequeña) y también:

scale = 255
EXPECT_PIXEL_EQ_WITH_TOLERANCE(reg(img, img), img, .05*scale)

dado que una imagen registrada para sí misma debe estar CERCANA a sí misma, pero es posible que experimente un poco más de errores de coma flotante debido al algoritmo en cuestión, así que solo verifique que cada píxel esté con +/- 5% del rango válido (0-255 es un rango común, escala de grises). Al menos debe ser del mismo tamaño. Incluso puede simplemente hacer una prueba de humo (es decir, llamarlo y asegurarse de que no se cuelgue). En general, esta técnica es mejor para pruebas más grandes donde el resultado final no puede calcularse (fácilmente) a priori para ejecutar la prueba.

4) Use O ALMACENE una semilla de número aleatorio para su RNG.

Carreras no tienen que ser reproducibles. Sin embargo, es falso que la única forma de obtener una ejecución reproducible es proporcionar una semilla específica a un generador de números aleatorios. A veces las pruebas de aleatoriedad son valiosas . He visto errores en el código científico que surgen en casos degenerados que se generaron al azar . En lugar de llamar siempre a su función con la misma semilla, genere una semilla aleatoria , luego use esa semilla y registre el valor de la semilla. De esa manera, cada ejecución tiene una semilla aleatoria diferente , pero si se bloquea, puede volver a ejecutar el resultado utilizando la semilla que ha registrado para depurar. De hecho, he usado esto en la práctica y aplastó un error, así que pensé en mencionarlo.Desventaja: debe registrar sus ejecuciones de prueba. Al revés: corrección y eliminación de errores.

HTH.

Matt Messersmith
fuente
2
  1. Tipos de prueba

    • Me parece un poco extraño comenzar a escribir pruebas unitarias para cientos de funciones que ya están implementadas

      Piénselo al revés: si un parche que toca varias funciones rompe una de sus pruebas de extremo a extremo, ¿cómo va a averiguar cuál es el problema?

      Es mucho más fácil escribir pruebas unitarias para funciones individuales que para todo el programa. Es mucho más fácil asegurarse de tener una buena cobertura de una función individual. Es mucho más fácil refactorizar una función cuando está seguro de que las pruebas unitarias detectarán los casos de esquina que rompió.

      Escribir pruebas unitarias para funciones ya existentes es perfectamente normal para cualquiera que haya trabajado en una base de código heredada. Son una buena manera de confirmar su comprensión de las funciones en primer lugar y, una vez escritas, son una buena manera de encontrar cambios inesperados de comportamiento.

    • Las pruebas de extremo a extremo también valen la pena. Si son más fáciles de escribir, hágalos primero y agregue pruebas unitarias ad-hoc para cubrir las funciones que más le preocupan que otros rompan. No tienes que hacerlo todo de una vez.

    • Sí, agregar CI al software existente es razonable y normal.

  2. Cómo escribir pruebas unitarias

    Si sus objetos son realmente caros y / o complejos, escriba simulacros. Puede vincular las pruebas usando simulacros por separado de las pruebas usando objetos reales, en lugar de usar polimorfismo.

    De todos modos, debería tener alguna forma fácil de crear instancias, una función para crear instancias ficticias es común, pero también es sensato tener pruebas para el proceso de creación real.

  3. Resultados variables

    Debes tener algunas invariantes para el resultado. Pruebe esos, en lugar de un solo valor numérico.

    Podría proporcionar un generador de números pseudoaleatorios simulados si su código monte carlo lo acepta como parámetro, lo que haría que los resultados fueran predecibles al menos para un algoritmo conocido, pero es frágil a menos que literalmente devuelva el mismo número cada vez.

Inútil
fuente
1
  1. Nunca es una idea tonta agregar CI. Por experiencia, sé que este es el camino a seguir cuando tienes un proyecto de código abierto donde las personas son libres de contribuir. CI le permite evitar que las personas agreguen o cambien código si el código rompe su programa, por lo que es casi invaluable tener una base de código que funcione.

    Al considerar las pruebas, sin duda puede proporcionar algunas pruebas de extremo a extremo (creo que es una subcategoría de pruebas de integración) para asegurarse de que su flujo de código funciona como debería. Debe proporcionar al menos algunas pruebas unitarias básicas para asegurarse de que las funciones generen los valores correctos, ya que parte de las pruebas de integración pueden compensar otros errores cometidos durante la prueba.

  2. La creación de objetos de prueba es algo bastante difícil y laborioso. Tienes razón al querer hacer objetos ficticios. Estos objetos deberían tener algunos valores predeterminados, pero con valores límite, para los cuales ciertamente sabe cuál debería ser la salida.

  3. El problema con los libros sobre este tema es que el panorama de CI (y otras partes de devops) evoluciona tan rápido que cualquier cosa en un libro probablemente estará desactualizada unos meses más tarde. No conozco ningún libro que pueda ayudarte, pero Google debería, como siempre, ser tu salvador.

  4. Debe ejecutar sus pruebas usted mismo varias veces y hacer un análisis estadístico. De esa forma, puede implementar algunos casos de prueba en los que toma la mediana / promedio de múltiples ejecuciones y la compara con su análisis, para saber qué valores son correctos.

Algunos consejos:

  • Use la integración de las herramientas de CI en su plataforma GIT para evitar que el código roto entre en su base de código.
  • Detenga la fusión del código antes de que otros desarrolladores realicen la revisión por pares. Esto hace que los errores se conozcan más fácilmente y nuevamente evita que el código roto ingrese a su base de código.
Pelícano
fuente
1

En una respuesta antes de amon ya mencioné algunos puntos muy importantes. Déjame agregar un poco más:

1. Diferencias entre el desarrollo de software científico y software comercial

Para el software científico, el foco normalmente está en el problema científico, por supuesto. Los problemas consisten más en manejar los antecedentes teóricos, encontrar el mejor método numérico, etc. El software es solo una, más o menos, una pequeña parte del trabajo.

En la mayoría de los casos, el software está escrito por una o solo unas pocas personas. A menudo se escribe para un proyecto específico. Cuando finaliza el proyecto y se publica todo, en muchos casos el software ya no es necesario.

El software comercial generalmente es desarrollado por grandes equipos durante un período de tiempo más largo. Esto requiere mucha planificación para la arquitectura, el diseño, las pruebas unitarias, las pruebas de integración, etc. Esta planificación requiere una cantidad considerable de tiempo y experiencia. En un entorno científico, normalmente no hay tiempo para eso.

Si desea convertir su proyecto en un software similar al software comercial, debe verificar lo siguiente:

  • ¿Tienes el tiempo y los recursos?
  • ¿Cuál es la perspectiva a largo plazo del software? ¿Qué pasará con el software cuando termines tu trabajo y salgas de la universidad?

2. Pruebas de punta a punta

Si el software se vuelve cada vez más complejo y varias personas están trabajando en él, las pruebas son obligatorias. Pero como ya mencionó amon , agregar pruebas unitarias al software científico es bastante difícil. Entonces tienes que usar un enfoque diferente.

Como su software obtiene su entrada de un archivo, como la mayoría del software científico, es perfecto para crear varios archivos de entrada y salida de muestra. Debe ejecutar esas pruebas automáticamente en cada versión y comparar los resultados con sus muestras. Este podría ser un muy buen reemplazo para las pruebas unitarias. También obtienes pruebas de integración de esta manera.

Por supuesto, para obtener resultados reproducibles, debe usar la misma semilla para su generador de números aleatorios, como ya escribió amon .

Los ejemplos deben cubrir los resultados típicos de su software. Esto también debe incluir casos extremos de su espacio de parámetros y algoritmos numéricos.

Debe intentar encontrar ejemplos que no necesiten demasiado tiempo para ejecutarse, pero que aún cubran los casos de prueba típicos.

3. Integración continua

Como ejecutar los ejemplos de prueba puede llevar algún tiempo, creo que la integración continua no es factible. Probablemente tendrá que discutir las partes adicionales con sus colegas. Por ejemplo, tienen que coincidir con los métodos numéricos utilizados.

Así que creo que es mejor hacer la integración de una manera bien definida después de discutir los antecedentes teóricos y los métodos numéricos, las pruebas cuidadosas, etc.

No creo que sea una buena idea tener algún tipo de automatismo para la integración continua.

Por cierto, ¿estás usando un sistema de control de versiones?

4. Probar sus algoritmos numéricos

Si está comparando resultados numéricos, por ejemplo, cuando verifica los resultados de su prueba, no debe verificar la igualdad de los números flotantes. Siempre puede haber errores de redondeo. En cambio, verifique si la diferencia es inferior a un umbral específico.

También es una buena idea verificar sus algoritmos con diferentes algoritmos o formular el problema científico de una manera diferente y comparar los resultados. Si obtiene los mismos resultados utilizando dos o más formas independientes, esta es una buena indicación de que su teoría y su implementación son correctas.

Puede hacer esas pruebas en su código de prueba y usar el algoritmo más rápido para su código de producción.

bernie
fuente
0

Mi consejo sería elegir cuidadosamente cómo gastar sus esfuerzos. En mi campo (bioinformática), los algoritmos de última generación cambian tan rápidamente que gastar energía en la prueba de errores de su código podría gastarse mejor en el algoritmo mismo.

Dicho esto, lo que se valora es:

  • ¿Es el mejor método en ese momento, en términos de algoritmo?
  • qué tan fácil es portar a diferentes plataformas de cómputo (diferentes entornos HPC, versiones de sistema operativo, etc.)
  • robustez: ¿se ejecuta en MI conjunto de datos?

Su instinto de construir una base de código a prueba de balas es noble, pero vale la pena recordar que este no es un producto comercial. Hágalo lo más portátil posible, a prueba de errores (para su tipo de usuario), conveniente para que otros contribuyan, luego concéntrese en el algoritmo mismo

pez globo
fuente