miércoles, 25 de febrero de 2009

Mock objects considered harmful (sometimes) [1]

Estuve leyendo un poco más sobre BDD y caí en estos artículos que explican como hacer BDD con RSpec en Ruby:

http://www.oreillynet.com/pub/a/ruby/2007/08/09/behavior-driven-development-using-ruby-part-1.html
http://www.oreillynet.com/pub/a/ruby/2007/08/30/behavior-driven-development-using-ruby-part-2.html

El segundo es más interesante que el primero. Si tienen un tiempo léanlo antes de seguir, por lo menos la parte final a partir de la cual crea la clase Game para representar el juego (Sección "Indroducing Mock Objects vis the Game Class"). Por favor, analicen críticamente el diseño que propone y los test que hace antes de seguir (por supuesto que pueden seguir leyendo si quieren, pero seguramente van a entender mejor lo que digo si lo leen).
Bueno, si lo leyeron y analizaron, deberían tener algunas críticas, ¿no?. Yo tengo un par que me parecen muy importantes, es más una hasta me hizo (y hace) dudar sobre la capacidad y experiencia de la persona que escribió pero voy a tratar de no ser muy duro puesto que es muy fácil criticar y muy difícil hacer. (Si llegaron hasta acá y se están preguntando que tiene que ver todo esto con el título, no desesperen, ya llega).
La primer critica que tengo es el acoplamiento que tienen los objetos instancias de Game[2] con la UI. Ver este acoplamiento me hace creer que la persona que escribió el artículo no conoce MCV (Model View Controller), el primer framework para desarrollar aplicaciones visuales (me atrevería a decir el primer framework) y del que se han derivado muchos patrones (si leyeron el libro de Design Patterns saben a que me refiero). 
Una de las reglas de diseño que  MVC "impone" es que el modelo nunca debe conocer la UI. La UI es un consumidor del modelo y el modelo únicamente provee comportamiento que luego las ventanas, views, etc. usarán para mostrarlo. En la solución planteada por G.Brown, objetos instancias de Game (que son objetos del modelo) conocen una UI, de la cual obtiene información como quienes son los jugadores o de cuantos puntos es el tablero. Este acoplamiento desde mi punto de vista es innecesario y puede ser resuelto fácilmente pasándole esa información a Game cada vez que se crea una instancia. O sea, la UI una vez que tiene toda la info y el usuario presionan el botón "Jugar" (o como se llame), crearía una instancia de Game indicándole quienes son los jugadores y cuantos puntos tiene el tablero, algo así:

playPushed
   ....
   game := Game playedBy: (Array with: 'Juan' with: 'Pepe') withSizeOf: 10.
   ....

De esta manera no existiría ningún acoplamiento entre Game y la UI, de hecho cualquier objeto podría crear un Game, como por ejemplo un test. Lo bueno de esta solución es que tampoco es necesario usar un mock para simular la UI (por no estar esta acoplada con Game), permite que nos concentrarnos en el modelo y que no tengamos que "adelantarnos" y "suponer" cual será el protocolo de la UI. So far, la primer crítica. Primera conclusión, seguir los principios de MVC! Segunda conclusión: El mock de UI no fue necesario
Paso a la segunda crítica: En el ejemplo del paso 15, se puede ver que se redefine el mensaje de construcción de instancia de Grid para devolver un mock que la simule. Según lo que comenta G. Brown, hacerlo evita que se tenga que usar Grid o crear instancias de esta clase "innecesariamente". No quiero sonar rudo, pero me parece un pésimo ejemplo y una muy mala conclusión. En primer lugar porque está rompiendo el encapsulamiento de Game al redefinir el comportamiento del mensaje de construcción de instancias de Grid, y lo está rompiendo porque sabe que Game "usa" Grid para representar el tablero. ¿Qué pasaría si Game decide utilizar otro objeto para representar el tablero? El test dejaría de funcionar. Claramente el hecho de que Game colabore con Grid es una decisión de Game que a nadie debería importarle y menos a un test!. Haberlo hecho hizo que el test se convirtiera en uno de "caja blanca", con todas las desventajas que sabemos que eso tiene. 
Lo mismo sucede con el mock que llama box_set y que utiliza para realizar movimientos por medio del mensaje #stub_move. Lo que está haciendo ahí en definitiva es simular lo que debería suceder en los objetos del modelo del juego, ¿pero para qué? ¿no se supone que estamos testeando el modelo? Además, si el mismo ya está hecho, ¿para qué lo simulamos? (además de romper nuevamente el encapsulamiento...). Devuelta, criticar es fácil, por lo tanto para no ser uno más del montón hay que ofrecer una solución, y para mi la solución acá es simple: ¡utilizar Game sin ningún mock! ¿Cuál es el problema de decirle a una instancia de Game que se unieron dos puntos tantas veces como sea necesario para crear el estado del juego sobre el cual se quiere testear (o empezar a testear) su comportamiento?
Bien, ahora si voy al título del post. Yo me pregunto, ¿G. Brown realmente cree que hacer esto está bueno o simplemente lo hace para utilizar (y mostrar como utilizar) mocks? Si el motivo fuese mostrar como utilizar los mocks, no me parece un buen ejemplo y hasta diría que es destructivo porque como ya sabemos, los seres humanos aprendemos de ejemplos y no quiero ni pensar todos los pobres programadores que están haciendo tests utilizando este ejemplo como guía. Si por el contrario lo hace así porque cree que es un buen diseño, mi pregunta es: ¿por qué cree que es un buen diseño? Una posible explicación es, como puse más arriba, que no conozca de MVC o de encapsulamiento, lo cual sería una lástima por todos los que lo leen; pero otra posible explicación, y es la que me motivó a poner el título del post y que veo muy a menudo últimamente, tiene que ver con la famosa frase que dice: "cuando lo único que conoces es un martillo, todos los problemas se parecen a un clavo". En este caso el martillo son los mock objects y que todo se parezca a un clavo son las soluciones que ofrece en su diseño usándolos. Lamentablemente en nuestra profesión es muy común ver como cuando aparece algo nuevo (o que parece nuevo) se lo empieza utilizar sin realizar el más mínimo análisis crítico de si se lo está usando correctamente; es nuevo, es cool, ¡usémoslo!
Eso es lo que veo muchas veces con los mocks, se los sobre-utiliza generando ejemplos perjudiciales y problemas de mantenimiento en los tests. En el ejemplo del mock para Grid: ¿qué pasaría con el test si se decide modificar el nombre del mensaje de creación de instancia de Grid? Claramente dejaría de funcionar y simplemente por el hecho de haber roto el encapsulamiento.
¿Cuándo utilizar un mock object entonces? Por lo menos para mi en este post me quedó claro que existen dos casos donde no hay que usarlos:
1) Para resolver problemas intrínsecos de diseño (como el acoplamiento de Game y UI)
2) Cuando se rompe el encapsulamiento (como el caso de Game y Grid)
En los tests, según mi experiencia, hay que usar mock objects para representar objetos que están por "afuera" del objeto que se está testeando y que este debe colaborar con para poder llevar adelante una responsabilidad. Pero mi consejo es, antes de usar un mock asegúrense que no pueden usar un objeto "de verdad"...

[1] Me imagino que todos deben conocer el famoso paper de Dijstra, "Goto considered harmful" que para algunos marcó el hito del inicio de la programación estructurada. De la misma manera que ya el "... driven development" está siendo sobre utilizado, me tomé el atrevimiento de sobre-utilizar el "... considered harmful" para llamar la atención. Espero por lo menos haber sido fiel al espíritu del artículo de Dijstra...
[2] Dicho sea de paso, Game no me parece un buen nombre para esta clase. Debería indicar por lo menos qué juego es.

4 comentarios:

Anónimo dijo...

En groovy se puede hacer algo similar usando easyb http://www.easyb.org/

Hernan Parra dijo...

La mayoría de los que defienden los mocks objects llevan los units tests al extremo. Entonces dicen que las clases hay que testarlas totalmente aisladas del mundo. Ni hablar además que les encanta definir expectativas, les gusta escribir 2 veces el programa, una vez orientado a objetos y otra orientado a expectativas. Hay un libro muy lindo que habla de patrones de testing xUnit Test Patterns: Refactoring Test Code. A mi entender muy recomendable.

Hernan Wilkinson dijo...

Que tal Hernán,
una aclaración, no digo que los mocks no sean necesarios, sino que simplemente a veces se los sobre utiliza y se hacen cosas con ellos que en vez de ayudar, perjudican. El ejemplo que puse me parece un buen caso de esto.
Tampoco estoy tan de acuerdo con testear la clases totalmente aisladas del mundo... ¿cuál es el objetivo? si en definitiva sus instancias estarán interactuado en uno...
TDD tiene que ayudar a desarrollar software, a entender el dominio de problema, no debe hacer del desarrollo un proceso formal ni completo...

Sergio Carabetta dijo...

Hernán (ambos),

La idea de que un test unitario debe testear al componente aislado del mundo no es caprichosa. En primer lugar, garantiza que los tests podrán correr sin depender de nada más (por ejemplo, como parte de un proceso de integración continua). En segundo lugar, te permite verificar tu componente con nada más que la interfaz de los componentes de los cuales depende, muchos de los cuales quizá todavía no están listos. Y finalmente, evita que se pierda la confianza en todo el mecanismo de testing cuando empiezan a fallar los tests por culpa de otros componentes -no del que se estaba probando- y empieza a crearse como un ambiente enrarecido.

Claro que a todos nos gusta probar nuestro componente y ver como graba registros en la base de datos y envía mails y deja mensajes en una cola y.... pero eso es un test de integración, no un test unitario.

Saludos