Escribir código comprobable
Publicado: 2022-11-03Las pruebas unitarias son un instrumento esencial en la caja de herramientas de cualquier desarrollador de software. Escribir pruebas unitarias es relativamente fácil cuando se trata de una base de código que sigue las mejores prácticas, patrones y principios de diseño de software. El verdadero problema surge cuando se intenta realizar pruebas unitarias de código mal diseñado y no comprobable.
Este blog discutirá cómo escribir más código comprobable por unidad y qué patrones y malas prácticas evitar para mejorar la capacidad de prueba.
Código comprobable y no comprobable
Cuando se trabaja en aplicaciones a gran escala que necesitan mantenimiento a largo plazo, debemos confiar en las pruebas automatizadas para mantener alta la calidad general del sistema. En comparación con las pruebas de integración, en las que se prueban varias unidades como un todo, las pruebas unitarias tienen la ventaja de ser rápidas y estables. Rápido porque estamos instanciando, idealmente, solo la clase bajo prueba, y estable porque generalmente simulamos las dependencias externas, por ejemplo, la base de datos o la conexión de red.
Si no está familiarizado con la diferencia exacta entre las pruebas unitarias y de integración, puede leer más sobre este tema en nuestro blog Introducción a las pruebas.
El código comprobable se puede aislar del resto de nuestra base de código. En otras palabras, las unidades más pequeñas se pueden probar de forma independiente. El código no verificable está escrito de tal manera que es difícil, o incluso imposible, escribir una buena prueba unitaria para él.
Revisemos algunos antipatrones y malas prácticas que debemos evitar al escribir código comprobable.
Los ejemplos están escritos en Java, pero las convenciones de codificación mencionadas aquí se aplican a cualquier marco de prueba y lenguaje de programación orientado a objetos. Usaremos asertj y JUnit5 como ejemplos en esta publicación de blog.
Inyección de dependencia
La inyección de dependencia es uno de los patrones de diseño más importantes para lograr el aislamiento de las pruebas. La inyección de dependencia es un patrón de diseño en el que un objeto recibe otros objetos (dependencias) a través de parámetros del constructor o establecedores en lugar de tener que construirlos por sí mismo.
Con la inyección de dependencias, podemos aislar fácilmente la clase bajo prueba al simular las dependencias de un objeto.
Veamos un ejemplo sin inyección de dependencia:

Dado que la dependencia del motor se construye en el constructor de la clase Car, puede decir que las clases Car y Engine están estrechamente acopladas. Son altamente dependientes unos de otros; cambiar uno requeriría un cambio en el otro.
Desde una perspectiva de prueba, no puede probar la clase Car de forma aislada porque el ejemplo anterior no puede reemplazar la implementación concreta del motor con un doble de prueba.
Sin embargo, podemos lograr el aislamiento con el uso de inyección de dependencia y polimorfismo:

Ahora podemos construir implementaciones de múltiples motores y, por lo tanto, automóviles con diferentes motores:

Ahora es posible realizar pruebas de forma aislada porque podemos crear una implementación simulada de la abstracción Engine y pasarla a nuestra clase Car:

Cuando se trata de objetos que requieren otros objetos (dependencias), debe proporcionarlos a través de parámetros de constructor (inyección de dependencia), idealmente ocultos detrás de alguna abstracción.
Siguiendo este patrón, su código se vuelve más legible y adaptable para cambiar con el tiempo. Además, debe evitar hacer trabajo real en constructores: cualquier cosa más que asignaciones de campo es trabajo real. La palabra clave `nuevo` en los constructores siempre es una señal de advertencia de código no comprobable.
Una cosa a tener en cuenta aquí es que en algunas situaciones, el acoplamiento estrecho (por ejemplo, nuevas palabras clave en constructores, clases internas para aislamiento lógico, mapeadores de objetos) no es una mala práctica: clases que no tendrían sentido como una clase "independiente".
Estado mundial
Compartir un estado global a menudo puede producir pruebas irregulares (a veces pasa, a veces falla), especialmente en entornos de subprocesos múltiples.
Imagine un escenario en el que varios objetos bajo prueba comparten el mismo estado global: si un método en uno de los objetos desencadena un efecto secundario que cambia el valor del estado global compartido, la salida de un método en el otro objeto se vuelve impredecible. Evite el uso de métodos estáticos impuros porque mutan el estado global de alguna manera o se transmiten a algún estado global.
Veamos este método estático impuro:


Esencialmente, este método lee la fecha y hora actual del sistema y devuelve un resultado basado en ese valor. Sería muy difícil escribir una prueba unitaria adecuada basada en el estado para este método porque la llamada estática LocalDateTime.now() producirá resultados diferentes durante la ejecución de nuestras pruebas. Escribir pruebas para este método es imposible sin cambiar la fecha y la hora del sistema.
Para solucionar esto, pasaremos el método date time a timeOfDay como argumento:

El método estático timeOfDay ahora es puro: las mismas entradas siempre producen los mismos resultados. Ahora podemos pasar fácilmente objetos de fecha y hora aislados como argumentos en nuestras pruebas:

Ley de Deméter
La Ley de Deméter, o el principio del conocimiento mínimo, establece que los objetos deben saber solo acerca de los objetos estrechamente relacionados con el primer objeto. En otras palabras, un objeto solo debe tener acceso a los objetos que necesita. Por ejemplo, tenemos un método que acepta un objeto de contexto como argumento:

Este método viola la Ley de Deméter porque necesita recorrer un gráfico de objetos para obtener la información requerida para hacer su trabajo. Pasar información innecesaria a clases y métodos perjudica la capacidad de prueba.
Imagine un enorme objeto BillingContext que contiene referencias a otros objetos:

Como podemos ver, nuestra prueba está repleta de información no esencial. Las pruebas que crean gráficos de objetos complejos son difíciles de leer e introducen una complejidad innecesaria.
Arreglemos nuestro ejemplo anterior:

Siempre debe pasar dependencias directas a sus clases y métodos. Sin embargo, pasar muchos argumentos a métodos tampoco es una buena práctica; idealmente, debería pasar dos argumentos al máximo o envolver argumentos relacionados en objetos de datos.
Dios objeta
El objeto de Dios es un objeto que hace referencia a muchos otros objetos distintos, tiene más de una responsabilidad y tiene múltiples razones para cambiar. Si es difícil resumir lo que hace la clase, o si resumir incluye la palabra "y", es probable que la clase tenga más de una responsabilidad.
Los objetos de Dios son difíciles de probar ya que estamos lidiando con múltiples dependencias no relacionadas, mezclando varios niveles de abstracciones y preocupaciones, y producen muchos efectos secundarios. En consecuencia, es difícil lograr el estado deseado para nuestros casos de prueba.
Por ejemplo:

UserService tiene más de una responsabilidad: registrar nuevos usuarios y enviar correos electrónicos. Mientras probamos el registro de usuario, debemos tratar con el servicio de correo electrónico y viceversa:

Imagine un UserService con más de dos dependencias no relacionadas. Estas dependencias tienen sus propias dependencias, y así sucesivamente. Terminaríamos con una prueba que es ilegible, inflada con información no relacionada y muy difícil de entender. Por lo tanto, cada clase debe tener una sola responsabilidad y razón para cambiar. Una clase que tiene una sola razón para cambiar es uno de los cinco principios de diseño de software llamado principio de responsabilidad única.
Puede leer más sobre los principios SOLID aquí.
Conclusión
Una base de código que sigue las mejores prácticas de diseño de software hace que escribir pruebas unitarias sea mucho más manejable.
Por otro lado, puede ser muy desafiante, o incluso imposible, escribir una prueba de unidad de trabajo para una base de código utilizando los antipatrones mencionados. Escribir un buen código comprobable requiere mucha práctica, disciplina y esfuerzo extra. La ventaja más significativa del código comprobable es la facilidad de comprobación y la capacidad de comprender, mantener y ampliar ese código.
Esperamos que este blog lo ayude a escribir código comprobable.