Grimpi IT Blog

diciembre 20, 2010

Programando bajo Assumptions Driven Development (ADD)

Filed under: Arquitectura, Opinion — grimpi @ 4:24 pm

Antes de empezar este post, quiero advertir al lector, que el dueño de este blog no está muy de acuerdo con el modo en que se está desarrollando software actualmente. La mayoría de las posturas del autor, son contrarias a las del “mainstream” informático.

Una de las cosas que actualmente se utiliza mucho en el desarrollo de software, son los supuestos. Qué pasa si el día de mañana cambiamos de base de datos? Qué pasa si el día de mañana en vez de escribir en una base de datos, escribimos en un xml? Qué pasa si el día de mañana cambiamos de framework de logging? Qué pasa si el día de mañana nuestro sistema desarrollado para correr en un servidor Windows 2008 debe correr en un celular Android?
En definitiva, planteamos una serie de supuestos posibles cambios tecnológicos o de infraestructura que podría sufrir nuestra aplicación, pero nunca evaluamos la posibilidad de que tal cosa efectivamente suceda. Y estos supuestos no son gratis, porque requieren de capas de abstracciones que agregan más código, necesidad de uso de frameworks y nos limitan nuestras posibilidades.
Es lo que yo llamo Assumptions Driven Development (ADD). Desarrollo a base de supuestos.
Lamentablemente hoy en día, es casi la regla trabajar bajo esa “metodología”, pero cuya importancia esta terriblemente sobrevalorada. El gran mito de la portabilidad.

Si una empresa trabaja hace 10 años con un motor de base de datos especifico, por ejemplo, SQL Server y no hay ningún proyecto a corto y mediano plazo de migrar a Oracle, cual es el sentido de hacer una aplicación especifica para esa empresa que sea portable por si el día de mañana se cambia de motor, sabiendo que es muy poco probable que tal cosa suceda? Y como dije antes, los supuestos no son gratis. Nos limitan tanto en productividad como en performance. Si yo quiero hacer que mi sistema sea portable, entonces estoy desaprovechando muchas características que son propias de una determinada tecnología. Puedo usar store procedures en vez de tener un ORM que me autogenere consultas SQL genéricas, puedo usar características avanzadas de SQL Server incorporadas en SQL 2005 y 2008 que nhibernate o cualquier ORM no soportan. Lo mismo aplica a otros motores. PostgreSQL tiene algo fantástico que son los campos vectores. Es algo muy específico de PostgreSQL y lamentablemente si quiero portabilidad, difícilmente pueda usarlos.
Y no solamente con las bases de datos. La metodología ADD se usa en casi todas partes y junto con la utilización de mocking, es responsable del gran auge que está teniendo los frameworks IoC y la inyección de dependencias. Realmente necesito inyección de dependencias en mis clases para otra cosa que no sea mocking? Realmente necesito crear tantas interfaces para mis clases puedan ser reemplazadas por otras? Puede que sí, depende de las características del negocio y la aplicación, pero si vamos a usar DI solamente para un eventual cambio de librería de logging o de acceso a base de datos, me parece que hay que hacer un stop y pensar nuevamente si realmente vale la pena ensuciar el código con esto.
Porque ese es uno de los costos que pago para ser en muchos casos, innecesariamente flexible. Y la realidad que el uso de tantos frameworks a veces a uno le hace perder un poco el control de la aplicación.
Otro supuesto también muy común, es la escabilidad. Nuestro sistema “debe ser” escalable de antemano, aun si estamos desarrollando un software especifico para una pyme que van a usar 20 empleados. Esto significa hacer uso de estrategias de caching (con su respectivo framework) muchas veces innecesariamente.
La pregunta que hay que hacer es: tiene sentido pensar en portabilidad si la vida util del sistema es de 3 o 4 años maximo? Tiene sentido tanta flexibilidad y portabilidad? Estos supuestos, es común que ocurran? Y la verdad que no siempre sucede.
Y me atrevería a decir, que son minoría.

Por supuesto que si yo estoy desarrollando un software ERP genérico que quiero venderlo al mercado, la portabilidad es una gran ventaja, porque me permite venderlo a clientes que utilicen distintas plataformas. En ese caso, cuando se inicia el desarrollo del proyecto, no queda otra que pensar en portabilidad. Pero la mayoría del software que hacemos en general, no es genérico. Es específico para una empresa particular cuya infraestructura no suele ser migrada radicalmente o es una página web en donde los cambios de arquitectura e infraestructura tienen otra dinámica totalmente distinta y generalmente, cuando se produce un cambio de este estilo, es necesario reescribirla gran parte y no por una cuestión de no haber pensado en estos supuestos cambios, sino porque los casos de uso de nuestra aplicación cambiaron mucho. La vida util de un sistema web suele ser por otra parte mucho mas corta.

Ojo, yo no planteo tirar por la borda todo y no pensar en posibles cambios tecnológicos que puedan existir en la aplicación, pero si tener un poco más de cuidado, evaluar los costos que significan estos supuestos y ver si realmente existe la posibilidad de que tal cosa exista. Dejar de tener miedo en “casarse” con una tecnología y aprovechar al máximo, todas las bondades que ofrece.

diciembre 17, 2010

Como hacer unit testing contra la base de datos?

Filed under: .NET, Arquitectura, Mocks, Test Driven Development — grimpi @ 6:30 pm

Hoy en día el desarrollo de software, no se concibe si unit testing. Hasta el famoso “Hola Mundo” debe tener su respectiva clase de testing…
Uno de los principales problemas que surge a la hora de crear los test unitarios, es como probar contra la base de datos.
Donde está el problema? Supongamos que hacemos un test unitario “Login” que valide el usuario con una password incorrecta ingresada a propósito para ver falla el login. Y supongamos que en la lógica de negocio, al tercer intento fallido de login se bloquee el usuario.
Que va a pasar si tenemos otro test unitario, que intenta probar el login exitoso si ya testeamos tres veces el test unitario anterior? Va a fallar, porque en la base de datos, el usuario quedo marcado como bloqueado.

Para ser más claro, si nuestros test unitarios “ensucian” la base de datos, existe una gran posibilidad de que fallen y no por culpa de que están mal escritos o hay un bug en el código, sino porque los datos no son los que esperamos que fuesemos.

Para solucionar este problema, existen diversas técnicas:

  • Resetear los datos de la base de datos antes de cada tests.
  • Hacer que el test corra dentro de una transacción distribuida y luego hacer un rollback de la transacción.
  • Resetear solo los datos usados por el test, después de que este haya corrido.
  • Usar mecanismo de versionado de datos incorporados en las nuevas versiones de SQL Server o Oracle (flashback tables).
  • Separar el test unitario de la base de datos (mocks).

Obviamente cada una de estas técnicas, tienen sus respectivas ventajas y desventajas. Vamos a empezar de la simple, a la más compleja.

Resetear los datos de la base de datos antes de cada tests.
Esta es la primera opción que viene a la mente. La principal ventaja es su facilidad de implementación.
Una opción practica para hacer esto es tener un backup de la base de datos “limpio” y restorearlo cada vez que se ejecuta un unit testing.
Otra opción es tener un set de datos especifico necesario para el testeo y cargarlo manualmente. Luego del testo, estos datos son reseteados. Existen frameworks como NDbUnit (hhttp://code.google.com/p/ndbunit/) o TDUnit (http://tdunit.codeplex.com/) que nos facilitan enormemente esta tarea. Básicamente funcionan configurando un xml de datos y llamando a una instrucción de carga de datos en cada test unitario.

Ejemplo de código con NDBUnit:

public void Test()
{
string connectionString = “server=localhost;user=dbuser;password=dbpassword;initial catalog=MyDatabase;” ;
NDbUnit.Core.INDbUnitTest mySqlDatabase = new
NDbUnit.Core.SqlClient.SqlDbUnitTest(connectionString);

mySqlDatabase.ReadXmlSchema(@”..\..\MyDataset.xsd”);
mySqlDatabase.ReadXml(@”..\..\MyTestdata.xml”);
}

La principal desventaja de este sistema es obvia. Performance. Limpiar y cargar los datos por cada test unitario, puede ser algo extremadamente costoso en cuanto a tiempos.

Hacer que el test corra dentro de una transacción y luego hacer un rollback de la transacción

Este mecanismo es tentadoramente simple, sin embargo, pocas personas lo deben usar.
Cada test que se corre, está dentro de una transacción distribuida (COM+) que al finalizar, hacemos un rollback. De esta manera extremadamente simple, mantenemos el estado de los datos de la base de datos en cada test.
El problema de este enfoque, es que las transacciones distribuidas son las lentas que las locales. Por otro lado, en determinados entornos, hacer que las transacciones distribuidas funciones, requieren de cierta configuración de Windows.
Existe una extensión para NUnit llamada XtUnit, que entre otras cosas, permite ponerle el atributo [Rollback] al método de testing y automáticamente este unit testing va a correr dentro de una transacción distribuida sin necesidad de que tengamos que escribir código adicional.

Ejemplo de Test con XtUnit:

[Test,DataRollBack]

public void Insert()

{

CustomerService custom = new
CustomerService();

custom.add(“Esteban”,29,21452367);

}

Ejemplo de test sin NUnit solo:

[Test]
public void Insert()
{
using (TransactionScope scope = new TransactionScope())
{
CustomerService custom = new
CustomerService();

custom.add(“Esteban”,29,21452367);
scope.rollback();
}
}

Resetear solo los datos usados por el test, después de que este haya corrido

Este enfoque evita tener que resetear toda la base de datos antes de que el test sea ejecutado. En cambio, se carga una serie de comandos típicamente de insert/delete antes y después de cada test. Las buenas noticias es que de esta manera el menor el trabajo adicional que se ejecuta y por lo tanto, mejora significativamente la performance.
El problema de esta técnica, es que uno debe conocer con antelación que datos va a necesitar el test y si nos olvidamos de insertar un dato necesario, puede que falle el test unitario no por un bug o error de diseño, sino por un simple olvido a la hora de insertar un registro en una tabla antes de que sea ejecutado el test.

Usar mecanismo de versionado de datos incorporados en las nuevas versiones de SQL Server o Oracle

Este es un enfoque bastante interesante y que nos ahorra escribir código. Las últimas versiones de las bases de datos modernas, incorporan la capacidad de hacer versionado de datos sobre las tablas. Cada vez que se hace un insert/update/delete, el dato anterior es guardado en otra tabla interna del motor con un timestamp, existiendo la posibilidad más adelante de consultar el estado de los datos en un momento determinado y de volver la tabla a ese estado.
En Oracle esta característica se llaman Flashback table y en SQL Server Change Tracking.
La ventaja de esta técnica es que no tengo que escribir manualmente el reseteado de los datos ni tengo que lidiar con transacciones distribuidas. Sin embargo, las desventajas son múltiples. En primer lugar, esta técnica solo funciona con las versiones más modernas de las bases de datos Oracle, SQL Server y Postgres. Por ejemplo, no sería posible hacerlo con SQL Server 2005. MySQL en ninguna versión tiene esta característica. Por otra parte, habilitar esta opción en la base de datos tiene un altísimo impacto en performance y en tamaño en disco. Por último, para habilitar esta opción, hay que hacerlo desde el motor de la base de datos.
Para pesar de todas estas desventajas, creo que es una técnica útil en determinados casos puntuales. Especialmente en situaciones donde no tenemos tiempo o no podemos escribir código.

Separar el test unitario de la base de datos (mocking)

Aquí presentamos la opción supuestamente “correcta” de la industria actualmente. Evitar a toda costa el test unitario contra la base de datos y reemplazarlo por mocks, que son clases que emulan el comportamiento de una base de datos (o de cualquier otra cosa que queramos, como web services por ejemplo), de tal manera que podemos probar si el método funciona como deseamos sin conectarse con la BD.
Existen varios frameworks de mocking (NMocks, Rhino, Typemock y Moq). En general son bastante parecidos en cuanto a funcionalidades básicas, aunque algunos son un poco más claros que otros. Personalmente me siento más cómodo trabajando con Moq.

Usar mocking como método de testeo unitario en reemplazo de la base de datos tiene varias ventajas. La primera es la velocidad. No es lo mismo ejecutar 300 test unitarios contra la BD que contra clases en memoria.

La segunda ventaja es la inmutabilidad de los datos. Al no correr contra una base de datos, no se modifica realmente ningún dato, lo que permite que se pueda simular siempre el mismo estado. Para esto es necesario que se cargue al objeto mock con el que se prueba, los valores iniciales que se necesitan, para que el test unitario no falle. O sea, el uso de mocking nos evita todos los problemas que vimos en las 4 soluciones del anterior post.

El uso de mocking también agrega mas previsibilidad al test unitario, cuando no tenemos mucho control sobre la fuente contra la que se prueba o esta tiene un comportamiento indeterminado, como por ejemplo, un monitor de red que prueba x tipo de paquetes. Son situaciones muy específicas y puntuales, pero en esos casos el uso de mocking resulta casi indispensable.

Pero este enfoque también tiene sus desventajas. En primer lugar, no es una prueba “real”. Es una simulación. Hay situaciones en las que un test unitario contra un mock puede resultar exitoso, pero en un escenario más real, contra la base de datos, puede que falle. Por ejemplo, en el caso de que un método llame un store procedure y devuelva determinado conjunto de datos. Si el store procedure fue por cuestiones de negocio modificado sin nuestro conocimiento de tal manera, que el resultado de salida cambie, si probamos contra el mock nunca nos vamos a enterar de esta modificación.

También puede darse el caso de que haya cambios en la estructura de una tabla o falten permisos. Todas situaciones que si no tenemos aviso, podrían hacer que el test unitario corra exitosamente cuando no debería.

Otra desventaja de esta técnica, es que no es inerte a como está el código de la aplicación. Para ser más claro, para que una clase de nuestra aplicación, sea “mockeable” debe estar programada para esto.

Debe estar hecha de tal manera, que uno pueda inyectar instancias distintas. Uno puede decir y de manera bastante correcta, que es una buena práctica que las clases permitan inyección de código. Puede ser, pero también es discutible. Depende de las características de la aplicación.

Pero el caso es que uno no puede mockear una clase si no pensó antes en que iba a usar esta técnica de test unitario. Claro que si uno trabaja con TDD no tenga este tipo de problemas, pero no todo el mundo trabaja así y ni tiene porque hacerlo.

Otra desventaja es que uno tiene que decirle a la clase mock, que valores espera. Las expectations. Ejemplo:


Lo que resulta un poco engorroso a veces si la función es compleja.

Como verán, soy bastante crítico de este tipo de soluciones. Mi principal problema es que agregan complejidad al código y más complejidad, es mayor probabilidad de errores. El test unitario tiene que ser lo más simple posible, tener la menor cantidad de errores. Y agregar mocks significa todo lo contrario.

Por supuesto, que se gana en velocidad. El tema es el siguiente: Constantemente estamos corriendo 300 test unitarios? Yo personalmente solo corro los tests unitarios de los métodos y clases que estoy tocando en este momento o calculo que pueden llegarse a verse afectados. En todo caso, al final del día, se corre todos los test para verificar que no se suba nada que rompa el código, pero tiene sentido hacerlo continuamente?

Y si no es así, entonces cuanto es la ganancia de velocidad?

También uno puede plantear soluciones intermedias. Probar las clases repository o DAO o aquellas clases de bajo nivel cuya función es trabajar con la base de datos sin mockeo y las clases de negocio, usando mock. De esta manera, al probar tanto contra la base de datos y contra una clase mock, estoy ganando fiabilidad en el test (al probar los repository directamente contra la BD) y por otro lado, también gano velocidad al evitar testear las clases de negocio contra la BD.

Muchos dicen por otra parte, que no hay que confundir un test de integración con un test unitario. El test de integración se prueba con la base de datos y el test unitario no. Es una práctica habitual escribir estos 2 tipos distintos de test para una misma función. Es una opción bastante prolija, pero el costo es la duplicación de código de testing.

De todas maneras, más allá de los gustos personales de cada uno, esta solución existe y puede ser altamente recomendable en determinadas situaciones puntuales.

Como ya deberíamos saber, cuando uno desarrolla, existen múltiples formas de hacer lo mismo indistintamente en el 80% de los casos. Pero siempre, existe ese 20% restante, donde determinada forma es claramente más conveniente que las otras.

Crea un blog o un sitio web gratuitos con WordPress.com.