ASP.NET MVC y otras Yerbas – Parte 2

En esta segunda parte vamos a ver el diseño e implementación del modelo de negocio, con lo cual seguiremos viendo conceptos de DDD; pero también agregaremos uno nuevo a nuestra creciente lista de términos raros, este es el “revolucionario” TDD (Test Driven Development).

Primero tendremos una pequeña introducción para entender cuales fueron los fundamentos que impulsaron la creación y expansión de TDD, y luego pasaremos a dar unas pequeñas definiciones.

El Problema

Los errores (bugs) que se producen dentro de una aplicación se podrían clasificar en 3 tipos:

  • Lógicos: A lejos son los errores más comunes y típicos, estos se encuentran dentro de los “if”s, “for”s y código similar dentro de la aplicación.
  • Funcionales: Estos se producen debido a una interacción incorrecta entre dos o más objetos, dicho de otra manera cuando el resultado de un objeto no es el esperado como entrada para un segundo objeto.
  • Gráficos: Cuando los datos mostrados sobre una interfaz gráfica no son los correctos: texto cortado, datos incompletos o inclusive cuando no se ve bien ( ¿Cómo es verse bien?).

Cuando hablamos de estos 3 tipos de errores es importante tomar en cuenta la frecuencia en la que se encuentran dentro de nuestras aplicaciones:

Frecuencia Aparición de los Errores

Pero no solo su frecuencia, sino también el costo que tiene encontrarlos y corregirlos:

Tabla Comparativa Errores

Los errores lógicos son claramente los más difíciles de encontrar, debido a que estos se presentan como consecuencia de un correcto número de entradas y condiciones, y encontrar ese mágico número de entradas hace que esta tarea sea difícil; asimismo para dar un solución se necesita entender todo el código que rodea el problema y que esta a su vez no cause errores en otras partes de la aplicación.

En este punto nos damos cuenta la verdadera importancia que tiene contar con medios que minimicen la frecuencia en la que se producen estos errores, no solo los lógicos sino también los funcionales y gráficos, y que estos medios también permitan encontrar y solucionar los errores de una manera efectiva.

Por otro lado, muchas veces se entiende que el proceso sobre el cual se implementa una aplicación involucra dos tareas bien definidas: desarrollar la aplicación por los ingenieros de software y realizar las pruebas manuales por las personas de QA.

Lo que normalmente se produce durante el desarrollo de estas tareas es que al encontrar un error durante las pruebas, se devuelve la aplicación a las personas de desarrollo para que corrijan los errores, para luego pasarla nuevamente a las personas de QA, para que revisen nuevamente de forma manual toda la aplicación ya que funcionalidad existente se ha podido ver afectada por los cambios realizados.

Los problemas que normalmente encontramos durante este ciclo son:

  • Se pierde mucho tiempo en buscar y corregir errores en las etapas finales de un proyecto.
  • Realizar pruebas manuales de toda la aplicación aumenta aún más el tiempo de búsqueda de errores; esta tarea se puede realizar múltiples veces durante el proyecto, por lo que se vuelve en algo tedioso, aburrido y se deja muchas veces de lado, lo cual contribuye a pasar errores a producción.
  • Por la necesidad de disminuir el tiempo y el trabajo se intenta automatizar las pruebas mediante algún tipo de herramienta casi “mágica”, ya que el diseño de la aplicación podría no estar preparado para una automatización, lo cuál lleva en muchos casos a una tarea casi imposible, con problemas y sin beneficios reales.

Para solucionar estos problemas necesitamos contar con un medio que nos permita encontrar errores desde etapas tempranas del proyecto, asimismo debe poderse realizar de manera rápida y repetitiva.

La Solución

Creo que se imaginan cual es la solución que el post pretende mostrar, pero en realidad son 3 soluciones: Unit Testing, TDD and Refactoring. Los tres significan cosas diferentes, tienen beneficios diferentes y problemas diferentes que pretenden resolver. A estas alturas puede ser todavía un poco confuso pero vamos a ver las diferencias entre estos:

Unit Testing

“A unit test is a piece of a code (usually a method) that invokes another piece of code and checks the correctness of some assumptions afterward. If the assumptions turn out to be wrong, the unit test has failed.” (Roy Osherove – The Art of Unit Testing with Examples in .NET)

Ejm:

public bool UsuarioValido(string usuario,string password)
{
        if(usuario=="juan" && password=="1234")
              return true;
         else
              return false;
}

Imaginemos que tenemos el código anterior en nuestra aplicación, entonces por lo menos se necesitaría 2 pruebas lógicas para verificar que el método es válido ( una prueba donde se ingresen los datos correctos verificando que nos devuelva verdadero y otra con los datos incorrectos verificando que nos devuelva falso).

Algunas de las características de un buen unit test son:

  • Son automatizados: cualquier tarea en la cual se tenga que realizar alguna configuración o un proceso tedioso, es muy probable que se deje de hacer; es por esto la necesidad de tener test automatizados, para que estos se puedan realizar de manera repetitiva por todos los miembros del equipo.
  • Se pueden realizar en cualquier momento del tiempo: esto permite observar que cualquier modificación que se realice al código, durante o mucho después de su desarrollo, devuelve los resultados correctos y a la vez indica si no se ha alterado algo ya existente. Con esto podemos mejorar constantemente el código y agregar nueva funcionalidad sin ninguna clase de miedo.
  • Se ejecutan en pocos segundos: los test unitarios buscan probar pequeñas partes lógicas de código y no el funcionamiento de recursos externos como base de datos u otros, por lo tanto estos devuelven resultados rápidamente lo que facilita su ejecución constante.
  • Son entendibles: cuando nos entregan una gran documentación y un apéndice con ejemplos, ¿Qué es lo que primero vemos?, los ejemplos por supuesto. Los unit test son exactamente eso: “documentación” ya que nos permiten observar que es lo que hace realmente la aplicación y como son código es lo que prefieren ver los ingenieros de software para conocer el sistema.
  • Son fáciles de desarrollar: hace algún tiempo un jefe de proyecto, con cara de preocupación, me dijo: “Entonces se escribe el doble de código!!”….Pues SI, como vimos anteriormente para un simple método que valida un usuario se necesitan 2 pruebas lógicas, esto  hace que el código total(producción+pruebas) aumente considerablemente. Pero lo que el JP no sabía es que existen diferencias claras entre el tiempo y dificultad que toma hacer un test y el que toma realizar el código de producción, esta diferencia es muy grande, en relación 1:10 como lo muestra el siguiente artículo
    De todas formas igual existe un pequeño tiempo adicional, pero consideremos este tiempo como una inversión a corto y largo plazo, ya que ahorraremos mucho tiempo al no levantar toda la aplicación y probar manualmente, disminuiremos el tiempo de uso del debug, menos tiempo al corregir y encontrar errores, menos tiempo al realizar pruebas de regresión, etc.

TDD

Se le atribuyen muchas definiciones a TDD: hacer las pruebas primero, tener muchas pruebas, crear una aplicación en base a pruebas desde cero (sin arquitectura previa ni ningún otro diseño anterior);  pero la mejor definición de TDD es una combinación de las anteriores “TDD es escribir las pruebas primero y dejar que estas guíen o modifiquen el diseño, sin necesariamente comenzar desde cero”.

Para dejar bien en claro la definición: “TDD is not about testing, it´s about design

Pero por que se dice que TDD ayuda al diseño: esto se debe a que la presencia de los test nos obligan a pensar que la aplicación ya no es la única que va a utilizar el código sino este también va a ser utilizado por lo test, lo que nos lleva a tener que desacoplar debidamente las clases, separar sus responsabilidades, usar patrones, etc, en general mejorar el diseño. Una frase que resume esto es “Listen the Test”, ya que si el código es difícil o no se puede probar probablemente signifique que no existe un buen diseño.

En la siguiente imagen podemos observar cuales son los pasos en el ciclo de TDD:

TDD Process

Mas fácil todavía: Escribir la prueba –> Escribir el código que cumpla la prueba –> Limpiar el código (refactorizar)

Asimismo se siguen 3 reglas bien simples:

  1. No esta permitido escribir código de producción, al menos que sea para hacer pasar un test unitario fallido.
  2. No esta permitido escribir más código en el test, que el suficiente que haga fallar el código de producción.
  3. No esta permitido escribir más código de producción, que el suficiente para hacer pasar un test fallido.

Nos olvidamos de un aspecto muy importante, ¿Porqué realizar el test primero? , en realidad son muchas razones por las cuales se recomienda hacer los test antes y no después:

  • Permite separar el qué es lo que necesita la aplicación del como es que esto se implementará, esto ayuda a no escribir más funcionalidad de la necesaria y permite entregar un producto que representa las necesidades reales del cliente y no cosas que nunca va a utilizar.
  • Permite que nos concentremos y pongamos nuestra toda nuestra atención en solo una pequeña pieza de funcionalidad lo que nos ayuda a realizarla de una manera más rápida.
  • Definitivamente es más difícil encontrar algún error en 50 líneas de un método terminado que en 3 líneas de un método que esta creciendo de manera incremental.
  • Te brinda más información y de manera constante en comparación a una prueba realizada al final, lo que permite que el código pueda mejorarse de manera más rápida. Posiblemente no se tenga ganas de mejorar el código si es que la prueba se realiza al final y aún peor, posiblemente ni se realice la prueba (si escribes las pruebas al inicio te aseguras de que las pruebas realmente se hagan)
  • Escribir buenas pruebas sobre código existente es más difícil que hacerlas de forma inicial.
  • Es más divertido escribir las pruebas al inicio =).
Refactoring

“Refactoring: a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior.” (Martin Fowler -Refactoring: Improving the Design of Existing Code)

Mientras que TDD se encarga de informar sobre nuestras decisiones de diseño, refactorizar se encarga de la adecuada modificación a dichas decisiones de diseño y para realizar esto se necesitan ciertas habilidades y conocimientos en patrones y principios de diseño.

Haciendo un resumen violento:

Unit Testing => como hacer buenos test.
TDD => realizar las pruebas primero y
Refactoring => como modificar adecuadamente tu diseño.

Palabras Finales

Palabras Finales!!!!!!!!!!!! O.o ¿Y el código?…… lo se, lo se, sin querer el post terminó siendo más largo de lo que pensaba, pero ojala esta pequeña introducción haya servido para comprender un poco más sobre lo que es realmente TDD y que beneficios nos aporta.

Todas las cosas que hemos visto anteriormente son solo herramientas que nos ayudan a llegar a un fin: “hacer mejor software” y es nuestra decisión utilizarlas o no, tampoco son dogmas que debemos seguir al pie de la letra pero se recomienda seguir ciertos lineamientos para su mejor aplicación.

Yo, al igual que muchos, estoy convencido que estas cosas traen muchos beneficios, pero a costa de un precio: investigación, aprendizaje y práctica, mucha práctica.

Hasta el siguiente post, donde veremos todo este romanticismo pero en la vida real.

Y no se olviden de ver el resto de la serie ASP.NET MVC y otras Yerbas.
Saludos.