TDD, Test Unitarios y Mocks

En el mes de Junio pasado dí una charla en lína sobre TDD usando ASP.NET MVC, gracias a la gente de ALT.NET Hispano. Pueden verla aquí. Usé "baby steps”, pasos de bebé, para ir escribiendo el código, usando el ciclo TDD “escribir el test, rojo, verde, refactor”. Tengo otros ejemplos similares, en mis posts sobre TDD de este blog, y en mi cuenta de GitHub. Usualmente, no escribo ni mocks ni stubs. Puedo ser clasificado como un miembro de la escuela “clásica” de TDD, en contraste con la escuela “mockista”. Mi referencia a esta dicotomía (que no tiene que ser tajante) es el artículo de Martin Fowler:

Mocks Aren’t Stubs

Esto es: sólo agrego un mock si REALMENTE lo necesito. Pero alguien podría decir: “Ah! Yo también, sólo los agrego cuando los necesito, pero LOS NECESITO TODO EL TIEMPO”. Yo no pienso de esa manera: los mocks nos son algo imperativo en la programación TDD. El propósito de este post es mostrar que semejante aserción “necesito mocks siempre” es una falacia, que deriva de un entendimiento parcial de lo que es un test, y en especial, un test unitario. Uds. pueden luego decidir usar o no usar mocks y stubs. Pero los mocks sólo son REALMENTE necesarios en TDD en algunas circunstancias (otra historia es la necesidad de mocks en test en general, fuera de TDD, por ejemplo, en “escribo test después del código sólo para tener buen code coverage”).

Leo en ese artículo de Fowler:

xUnit tests follow a typical four phase sequence: setup, exercise, verify, teardown. In this case the setup phase is done partly in the setUp method (setting up the warehouse) and partly in the test method (setting up the order). The call to order.fill is the exercise phase. This is where the object is prodded to do the thing that we want to test. The assert statements are then the verification stage, checking to see if the exercised method carried out its task correctly. In this case there’s no explicit teardown phase, the garbage collector does this for us implicitly.

Sí, usualmente escribo el test con una parte de “setup” o “arrange”, que arma el escenario del test. La escual mockista escribiría los mocks (vean el mismo ejemplo en el artículo de Fowler, usando mocks). De ese artículo, se ve que el mismo test, SE PUEDE escribir CON y SIN mocks. Fowler escribe:

The classical TDD style is to use real objects if possible and a double if it’s awkward to use the real thing. So a classical TDDer would use a real warehouse and a double for the mail service. The kind of double doesn’t really matter that much.

A mockist TDD practitioner, however, will always use a mock for any object with interesting behavior. In this case for both the warehouse and the mail service.

Veo que todo este tema termina relacionado con lo que llamamos y entendemos por Test Unitario. No hay una clara definición del término: muchos libros de TDD sólo mencionan test, o ponen la expresión “test unitario” de vez en cuando, sin definirla. Voy entonces a la wikipedia:

http://en.wikipedia.org/wiki/Unit_testing

In computer programming, unit testing is a method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine if they are fit for use.[1] Intuitively, one can view a unit as the smallest testable part of an application. In procedural programming a unit could be an entire module but is more commonly an individual function or procedure. In object-oriented programming a unit is often an entire interface, such as a class, but could be an individual method. [2] Unit tests are created by programmers or occasionally by white box testersduring the development process.

La parte clave es:  un Test Unitario ejercita, prueba, “pone en la fragua” una PARTE del software, usualmente un método individual. En la parte de “act” (dentro de “arrange/act/assert”) pongo un solo comando: la llamada al método que estoy probando, nada más, nada menos.

PERO MUCHA GENTE piensa: esa llamada al método, DEBE INVOLUCRAR SOLO al código de ese método, y no meterse en el código de las otras partes que ese método usa y llama. Bien, eso no es imperativo en TDD. El primer ejemplo de Martin Fowler muestra claramente que se pueden usar otras instancias y métodos. Y esas instancias pueden ser código real de producción, no necesariamente mocks ni stubs. Esa es la escuela clásica de TDD en acción.

Sigo leyendo en la Wikipedia:

Ideally, each test case is independent from the others: substitutes like method stubs, mock objects,[3] fakes and test harnesses can be used to assist testing a module in isolation.

Acá la clave está en lo de “ideally”. Idealmente para qué? Para poder probar algo de forma aislada, para lograr “isolation”. Pero como toda solución, tiene sus pros y sus contras IMNSHO (In My No So Humbler Opinion :-) perseguir el aislamiento DEBE SER EVALUADO, en términos de costo versus beneficios, NO DEBE SER tomado como un fin en sí. Mi experiencia me muestra, entonces: no vale la pena llegar a eso usando mocks. Usándolos, hay que agregar parvas de código, solamente para conseguir el aislamiento de una prueba unitaria. En cambio, uso otros caminos, como usar código real en el contexto de un escenario que trato de armar de forma ágil. Fowler menciona Object Mother:

In practice, classic testers tend to reuse complex fixtures as much as possible. In the simplest way you do this by putting fixture setup code into the xUnit setup method. More complicated fixtures need to be used by several test classes, so in this case you create special fixture generation classes. I usually call these Object Mothers, based on a naming convention used on an early ThoughtWorks XP project. Using mothers is essential in larger classic testing, but the mothers are additional code that need to be maintained and any changes to the mothers can have significant ripple effects through the tests. There also may be a performance cost in setting up the fixture – although I haven’t heard this to be a serious problem when done properly. Most fixture objects are cheap to create, those that aren’t are usually doubled.

En mi ejemplo del video que mencioné alprincio, uso algo similar al “object mother” de Fowler, un objeto que prepara el escenario bajo el cual voy a ejecutar mi prueba unitaria. En general, preparo los objetos del dominio, leyéndolos de objetos JSON. Ese “object mother” SOLO APARECE cuando lo comienzo a necesitar en TDD. Recuerden: TDD favorece el uso de YAGNI: no agregar algo que no necesitamos todavía. En muchos proyectos, siguiendo “baby steps”, termino usando un dominio en memoria, con una base de datos/repositorios en memoria. Lean In memory database del mismo Fowler. Un ejemplo de la evolución de mi código siguiendo ese camino (con commits atómicos, casi por tests) en: https://github.com/ajlopez/TddOnTheRocks/tree/master/TddApp. Y en ese ejemplo, comencé programando el controlador, siguiendo un camino “top-down”. Se puede escribir TDD de forma “top-down” o “bottom-up” CON y SIN mocks. El uso de mocks no está relacionado ni con TDD ni con la dirección del armado del código.  Algo relacionado y muy interesante referencia es el artículo de @unclebobmartine: No DB:

The center of your application is not the database. Nor is it one or more of the frameworks you may be using. The center of your application are the use cases of your application.

What is the best time to determine your data model? When you know what the data entities are, how they are related, and how they are used. When do you know that? When you’ve gotten all the use cases and business rules written and tested. By that time you will have identified all the queries, all the relationships, all the data elements, and you’ll be able to construct a data model that fits nicely into a database.

Does this change if you are using a NoSql database? Of course not! You still focus on getting the use cases working and tested before you even think about the database; no matter what kind of database it ends up being.

Yo pude, varias veces, seguir ese camino, y SIN escribir un SOLO MOCK.

Qué pasa con la pérdida del aislamiento? Por lo menos, en mis proyectos, no ha sido un probelma. Si rompo algo “grande”, puedo tener 40 tests en rojo, pero una rápida inspección me dice que 15 son tests de controladores, 20 en una capa de servicios lógicos y 5 en un repositorio. Entonces, el problema está altamente relacionado con el repositorio. El aislamiento a priori no me fue necesario.

Recientemente, estuve trabajando en un equipo, con gente de diferentes estilos. Algunos no usaban TDD, o sólo escribían "test para aumentar el code ccoverage”. Otros usanban TDD pero había representantes de ambas escuelas, la “clasista combativa” :-) y la mockista. Entonces, cuando había que refactorear una implementación, los test clásicos relacionados con el cambio QUEDABAN en VERDE automáticamente. En cambio, los tests mockistas se rompían, porque estaban muy ligados a lo que se esperaba en la implementación actual o interna. Yo veo ahí que los mocks son una mochila, algo que nos pesa y va en contra del refactor ágil.

¿Cuándo usar, entonces, un mock o un stub? Bueno, esa decisión se las dejo a Uds. Pero quiero mosntrar que es una decisión a tomar en un contexto, no es cuestión de decir “hay que usar mocks siempre, porque eso es TDD”. Realmente, es rara la ocasión en la que tenga que usar un mock en mis proyectos personales. Un ejemplo: puedo tener código que no escribí, un caso: un servicio web externo que tengo que consumir. Entonces, en mis tests, no llamo al servicio web externo (que puede no estar siempre accesible, o tener problemas de latencia). Tampoco un stub. Escribo una implementación que simule el servicio web y LO ENTREGO en la aplicación final de la iteración. No es código sólo para test. Luego, con el tiempo, voy preparando la aplicación para que se pueda configurar, a gusto del consumidor, si quiere usar esa implementación “liviana” o el servicio web real. De esta manera, puedo ir manteniendo el proyecto ágil, y acercarme en cualquier momento a la implementación real.

¿Alguna situación donde usar mocks sea necesario? Hmm.. puede ser en un equipo distribuido, o que tiene repartidas las tareas. El equipo A tiene a su cargo escribir los controladores de una aplicación ASP.NET, y el equipo B tiene que escribir los servicios lógicos, el dominio a consumir. Mientras el equipo B los escribe, el equipo A puede usar mocks de ellos, para tener listos sus controladores. Esa es una situación, en mi opinión, más “legitimada” para usar mocks.

¿Comentarios? ¿Otros pros y contras de las escuelas clásica y mockista?

Podría parafrasear a un político norteamericano del siglo XIX y decir: “El único mock bueno es el mock muerto, el que no se escribe” :-)

Nos leemos!

Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez

This entry was posted in 10549, 11699, 3463. Bookmark the permalink.

One Response to TDD, Test Unitarios y Mocks

  1. Jose says:

    Muy interesante post. Ví la presentación de Alt.Net y me pareció muy esclarecedora. Lo que quería preguntar es lo siguiente: ¿cómo lograr vencer las ganas de escribir un repositorio desde el vamos cuando ya está avanzado el desarrollo? Quiero decir, en la presentación, fuiste llevando desde una lista fija de entidades hasta un repositorio, todo con baby steps. Ahora bien, después de unas semanas de desarrollo, ves que siempre terminás teniendo un respositorio. Mi pregunta entonces es, ¿cuál es la actitud ante ese escenario? ¿Seguís haciendo los baby steps desde la nada hacia el repositorio? ¿o salteas pasos y escribis directamente el repositorio?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>