Pruebas unitarias y de integración para mejorar la confiabilidad del software / Sudo Null IT News

Hola a todos, mi nombre es Andrey Fedotov, soy desarrollador backend en la empresa Digital Industrial Platform, donde creamos un producto del mismo nombre: una plataforma industrial de Internet de las cosas. ZIIoT Petróleo y Gas. Nuestro equipo está desarrollando un conjunto de servicios diseñados para recibir datos de diversas fuentes. Y mi artículo es una especie de historia de nuestro proyecto a través del prisma de las pruebas unitarias y de integración.

Como dijo Kent Beck: “Muchas fuerzas nos impiden obtener código limpio y, a veces, ni siquiera podemos obtener código que simplemente funcione.” Su libro sobre TDD (haré una reserva de inmediato de que no usamos TDD, pero el libro es muy bueno) utiliza un enfoque en el que primero se escribe el código “que funciona”, después de lo cual se crea un “código limpio”. . Y este enfoque contradice el modelo de desarrollo impulsado por la arquitectura, en el que primero escribimos “código limpio” y luego luchamos por integrar código “que funcione” en el proyecto. Nuestra realidad era aún peor: teníamos un código que no funcionaba bien y, tras mucho esfuerzo, se convirtió en un código que funcionaba. Quizás algún día lleguemos a algo puro.

Para entender de qué estoy hablando, sugiero mirar las pruebas unitarias que estaban en el proyecto hace un año y medio, aproximadamente al mismo tiempo que nuestro equipo se hizo cargo del conjunto de servicios mencionados anteriormente.

La siguiente captura de pantalla muestra una carpeta con los nombres de los archivos con pruebas. Había alrededor de doscientos en esta carpeta.

Sí, así se llamaban: UnitTest001.cs, UnitTest020.cs, UnitTest120.cs

La siguiente imagen muestra un ejemplo de una de las pruebas ubicadas en el archivo. UnitTest015.cs.

Nombre de la prueba TestMethod02 – un nombre de prueba típico en dichos archivos.

En esta prueba, las solicitudes a fuentes externas se realizan dos veces: primero para escribir y luego para leer. Lo que ya da a entender que esto no parece una prueba unitaria. Y los resultados de la consulta se verifican aleatoriamente.

Aquí hay otro ejemplo de prueba. TestMethod16. Contiene una línea y llama al formidable método. Run_142_04.

¿Qué podría significar esto? Fallemos y veamos cuál es este método.

Este es un método de extensión y su código no cabe en ninguna parte, por lo que aquí se muestran solo partes del mismo. Y existen cientos de métodos de este tipo. Sugiero ni siquiera intentar ahondar en las intenciones de los autores.

Se podría pensar que se trata de algún tipo de pruebas generadas automáticamente. Pero no, fueron escritos a mano.

Como resultado provisional, enumeraré los problemas que tenía el proyecto en ese momento:

  • muchas solicitudes de TP con errores que requieren la participación del equipo de desarrollo;

  • código difícil de mantener (no solo en las pruebas, sino en general);

  • el propósito de las pruebas existentes no está claro (no hay protección contra errores);

  • el costo de las correcciones es alto.

Como dijo Vladimir Khorikov en su libro “Principios de pruebas unitarias”: “Es mejor no escribir ningún examen que escribir uno malo” Por lo tanto, decidimos escribir nuestras propias pruebas y deshacernos de las existentes.

Por cierto, si no has leído este libro, te recomiendo mucho que lo leas. Este es un almacén de experiencias y recomendaciones. El autor es un verdadero profesional y el libro es excelente, aunque, por supuesto, puede haber momentos que no sean adecuados para su equipo (por ejemplo, no hemos adoptado recomendaciones para nombrar las pruebas que se describen en el libro, pero estas son cosas menores).

Antes de continuar, veamos un poco de teoría y luego veremos qué pruebas hicimos y qué resultados arrojaron. Aquí me baso en definiciones de libros Vladimir Khorikov.

Propósito de las pruebas unitarias y de integración.

Las pruebas unitarias y de integración no se tratan solo de escribir pruebas. Su objetivo es garantizar un crecimiento estable del proyecto de software. Y la palabra clave aquí es “estable”. Al comienzo de la vida de un proyecto, desarrollarlo es bastante sencillo. Es mucho más difícil mantener este desarrollo en el tiempo. El siguiente gráfico muestra la dependencia del tiempo del progreso para proyectos con y sin pruebas.

Esta reducción en la velocidad de desarrollo se llama entropía del software.

En nuestro proyecto, no planteamos las pruebas de redacción como un fin en sí mismo. En primer lugar, queríamos resolver los problemas existentes que mencioné anteriormente y también poder hacer crecer la base del código sin reducir la confiabilidad del producto en su conjunto.

Pruebas y estrés

Además, existe una conexión entre las pruebas y los niveles de estrés. Esto está escrito en el libro sobre TDD de Kent Beck (aunque el libro trata sobre TDD, en realidad es una lectura fascinante con un conjunto de historias interesantes de la práctica y la vida del autor, por lo que también recomiendo leerlo con una taza de té).

Cuanto más estrés sentimos, menos probamos el código que desarrollamos. Cuanto menos probamos el código que desarrollamos, más errores cometemos. Cuantos más errores cometemos, mayor será el nivel de estrés que sentimos. Resulta ser un círculo vicioso con retroalimentación positiva: un mayor estrés conduce a un mayor estrés.

Las pruebas, a su vez, convierten el estrés en aburrimiento. “No, no rompí nada. Todavía se están realizando pruebas”. Por lo tanto, hacerse pruebas también reduce el estrés.

¿Qué es una prueba unitaria?

También es una prueba unitaria. La definición general es la siguiente: es una prueba automatizada que:

  • comprueba que un pequeño fragmento de código (también llamado unidad) esté funcionando correctamente;

  • lo hace rápidamente;

  • admite el aislamiento de otro código.

Puede surgir la pregunta ¿qué es una unidad y qué es el aislamiento?

Hay que decir que existen dos escuelas de pruebas unitarias: la clásica y la de Londres.

Se llama clásico porque así es como se abordaron inicialmente las pruebas unitarias. La London School fue formada por una comunidad de programadores de Londres (de repente). La raíz de las diferencias entre la escuela clásica y la londinense es precisamente la cuestión del aislamiento. La Escuela de Londres describe el aislamiento a nivel de clase, y la unidad suele ser la clase misma. En la escuela clásica, una unidad significa una unidad de conducta. En nuestros servicios nos adherimos a la escuela clásica. Por lo general, da como resultado pruebas de mayor calidad y es más adecuado para lograr el objetivo de crecimiento sostenible del proyecto.

Tipos de dependencias

También cabe mencionar los tipos de dependencias que existen:

  • compartido: más de una prueba tiene acceso. Les permite influir mutuamente en los resultados (por ejemplo, DB);

  • privado – no compartido;

  • fuera de proceso: trabajo fuera del proceso de solicitud.

Prueba de integración

Una prueba de integración es aquella que no satisface al menos un criterio de la definición de pruebas unitarias. Puede (y a menudo lo hace) probar múltiples unidades de comportamiento a la vez. Una prueba de integración también verifica que el código funcione en integración con dependencias compartidas, dependencias fuera de proceso o código desarrollado por otros equipos de la organización.

Pruebas de un extremo a otro

También son pruebas de API. Son pruebas de extremo a extremo. Constituyen un subconjunto de pruebas de integración. También verifican cómo funciona el código con dependencias que no son del procesador. Se diferencian de las pruebas de integración principalmente en que las pruebas de un extremo a otro generalmente incluyen una mayor cantidad de dependencias y generalmente verifican la ruta completa del usuario.

¿Qué deben hacer las pruebas?

Idealmente, las pruebas no deberían probar unidades de código, sino unidades de comportamiento, algo que tenga sentido para el dominio y cuya utilidad sea clara para el negocio.

Pirámide de pruebas

El concepto de pirámide de pruebas clásica prescribe una cierta proporción de diferentes tipos de pruebas en un proyecto. Los diferentes tipos de pruebas en la pirámide hacen diferentes compensaciones entre la velocidad de retroalimentación y la protección contra errores. Las pruebas en niveles superiores de la pirámide priorizan la protección contra errores, mientras que las pruebas en niveles inferiores enfatizan la velocidad de ejecución. Y viceversa: cuanto menor sea el nivel, menor será la protección contra errores, y cuanto más alto, menor será la velocidad.

Ahora pasemos de la teoría a las pruebas en nuestros servicios.

En nuestros servicios, la pirámide de pruebas actualmente tiene este aspecto:

Nuestro proyecto es una especie de proxy y contiene poca lógica empresarial, pero sí muchas interacciones con otros servicios. Por eso tenemos más pruebas de integración. Pero ésta no es la única razón.

Fábrica de aplicaciones web

Sí, las pruebas unitarias siempre son más rápidas de ejecutar que las pruebas de integración. Pero no todo es tan aterrador.

Uso WebApplicationFactory le permite crear muchas instancias de ejecución paralelas de una aplicación en la memoria, aisladas entre sí, y hacerlo de forma “económica”. Gracias a WebApplicationFactory todo sucede rápidamente (todavía no al precio de unidades, pero aun así). Leer más sobre WebApplicationFactory puedes leerlo aquí.

Y para nosotros, las pruebas de integración son una especie de dogfooding. Mientras lo hacían, ellos mismos se dieron cuenta de las partes débiles e inconvenientes de su API y tomaron medidas.

¿Por qué utilizamos pruebas?

Utilizamos pruebas de integración y de un extremo a otro para probar el comportamiento del usuario. Tenemos estas pruebas:

  • ejecutar localmente en máquinas de desarrollo,

  • interactuar con servicios reales en los stands,

  • existe la posibilidad de depurar,

  • Es posible ejecutar servicios localmente.

Usamos pruebas unitarias para probar la lógica de validación de consultas y verificar cualquier otra lógica interna.

Ejemplos:

Así es como se ve ahora una prueba unitaria típica. Aquí se marca el modo de solicitud de datos.

Así es como se ve una prueba de integración simple.

Usamos nombres recomendados por Microsoft. También usado aquí patrón AAA.

Características de las buenas pruebas

Me gustaría señalar que nuestras pruebas ahora no son solo código mejorado y el uso de algunas prácticas y patrones, son pruebas escritas desde cero que cumplen con los criterios de buenas pruebas:

  • protección contra errores,

  • resistencia a la refactorización,

  • retroalimentación rápida,

  • facilidad de soporte.

Los tres primeros son mutuamente excluyentes; como máximo, una prueba sólo puede utilizar dos de ellos. Elegimos la protección contra errores y la resistencia a la refactorización como las principales. En cuanto a la retroalimentación rápida, como mencioné anteriormente, esto no es muy crítico y WebApplicationFactory – esta es la misma pastilla. También hemos lanzado un cliente mecanografiado para nuestros usuarios y lo utilizamos nosotros mismos en las pruebas. Otro criterio importante es que nuestras pruebas sean parte DoD (Definición de Hecho).

También existen propiedades de un conjunto de pruebas exitoso:

  • integración en el ciclo de desarrollo. Por ahora, lo tenemos condicional y es responsabilidad de los desarrolladores, nuestro CI no está listo para ejecutar pruebas de integración, pero estos son planes para un futuro muy cercano.

  • comprobar las partes más importantes del código,

  • Máxima protección contra errores con mínimos costes de mantenimiento.

Algunas estadísticas

Hace dos años tuvimos 125 tickets de soporte técnico y la mayoría requirió correcciones. Hace un año, la situación mejoró: 75 solicitudes, pero muchas de ellas aún requerían la participación de los desarrolladores.

De momento, este año hay muchas menos solicitudes: sólo 24. Y lo más importante es que la mayoría son solicitudes de consulta o que no llegaron a los desarrolladores.

Durante el período de estabilización durante los lanzamientos, también hay una disminución en la cantidad de errores.

Por supuesto, hay muchos factores aquí, esto se debe no solo a nuestras pruebas, porque también reescribimos la mayor parte del código de servicio, pero aún así, ahora se detectan una gran cantidad de defectos en la etapa de desarrollo.

Todo lo anterior indica un aumento en la confiabilidad de nuestro software, como se indica en el título del artículo.

Breves conclusiones y recomendaciones.

  • Las pruebas deben probar unidades de comportamiento, no unidades de código.

  • no descuide las prácticas de redacción de exámenes: denominación, patrón AAA, etc.

  • la métrica más reveladora es la cantidad de errores,

  • En el mundo moderno, las pruebas de integración no son mucho más “caras” que las pruebas unitarias.

Deje sus preguntas, comentarios y consejos en los comentarios; estaré encantado. También escribí sobre el uso de HttpClient en nuestro trabajo. Puedes leer sobre esto aquí.

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *