Estoy investigando técnicas y estrategias para escalar nuestro creciente número de pruebas de integración en nuestro producto actual, para que puedan (humanamente) seguir siendo parte de nuestro proceso de desarrollo y CI.
En más de 200 pruebas de integración, ya estamos alcanzando la marca de 1 hora para completar una ejecución de prueba completa (en una máquina de desarrollo de escritorio), y esto está afectando negativamente la capacidad de un desarrollador para tolerar la ejecución de todo el conjunto como parte de los procesos de inserción rutinarios. Lo que está afectando la motivación para ser disciplinado sobre cómo crearlos bien. Probamos la integración solo en escenarios clave de adelante hacia atrás, y utilizamos un entorno que refleja la producción, que se construye desde cero en cada ejecución de prueba.
Debido al tiempo que lleva ejecutar, está generando un ciclo de retroalimentación terrible y muchos ciclos desperdiciados esperando que las máquinas terminen las ejecuciones de prueba, sin importar cuán enfocadas estén las ejecuciones de prueba. No importa el impacto negativo más costoso en el flujo y el progreso, la cordura y la sostenibilidad.
Esperamos tener 10 veces más pruebas de integración antes de que este producto comience a ralentizarse (en realidad, no tengo idea, pero parece que aún no estamos comenzando en términos de características). Tenemos que esperar razonablemente estar en los pocos cientos o miles de pruebas de integración, supongo que en algún momento.
Para ser claros, para tratar de evitar que esto se convierta en una discusión sobre las pruebas unitarias versus las pruebas de integración (que nunca deberían intercambiarse). Estamos realizando pruebas unitarias con TDD Y pruebas de integración en este producto. De hecho, hacemos pruebas de integración en las diversas capas de la arquitectura de servicios que tenemos, donde tiene sentido para nosotros, ya que necesitamos verificar dónde introducimos cambios importantes al cambiar los patrones de nuestra arquitectura a las otras áreas del sistema.
Un poco sobre nuestra pila tecnológica. Actualmente estamos probando en un entorno de emulación (CPU y memoria intensiva) para ejecutar nuestras pruebas de principio a fin. Que se compone de los servicios web REST de Azure frente a un back-end noSql (ATS). Estamos simulando nuestro entorno de producción al ejecutar el emulador de escritorio Azure + IISExpress. Estamos limitados a un emulador y un repositorio backend local por máquina de desarrollo.
También tenemos un CI basado en la nube, que ejecuta la misma prueba en el mismo entorno emulado, y las ejecuciones de prueba están tardando el doble (2 horas +) en la nube con nuestro proveedor actual de CI. Hemos alcanzado los límites del SLA de los proveedores de CI en la nube en términos de rendimiento de hardware, y excedimos su límite de tiempo de ejecución de prueba. Para ser justos con ellos, sus especificaciones no son malas, pero claramente la mitad de buenas que una máquina de escritorio desagradable.
Estamos utilizando una estrategia de prueba para reconstruir nuestro almacén de datos para cada grupo lógico de pruebas y precargar con datos de prueba. Si bien asegura integralmente la integridad de los datos, esto agrega un impacto del 5-15% en cada prueba. Por lo tanto, creemos que hay poco que ganar al optimizar esa estrategia de prueba en este punto del desarrollo del producto.
En resumidas cuentas, si bien podríamos optimizar el rendimiento de cada prueba (aunque sea entre un 30% y un 50% cada una), todavía no escalaremos de manera efectiva en el futuro cercano con varios cientos de pruebas. 1 hora ahora es aún mucho más que tolerable para el ser humano, necesitamos un orden de mejora de magnitud en el proceso general para que sea sostenible.
Por lo tanto, estoy investigando qué técnicas y estrategias podemos emplear para reducir drásticamente el tiempo de prueba.
- Escribir menos pruebas no es una opción. No discutamos ese tema en este hilo.
- Usar hardware más rápido es definitivamente una opción, aunque muy costosa.
- Ejecutar grupos de pruebas / escenarios en hardware separado en paralelo también es definitivamente una opción preferida.
- Crear grupos de pruebas en torno a características y escenarios en desarrollo es plausible, pero en última instancia no es confiable para probar la cobertura total o la confianza de que el sistema no se ve afectado por un cambio.
- Es técnicamente posible ejecutar en un entorno de ensayo a escala de la nube en lugar de ejecutarse en el emulador de escritorio, aunque comenzamos a agregar tiempos de implementación a las ejecuciones de prueba (~ 20 minutos cada una al comienzo de la ejecución de prueba para implementar las cosas).
- Dividir los componentes del sistema en piezas logiales independientes es plausible hasta cierto punto, pero esperamos un kilometraje limitado, ya que se espera que las interacciones entre los componentes aumenten con el tiempo. (es decir, es probable que un cambio afecte a otros en formas inesperadas, como sucede a menudo cuando un sistema se desarrolla de forma incremental)
Quería ver qué estrategias (y herramientas) están usando otros en este espacio.
(Tengo que creer que otros pueden estar viendo este tipo de dificultad al usar ciertos conjuntos de tecnología).
[Actualización: 16/12/2016: Terminamos invirtiendo más en pruebas paralelas de CI, para una discusión del resultado: http://www.mindkin.co.nz/blog/2015/12/16/16-jobs]
fuente
Respuestas:
Trabajé en un lugar que tomó 5 horas (en 30 máquinas) para ejecutar pruebas de integración. Refactoré la base de código e hice pruebas unitarias para las cosas nuevas. Las pruebas unitarias duraron 30 segundos (en 1 máquina). Ah, y los errores también cayeron. Y tiempo de desarrollo ya que sabíamos exactamente qué rompía con las pruebas granulares.
Larga historia corta, no lo haces. Las pruebas de integración completa crecen exponencialmente a medida que crece su base de código (más código significa más pruebas y más código significa que todas las pruebas tardan más en ejecutarse ya que hay más "integración" para trabajar). Yo diría que cualquier cosa en el rango de "horas" pierde la mayoría de los beneficios de la integración continua ya que el ciclo de retroalimentación no está allí. Incluso una mejora en el orden de magnitud no es suficiente para hacerte sentir bien, y no está cerca de hacerte escalable.
Por lo tanto, recomendaría reducir las pruebas de integración a las pruebas de humo más amplias y vitales. Luego se pueden ejecutar todas las noches o un intervalo no continuo, reduciendo gran parte de su necesidad de rendimiento. Las pruebas unitarias, que solo crecen linealmente a medida que agrega más código (las pruebas aumentan, el tiempo de ejecución por prueba no lo hace) son el camino a seguir para la escala.
fuente
Las pruebas de integración siempre serán de larga duración, ya que deben imitar a un usuario real. Por esta misma razón, ¡no deberías ejecutarlos todos sincrónicamente!
Dado que ya está ejecutando cosas en la nube, me parece que está en una posición privilegiada para escalar sus pruebas en varias máquinas.
En el caso extremo, active un nuevo entorno por prueba y ejecútelos todos al mismo tiempo. Sus pruebas de integración solo tomarán el tiempo que dura la prueba más larga.
fuente
Cortar / optimizar las pruebas me parece la mejor idea, pero en caso de que no sea una opción, tengo una alternativa que proponer (pero requiere construir algunas herramientas propietarias simples).
Me enfrenté a un problema similar pero no en nuestras pruebas de integración (se ejecutaron en minutos). En cambio, estaba simplemente en nuestras compilaciones: la base de código C a gran escala, llevaría horas construir.
Lo que vi como un desperdicio extremo fue el hecho de que estábamos reconstruyendo todo desde cero (aproximadamente 20,000 archivos fuente / unidades de compilación) incluso si solo cambiaban unos pocos archivos fuente, y por lo tanto, pasamos horas para un cambio que solo debería tomar segundos o minutos lo peor.
Por lo tanto, probamos la vinculación incremental en nuestros servidores de compilación, pero eso no era confiable. A veces daría falsos negativos y no se basaría en algunas confirmaciones, solo para tener éxito en una reconstrucción completa. Peor aún, a veces daría falsos positivos e informaría un éxito de compilación, solo para que el desarrollador fusione una compilación rota en la rama principal. Así que volvimos a reconstruir todo cada vez que un desarrollador empujaba cambios desde su rama privada.
Odiaba mucho esto. Entraría a las salas de conferencias con la mitad de los desarrolladores jugando videojuegos y simplemente porque había poco más que hacer mientras esperaba las compilaciones. Traté de obtener una ventaja de productividad al realizar múltiples tareas y comenzar una nueva rama una vez que me comprometí para poder trabajar en el código mientras esperaba las compilaciones, pero cuando una prueba o compilación fallaba, se volvía demasiado doloroso poner en cola los cambios más allá de ese punto. e intenta arreglar todo y coserlo todo de nuevo.
Proyecto paralelo mientras espera, integre más tarde
Entonces, lo que hice en su lugar fue crear un marco esquelético de la aplicación, el mismo tipo de interfaz de usuario básica y partes relevantes del SDK para desarrollar en un proyecto completamente separado. Luego escribiría un código independiente contra eso mientras esperaba las compilaciones, fuera del proyecto principal. Eso al menos me dio algo de codificación para poder seguir siendo algo productivo, y luego comenzaría a integrar ese trabajo realizado completamente fuera del producto en el proyecto más adelante, fragmentos de código. Esa es una estrategia para sus desarrolladores si se encuentran esperando mucho.
Analizar archivos de origen manualmente para averiguar qué reconstruir / volver a ejecutar
Sin embargo, odiaba cómo perdíamos tanto tiempo para reconstruir todo todo el tiempo. Así que me encargué durante un par de fines de semana escribir un código que realmente escaneara los archivos en busca de cambios y reconstruyera solo los proyectos relevantes, aún una reconstrucción completa, sin enlaces incrementales, pero solo de los proyectos que necesitan ser reconstruidos ( cuyos archivos dependientes, analizados recursivamente, cambiaron). Eso fue totalmente confiable y después de demostrarlo y probarlo exhaustivamente, pudimos usar esa solución. Eso redujo el tiempo promedio de construcción de horas a unos pocos minutos, ya que solo estábamos reconstruyendo los proyectos necesarios (aunque los cambios centrales del SDK aún podrían tomar una hora, pero lo hicimos con mucha menos frecuencia que los cambios localizados).
La misma estrategia debería ser aplicable a las pruebas de integración. Simplemente analice los archivos fuente de forma recursiva para averiguar de qué archivos dependen las pruebas de integración (por ejemplo,
import
en Java,#include
en C o C ++) en el lado del servidor, y los archivos incluidos / importados de esos archivos y así sucesivamente, creando un gráfico de archivo de dependencia de inclusión / importación completo para el sistema. A diferencia del análisis de compilación que forma un DAG, el gráfico no debe estar dirigido ya que está interesado en cualquier archivo que haya cambiado y que contenga código que pueda ejecutarse indirectamente *. Vuelva a ejecutar la prueba de integración solo si alguno de esos archivos en el gráfico para la prueba de integración de interés ha cambiado. Incluso para millones de líneas de código, fue fácil hacer este análisis en menos de un minuto. Si tiene archivos distintos al código fuente que pueden afectar una prueba de integración, como los archivos de contenido, tal vez pueda escribir metadatos en un comentario en el código fuente que indique esas dependencias en las pruebas de integración, de modo que si esos archivos externos cambian, las pruebas también volver a correr.* Como ejemplo, si test.c incluye foo.h, que también está incluido por foo.c, entonces un cambio a test.c, foo.h o foo.c debería marcar la prueba integrada como la necesidad de una nueva ejecución.
Esto puede tomar uno o dos días completos para programar y probar, especialmente en el entorno formal, pero creo que debería funcionar incluso para las pruebas de integración y vale la pena si no tiene otra opción que esperar en el rango de horas para las compilaciones para terminar (ya sea por el proceso de construcción o prueba o empaque o lo que sea). Eso puede traducirse en tantas horas de trabajo perdidas en solo una cuestión de meses que reduciría el tiempo que lleva construir este tipo de solución patentada, además de matar la energía del equipo y aumentar el estrés causado por los conflictos en fusiones más grandes hechas menos frecuentemente como resultado de todo el tiempo perdido esperando. Es simplemente malo para el equipo en general cuando pasan gran parte de su tiempo esperando cosas.todo para ser reconstruido / re-ejecutado / reempacado en cada pequeño cambio.
fuente
Parece que tienes demasiadas pruebas de integración. Recordar la pirámide de prueba . Las pruebas de integración pertenecen al medio.
Como un ejemplo tomar un repositorio con método
set(key,object)
,get(key)
. Este repositorio se usa ampliamente en toda su base de código. Todos los métodos que dependen de este repositorio se probarán con un repositorio falso. Ahora solo necesita dos pruebas de integración, una para set y otra para get.Algunas de esas pruebas de integración probablemente podrían convertirse en pruebas unitarias. Por ejemplo, las pruebas de extremo a extremo en mi opinión solo deberían probar que el sitio está configurado correctamente con la cadena de conexión correcta y los dominios correctos.
Las pruebas de integración deberían comprobar que el ORM, los repositorios y las abstracciones de cola son correctos. Como regla general, no se necesita código de dominio para las pruebas de integración, solo abstracciones.
Casi todo lo demás puede probarse en unidades con implementaciones stubbed / burladas / falsificadas / in-mem para dependencias.
fuente
En mi experiencia en un entorno Agile o DevOps donde las canalizaciones de entrega continua son comunes, las pruebas de integración deben llevarse a cabo a medida que se completa o ajusta cada módulo. Por ejemplo, en muchos entornos de canalización de entrega continua, no es raro tener múltiples implementaciones de código por desarrollador por día. La ejecución de un conjunto rápido de pruebas de integración al final de cada fase de desarrollo antes de la implementación debería ser una práctica estándar en este tipo de entorno. Para obtener información adicional, un gran libro electrónico para incluir en su lectura sobre este tema es Una guía práctica para probar en DevOps , escrita por Katrina Clokie.
Para realizar una prueba eficiente de esta manera, el nuevo componente debe probarse con los módulos completos existentes en un entorno de prueba dedicado o con Stubs y Drivers. Dependiendo de sus necesidades, generalmente es una buena idea mantener una biblioteca de Stubs y controladores para cada módulo de aplicación en una carpeta o biblioteca para permitir el uso repetitivo y rápido de las pruebas de integración. Mantener Stubs y Drivers organizados de esta manera hace que sea fácil realizar cambios iterativos, manteniéndolos actualizados y funcionando de manera óptima para satisfacer sus necesidades de pruebas en curso.
Otra opción a considerar es una solución desarrollada originalmente alrededor de 2002, llamada virtualización de servicios. Esto crea un entorno virtual, simulando la interacción del módulo con los recursos existentes para fines de prueba en un DevOps empresarial complejo o en el entorno Agile.
Este artículo puede ser útil para comprender más acerca de cómo realizar pruebas de integración en la empresa
fuente
¿Ha medido cada prueba para ver dónde se está tomando el tiempo? Y luego, midió el rendimiento de la base de código si hay un bit particularmente lento. ¿El problema general es una de las pruebas o la implementación, o ambas?
Por lo general, desea reducir el impacto de la prueba de integración para minimizar su ejecución en cambios relativamente menores. Luego, puede dejar la prueba completa para una ejecución de 'QA' que realiza cuando la rama es promovida al siguiente nivel. Por lo tanto, tiene pruebas unitarias para ramas de desarrollo, ejecuta pruebas de integración reducidas cuando se fusionan y ejecuta una prueba de integración completa cuando se combina con una rama candidata de lanzamiento.
Esto significa que no tiene que reconstruir, volver a empaquetar y volver a implementar todo cada commit. Puede organizar su configuración, en el entorno de desarrollo, para realizar una implementación lo más barata posible confiando en que todo estará bien. En lugar de hacer girar una máquina virtual completa e implementar todo el producto, deje la máquina virtual con la versión anterior y copie los nuevos archivos binarios, por ejemplo (YMMV dependiendo de lo que tenga que hacer).
Este enfoque optimista general todavía requiere la prueba completa, pero eso se puede realizar en una etapa posterior cuando el tiempo necesario es menos urgente. (por ejemplo, puede ejecutar la prueba completa una vez durante la noche, si hay algún problema, el desarrollador puede resolverlos por la mañana). Esto también tiene la ventaja de actualizar el producto en la plataforma de integración para las pruebas del día siguiente: puede quedar desactualizado a medida que los desarrolladores cambien las cosas, pero solo por 1 día.
Tuvimos un problema similar al ejecutar una herramienta de análisis estático basada en seguridad. Las ejecuciones completas tomarían años, por lo que lo trasladamos de las confirmaciones de desarrollador a una confirmación de integración (es decir, teníamos un sistema en el que el desarrollador dijo que habían terminado, se fusionó con una rama de 'nivel 2' donde se realizaron más pruebas, incluido el rendimiento pruebas. Cuando se completó, se fusionó con una rama de control de calidad para su implementación. La idea es eliminar las ejecuciones regulares que ocurrirían continuamente a las ejecuciones que se realizaban todas las noches: los desarrolladores obtendrían los resultados por la mañana y no afectarían su desarrollo enfoque hasta más tarde en su ciclo de desarrollo).
fuente
En algún momento, un conjunto completo de pruebas de integración puede tardar muchas horas en completarse, incluso en hardware costoso. Una de las opciones es no ejecutar la mayoría de esas pruebas en cada confirmación, y en su lugar ejecutarlas todas las noches, o en un modo continuo por lotes (una vez por múltiples confirmaciones).
Sin embargo, esto crea un nuevo problema: los desarrolladores no reciben comentarios inmediatos y las compilaciones rotas pueden pasar desapercibidas. Para solucionar esto, es importante que sepan que algo está roto en todo momento. Crear herramientas de notificación como Catlight o el notificador de bandeja de TeamCity puede ser bastante útil.
Pero habrá otro problema más. Incluso cuando el desarrollador ve que la compilación está rota, es posible que no se apresure a revisarla. Después de todo, alguien más puede estar ya revisándolo, ¿verdad?
Por esa razón, esas dos herramientas tienen una función de "investigación de compilación". Indicará si alguien del equipo de desarrollo está realmente revisando y reparando la compilación rota. Los desarrolladores pueden ofrecerse como voluntarios para verificar la construcción y, hasta que eso suceda, todos los miembros del equipo estarán molestos por un icono rojo cerca del reloj.
fuente
Parece que su base de código está creciendo y algo de administración de código ayudará. Usamos Java, así que disculpas de antemano si asumo esto.
La tienda de Java en la que trabajo utiliza este enfoque, y rara vez estamos a la espera de que se ejecuten las pruebas de integración.
fuente
Otro posible enfoque para mantener en las pruebas de integración de canalización de CI (o cualquier tipo de verificaciones, incluidas las compilaciones) con tiempos de ejecución largos o que requieren recursos limitados y / o costosos es cambiar de los sistemas de CI tradicionales basados en verificaciones posteriores a la confirmación (que son susceptible de congestión ) a uno basado en verificaciones previas a la confirmación .
En lugar de comprometer directamente sus cambios en la sucursal, los desarrolladores los envían a un sistema de verificación automatizado centralizado que realiza las verificaciones y:
Dicho enfoque permite combinar y probar múltiples cambios enviados, lo que puede aumentar la velocidad efectiva de verificación de CI muchas veces.
Un ejemplo de ello es el sistema de compuerta basado en Gerrit / Zuul utilizado por OpenStack .
Otro es ApartCI ( descargo de responsabilidad : soy su creador y el fundador de la compañía que lo ofrece).
fuente