Desplazamiento a la izquierda de las pruebas con pruebas unitarias

Las pruebas ayudan a garantizar que el código funcione según lo previsto, pero el tiempo y el esfuerzo para compilar pruebas emplean tiempo de otras tareas, como el desarrollo de funciones. Por este motivo, es importante sacarles el máximo valor a las pruebas. En este artículo se describen los principios de las prueba de DevOps, centrándose en el valor de las pruebas unitarias y en una estrategia de prueba de desplazamiento a la izquierda.

Los probadores específicos solían escribir la mayoría de pruebas y muchos desarrolladores de productos no aprendían a escribir pruebas unitarias. Escribir pruebas puede parecer demasiado difícil o demasiado laborioso. Pueden surgir dudas sobre si una estrategia de prueba unitaria funcionara, malas experiencias con pruebas unitarias mal escritas o miedo a que las pruebas unitarias reemplacen las pruebas funcionales.

Graphic that describes arguments about adopting unit testing.

Para implementar una estrategia de prueba de DevOps, actúe de forma pragmática y céntrese en darle dinamismo. Aunque puede hacer hincapié en pruebas unitarias de código nuevo o código existente que se pueden refactorizar de forma limpia, tendría sentido que un código base heredado permita alguna dependencia. Si las partes significativas del código de producto usan SQL, permitiendo que las pruebas unitarias tomen dependencias en el proveedor de recursos de SQL en lugar de simular esa capa, podría ser una estrategia a corto plazo para progresar.

A medida que las organizaciones de DevOps maduran, resulta más fácil para los responsables mejorar los procesos. Aunque puede haber cierta resistencia al cambio, las organizaciones ágiles valoran los cambios que sean claramente rentables. Las ejecuciones de pruebas más rápidas con menos errores deberían ser algo más atractivo, ya que implica más tiempo para invertir en generar un nuevo valor a través del desarrollo de funciones.

Taxonomía de pruebas de DevOps

Definir la taxonomía de una prueba es muy importante en los procesos de pruebas de DevOps. La taxonomía de una prueba de DevOps clasifica las pruebas individuales por sus dependencias y el tiempo que tardan en ejecutarse. Los desarrolladores deben conocer los tipos correctos de pruebas que se vayan a usar en diferentes casos y qué pruebas necesitan de diferentes partes del proceso. La mayoría de organizaciones dividen las pruebas en cuatro niveles:

  • Las pruebas L0 y L1 son pruebas unitarias o pruebas que dependen del código del ensamblado que se somete a prueba y nada más. Las L0 son una amplia clase de pruebas unitarias rápidas en memoria.
  • Las L2 son pruebas funcionales que podrían necesitar el ensamblado, así como otras dependencias, como SQL o el sistema de archivos.
  • Las L3 son pruebas funcionales que se ejecutan en implementaciones de servicio que se pueden probar. Esta categoría de prueba precisa una implementación de servicio, pero puede usar códigos auxiliares para las dependencias de servicio principales.
  • Las pruebas L4 son una clase restringida de pruebas de integración que se ejecutan en fase de producción. Para las pruebas L4 se necesita una implementación completa del producto.

Aunque sería ideal para que todas las pruebas se ejecuten en todo momento, esto no es factible. Los equipos pueden seleccionar en qué punto se encuentra el proceso de DevOps para ejecutar cada prueba y usar estrategias de desplazamiento a la izquierda o de desplazamiento a la derecha para mover diferentes tipos de prueba anteriores o posteriores en el proceso.

Por ejemplo, la que se busca podría ser que los desarrolladores siempre ejecuten las pruebas L2 antes de confirmar, se produce un error automáticamente en una solicitud de incorporación de cambios si se produce un error en la ejecución de pruebas L3 y la implementación podría bloquearse si se produce un error en las pruebas L4. Las reglas específicas pueden variar de una organización a otra, pero aplicar las expectativas de todos los equipos de una organización dirige a todos hacia los mismos objetivos de calidad.

Directrices de pruebas unitarias

Cree directrices estrictas para las pruebas unitarias L0 y L1. Estas pruebas deben ser muy rápidas y fiables. Por ejemplo, el tiempo medio de ejecución por prueba L0 en un ensamblado debe ser inferior a 60 milisegundos. El tiempo medio de ejecución por prueba L1 en un ensamblado debe ser inferior a 400 milisegundos. Ninguna prueba en este nivel debe superar los 2 segundos.

Un equipo de Microsoft ejecuta más de 60 000 pruebas unitarias en paralelo en menos de seis minutos. Su objetivo es reducir este tiempo a menos de un minuto. El equipo realiza un seguimiento del tiempo de ejecución de pruebas unitarias con herramientas como la siguiente gráfica y archivos de errores en las pruebas que superan el tiempo permitido.

Chart that shows continuous focus on test execution time.

Directrices de pruebas funcionales

Las pruebas funcionales deben ser independientes. El concepto clave de las pruebas L2 es el aislamiento. Las pruebas aisladas se pueden ejecutar correctamente y de forma fiable en cualquier secuencia, ya que tienen control completo sobre el entorno en el que se ejecutan. El estado debe conocerse al principio de la prueba. Si una prueba ha creado datos y los ha dejado en la base de datos, esto podría afectar negativamente la ejecución de otra prueba que se base en un estado de base de datos diferente.

Las pruebas heredadas que necesitan una identidad de usuario podrían haber llamado a proveedores de autenticación externos para obtener la identidad. Esta práctica presenta varios desafíos. La dependencia externa podría no ser fiable o no estar disponible momentáneamente, lo que podría interrumpir la prueba. Esta práctica también infringe el principio de aislamiento de prueba, ya que una prueba podría cambiar el estado de una identidad, como el permiso, lo que da lugar a un estado predeterminado inesperado para otras pruebas. Plantéese la posibilidad de evitar estos problemas poniendo atención a la compatibilidad con la identidad en el marco de pruebas.

Principios de pruebas de DevOps

Para realizar la transición de una cartera de pruebas a procesos modernos de DevOps, elabore una estrategia de calidad. Los equipos deben cumplir los siguientes principios de pruebas al definir e implementar una estrategia de pruebas de DevOps.

Diagram that shows an example of a quality vision and lists test principles.

Desplazamiento a la izquierda para hacer las pruebas en una fase anterior

Las pruebas pueden tardar mucho tiempo en ejecutarse. A medida que los proyectos escalan, el número de prueba y los tipos aumentan considerablemente. Cuando los conjuntos de pruebas crecen y duran horas o días en completarse, pueden pasar a una etapa más tardía hasta que se ejecuten en el último momento. Las ventajas de la calidad del código de las pruebas no se sacan hasta pasado mucho tiempo después de confirmar el código.

Las pruebas de larga duración también pueden producir errores que necesitan de mucho tiempo para investigar. Los equipos pueden crear tolerancia frente a errores, especialmente al principio de los sprints. Esta tolerancia debilita el valor de las pruebas como barómetro sobre la calidad del código base. Las pruebas de larga duración y de última hora también hacen las expectativas de final de sprint sean impredecibles, ya que se debe pagar una cantidad desconocida de deuda técnica para obtener el código entregable.

El objetivo para desplazar las pruebas a la izquierda es mover la calidad de forma ascendente mediante la realización de tareas de prueba anteriormente en el proceso. A través de una combinación de pruebas y mejoras de procesos, el desplazamiento a la izquierda reduce el tiempo que tardan en ejecutarse las pruebas y el impacto de los errores más adelante en el ciclo. El desplazamiento a la izquierda garantiza que la mayoría de pruebas se completan antes de que se combine un cambio en la rama principal.

Diagram that shows the move to shift-left testing.

Además de cambiar ciertas responsabilidades de pruebas de desplazamiento a la izquierda para mejorar la calidad del código, los equipos pueden cambiar otros aspectos de las pruebas hacia la derecha, o más adelante en el ciclo de DevOps, para mejorar el producto final. Para obtener más información, consulte Desplazamiento a la derecha de las pruebas en fase de producción.

Escribir pruebas en el nivel más bajo posible

Escriba más pruebas unitarias. Favorezca las pruebas con las pocas dependencias externas y céntrese en ejecutar la mayoría de las pruebas como parte de la compilación. Piense en un sistema de compilación paralelo que pueda ejecutar pruebas unitarias en un ensamblado en cuanto se retiren el ensamblado y las pruebas asociadas. No resulta viable probar todos los aspectos de un servicio en este nivel, pero lo esencial es usar pruebas unitarias más ligeras si pueden dar los mismos resultados que las pruebas funcionales más pesadas.

Buscar más fiabilidad en las pruebas

Una prueba que no es fiable le resulta costosa de mantener a la organización. Esta prueba gira directamente en torno al objetivo de eficacia de ingeniería haciendo que sea difícil realizar cambios con confianza. Los desarrolladores deben poder realizar cambios en cualquier lugar y estar convencidos al instante de que no se ha roto nada. Mantenga unos niveles de calidad altos para favorecer la fiabilidad. Desincentive el uso de pruebas de IU, ya que tienden a no ser fiables.

Escribir pruebas funcionales que se pueden ejecutar en cualquier lugar

Las pruebas pueden usar puntos de integración especializados diseñados específicamente para habilitar las pruebas. Una razón para realizar esta práctica es la falta de capacidad de hacer pruebas en el propio producto. Desafortunadamente, las pruebas como estas suelen depender de conocimientos internos y usar datos de implementación que no importan desde una perspectiva de prueba funcional. Estas pruebas se limitan a entornos que tienen los secretos y los ajustes necesarios para ejecutar las pruebas, que generalmente excluyen las implementaciones de producción. Las pruebas funcionales solo deben usar la API pública del producto.

Diseñar productos para que se puedan probar

Las organizaciones con un proceso de DevOps ya maduro tienen una visión completa de lo que significa ofrecer un producto de calidad según la cadencia de la nube. Si se produce una descompensación a favor de las pruebas unitarias sobre las pruebas funcionales, los equipos se verán obligados a optar por diseños e implementaciones que incluyan la posibilidad de probarse. Hay diferentes ideas sobre lo que constituye código bien diseñado y bien implementado para favorecer su capacidad de someterse a prueba, al igual que hay diferentes estilos de codificación. La teoría establece que adaptar el diseño a su capacidad de prueba debe convertirse en la clave en las cuestiones sobre el diseño y la calidad del código.

Considerar el código de prueba como código de producto

Al afirmar expresamente que el código de prueba es código de producto, queda claro que la calidad del código de prueba es tan importante en la entrega como en la del código del producto. Los equipos deben tratar el código de prueba de la misma manera que tratan el código del producto y ponerle el mismo nivel de atención al diseño e implementación de pruebas y a los marcos de pruebas. Esto es similar a administrar la configuración y la infraestructura como código. Para finalizar, la revisión de un código debe tener en cuenta el código de prueba y categorizarlo en el mismo nivel de calidad que el código del producto.

Usar la infraestructura de pruebas compartidas

Facilite el proceso usando una infraestructura de pruebas para generar indicios de calidad de confianza. Vea las pruebas como un servicio compartido para todo el equipo. Almacene el código de las pruebas unitarias junto con el código del producto y compílelo con el producto. Las pruebas que se ejecutan como parte del proceso de compilación también deben ejecutarse en herramientas de desarrollo como Azure DevOps. Si las pruebas se pueden ejecutar en todos los entornos a partir de la fase de desarrollo local en la fase de producción, tendrán la misma fiabilidad que el código del producto.

Hacer que los propietarios del código sean responsables de las pruebas

El código de prueba debe residir junto al código del producto en un repositorio. Para que el código se pruebe en un límite de componente, traslade la responsabilidad de las pruebas a la persona que escriba el código del componente. No confíe en otros usuarios para probar el componente.

Caso práctico: Desplazamiento a la izquierda con pruebas unitarias

Un equipo de Microsoft decidió reemplazar sus conjuntos de pruebas heredados por pruebas unitarias modernas de DevOps y un proceso de desplazamiento a la izquierda. El equipo llevó un control del progreso entre sprints cada tres semanas, tal como se muestra en la gráfica siguiente. La gráfica incluye sprints de 78 a 120, equivalente a 42 sprints durante 126 semanas, o aproximadamente dos años y medio de trabajo.

El equipo comenzó con 27 000 pruebas heredadas en el sprint 78 y alcanzó cero pruebas heredadas en S120. Un conjunto de pruebas unitarias L0 y L1 reemplazó la mayoría de pruebas funcionales antiguas. Las nuevas pruebas L2 reemplazaron algunas de las pruebas y muchas de las pruebas antiguas se eliminaron.

Diagram that shows a sample test portfolio balance over time.

En el recorrido de un software que tarda más de dos años en completarse, hay mucho que aprender del propio proceso. En general, el esfuerzo por rehacer completamente el sistema de pruebas a lo largo de dos años supuso una inversión enorme. No todos los equipos de funciones trabajaron al mismo tiempo. Muchos equipos de la organización invirtieron tiempo en cada sprint y, en algunos sprints, el equipo empleó total dedicación. Aunque es difícil medir los costes del cambio, fue un requisito no negociable en favor de los objetivos de calidad y el rendimiento del equipo.

Primeros pasos

Al principio, el equipo dejó por una lado las pruebas funcionales antiguas, llamadas pruebas TRA. El equipo quería que los desarrolladores asimilaran la idea de escribir pruebas unitarias, especialmente para nuevas funciones. El objetivo era facilitar la creación de pruebas L0 y L1. El equipo necesitaba desarrollar esa capacidad en primer lugar y ganar impulso.

En la gráfica anterior se muestra el número de pruebas unitarias que empiezan a aumentar al principio, cuando el equipo descubrió la ventaja de crear pruebas unitarias. Las pruebas unitarias fueron más fáciles de mantener, más rápidas de ejecutar y tenían menos errores. Era fácil obtener ayuda con la ejecución de todas las pruebas unitarias en el flujo de solicitud de incorporación de cambios.

El equipo no se centró en escribir nuevas pruebas L2 hasta el sprint 101. Mientras tanto, el número de pruebas TRA pasó de 27 000 a 14 000 del sprint 78 al sprint 101. Las nuevas pruebas unitarias reemplazaron algunas de las pruebas TRA, pero muchas se eliminaron, en función de lo útiles que le parecieran al equipo.

Las pruebas TRA pasaron de 2100 a 3800 en el sprint 110 porque se detectaron más pruebas en el árbol de origen y se añadieron a la gráfica. Se observó que las pruebas siempre se habían estado ejecutando, pero no se hacía un seguimiento correcto. Esto no era una crisis, pero había que ser honesto y era necesario hacer una reevaluación donde fuera necesario.

Usar procesos más rápidos

Cuando el equipo se creó un modelo de integración continua (CI) que fuera extremadamente rápida y fiable, se convirtió en un parámetro de confianza para la calidad del producto. En la captura de pantalla siguiente podemos ver la solicitud de incorporación de cambios y el proceso de CI en acción, así como el tiempo necesario para pasar por las diferentes fases.

Diagram that shows the pull request and rolling CI pipeline in action.

La solicitud de incorporación de cambios tarda unos 30 minutos en combinarse, lo que implica la ejecución de 60 000 pruebas unitarias. La combinación del código en la compilación de CI tarda aproximadamente 22 minutos. El primer indicio de calidad de la CI, SelfTest, viene después de alrededor de una hora. A continuación, la mayor parte del producto se prueba con el cambio propuesto. En un plazo de dos horas de Merge a SelfHost, se prueba todo el producto y el cambio está listo para entrar en producción.

Uso de métricas

El equipo realiza un seguimiento con una ficha de evaluación como se ve en el ejemplo siguiente. En un nivel alto, la ficha de evaluación hace uso de dos tipos de métricas: estado o deuda y velocidad.

Diagram that shows a metrics scorecard for tracking test performance.

En el caso de las métricas de estado del sitio activo, el equipo realiza un seguimiento del tiempo de detección, el tiempo de mitigación y el número de instancias de reparación que lleva un equipo. Una instancia de reparación es el trabajo que el equipo identifica en un análisis del sitio activo para evitar que se repitan incidencias similares. La ficha de evaluación también controla si los equipos cierran las instancias de reparación dentro de un período de tiempo razonable.

En el caso de las métricas de estado de las tareas de ingeniería, el equipo realiza un seguimiento de los errores activos por desarrollador. Si un equipo tiene más de cinco errores por desarrollador, el equipo debe priorizar la corrección de esos errores antes de desarrollar nuevas funciones. El equipo también lleva un control de errores antiguos en categorías especiales, como la seguridad.

Las métricas de velocidad de ingeniería calculan la velocidad en diferentes partes del proceso de integración continua y entrega continua (CI/CD). El objetivo general es aumentar la velocidad del proceso de DevOps: concepción de una idea, inserción del código en la fase de producción y recepción de datos de los clientes.

Pasos siguientes