Fecha Versin Cambios 20/04/2013 Prototipo 01 Primer prototipo pblico. 28/04/2013 Prototipo 02 Incluimos captulo 03. Incluimos ndice y enlace de la encuesta Corregimos algunos errores en el captulo 1 gracias a Guillermo Gutirrez (va ScribD). 19/05/2013 Prototipo 03 Incluimos captulo 6 (incompleto) y un cuestionario de autoevaluacin sobre conocimientos de testing. Arreglamos el ndice para que muestre correctamente los ttulos de los captulos. 01/06/2013 Prototipo 04 Mejoramos los textos de los captulos 1 y 2. Aadimos un apartado de agradecimientos del libro (adems de los agradecimientos de cada captulo). Aadimos una descripcin del contenido de los captulos 4 y 5. Comentarios son bienvenidos. 03/07/2013 Prototipo 05 Aadimos el captulo 4 completo con dos soluciones distintas para la kata Mancala. 15/11/2013 - Prototipo 06 Revisamos el captulo 1. Revisamos el captulo 2 e incluimos un ligero sombreado en los cuadros de cdigo que contiene cdigo de prueba. Revisamos el captulo 3. Aadimos el captulo 7 completo. Aadimos el apndice II completo Se modifica ligeramente el formato del ndice para resaltar los captulos.
5
Introduccin al proyecto
Alberto Savioa (@Pretotyping en Twitter) escribi un interesante y muy recomendable libro sobre prototipos y que, a su vez, tambin es un prototipo de libro (Pretotype It: http://www.pretotyping.org/pretotype-it---the-book). He decidido adoptar esa misma filosofa en el futuro libro que, sin embargo, ya ests leyendo justo en este momento. Tengo muy claro que no quiero escribir un libro solo para plasmar mis conocimientos de TDD o mi visin particular. Existen algunos libros muy buenos que explican casi todo lo que necesitas saber, y el resto te lo da la prctica. Mi objetivo es ensear TDD a travs de ejemplos que muestren muchos de los escenarios y dudas que aparecen cuando se aplican y que ofrezcan ideas y soluciones para abordarlos. Todo el material que se elabore para este libro ser de libre descarga lo ms rpidamente posible. Creo que vale la pena que el material llegue pronto a los interesados, aunque eso implique que incluya notas de cosas por hacer y arreglar o que el formato no est pulido. Mi objetivo es que quin tenga inters, pueda ver cmo va a ser el libro y pueda anticiparse y contarme qu cosas quiere que ponga o cambie. As, tendremos un libro de todos para todos. Voy a desarrollar el libro en dos fases: prototipos y versiones beta. Los prototipos sern versiones del libro incompletas. Incluso con captulos que tampoco estn completos. El contenido de un prototipo no es estable y todo puede cambiar o desaparecer en futuras versiones. Probablemente la versin que ests leyendo ahora mismo sea un prototipo. Una vez que encontremos la estructura del libro adecuada, los ejemplos ms interesantes y la mejor manera de exponerlos, pasaremos a las versiones beta. Una versin beta ya ser un libro completo y cerrado con todos (o casi todos) sus captulos. El objetivo de las versiones beta ser pulir el contenido y corregir erratas. Adems, tambin espero que esta filosofa de liberar el trabajo rpidamente me permita obtener un feedback que me anime y me motive a trabajar en el libro. No quiero despedirme sin agradecerte la ayuda que me ests prestando slo por leer estas lneas. Espero que lo que encuentres aqu te resulte til y entretenido.
Javier Jess Gutirrez Sevilla Primavera 2.013
6
Agradecimientos Hay muchas personas que me han ayudado de una u otra manera a que este libro vaya creciendo y, sobre todo, se vaya adaptando a los gustos y expectativas de vosotros, sus lectores. Como justo reconocimiento de toda esa ayuda he querido poner en cada captulo el nombre de las personas y proyectos que han estado involucrados. Pero tambin he tenido la enorme suerte de encontrar a personas que se merecen ser citadas en todos y cada uno de los captulos. Va para ellos tambin mi agradecimiento. A Joaqun Engelmo Moriche (Kinisoft) por sus muchos y muy buenos comentarios durante los primeros prototipos del libro, que eran justo cuando ms lo necesitaba. A Roberto Fernndez Leis por sus valiosos, detallados y extensos comentarios de todos los captulos del prototipo 5.
7
ndice
Captulo 1. Desarrollo Dirigido por Pruebas. Lo Mnimo que necesitas saber...... 10 Un ejemplo de Test-Driven Development .................................................................. 11 El diario de diseo ..................................................................................................... 12 El ciclo de Test-Driven Development ......................................................................... 13 Cdigo mnimo para pasar una prueba ...................................................................... 14 Empezar fallando ...................................................................................................... 15 La importancia de la refactorizacin de todo el cdigo .............................................. 15 Cmo continuar aplicando el ciclo TDD ..................................................................... 17 Heursticas TDD ......................................................................................................... 17 Formato del cdigo ................................................................................................... 18 Para terminar ............................................................................................................ 18 Enlaces y referencias ................................................................................................. 19 Agradecimientos del captulo .................................................................................... 19 Captulo 2. Primeros ejemplos de desarrollo dirigido por pruebas ...................... 20 Listas palndromas (en Java) ...................................................................................... 20 Tipo de dato lista (en Python).................................................................................... 24 Para reflexionar ........................................................................................................ 26 Solucin al ejercicio planteado .................................................................................. 27 Agradecimientos del captulo .................................................................................... 28 Captulo 3. Criba de Eratstenes en Java............................................................ 29 El problema ............................................................................................................... 29 Empezamos por un mal camino................................................................................. 29 Un nuevo comienzo .................................................................................................. 31 Integrando ................................................................................................................ 35 Conclusiones ............................................................................................................. 36 Para reflexionar......................................................................................................... 36 Captulo 4. Dos soluciones para el Mancala ....................................................... 38 El problema ............................................................................................................... 38 Cada semilla en su pozo ............................................................................................ 40 8
Sembrar para el futuro .............................................................................................. 43 Haced sitio ................................................................................................................ 44 Una pausa para refactorizar ...................................................................................... 45 Nada que plantar ...................................................................................................... 46 De vuelta a la choza .................................................................................................. 47 No uses las chozas..................................................................................................... 48 De la ltima a la primera ........................................................................................... 49 La ltima refactorizacin ........................................................................................... 50 Retrospectica de la primera solucin ......................................................................... 52 Un nuevo comienzo. ................................................................................................. 55 Nuestra segunda primera prueba. ............................................................................. 55 Un error comn. Triangular el fake ............................................................................ 56 Refactorizar y avanzar ............................................................................................... 57 Triangulando la siembra ............................................................................................ 58 Triangulando pruebas y cdigo.................................................................................. 60 Implantando el segundo jugador ............................................................................... 62 Del final al principio .................................................................................................. 64 Solo pozos vlidos ..................................................................................................... 65 Retrospectica de la segunda solucin ........................................................................ 66 Comparacin entre ambas soluciones. ...................................................................... 67 Para reflexionar ........................................................................................................ 67 Conclusiones ............................................................................................................. 68 Agradecimientos del captulo .................................................................................... 68 Captulo 5. Ejercicio TDD para presentar y usar mocks / doubles, etc. ................ 69 Captulo 6. Cifrado Esctala y Refactoriacin ..................................................... 70 El Problema ............................................................................................................... 70 El Cdigo ................................................................................................................... 70 Podemos leerlo? ..................................................................................................... 72 Escribiendo Pruebas .................................................................................................. 75 Desbloqueando a los mtodos principales. ................................................................ 77 Cambiando mensajes por excepciones ...................................................................... 79 Conclusiones ............................................................................................................. 82 Agradecimientos del captulo .................................................................................... 82 9
Captulo 7. Suscriptores y Dobles de Prueba ...................................................... 83 El problema ............................................................................................................... 83 La primera prueba de subscripcin ............................................................................ 85 Dos son multitud ....................................................................................................... 87 Refactorizando pruebas repetidas ............................................................................. 89 Completando las subscripciones................................................................................ 90 Refactorizando las pruebas ....................................................................................... 92 Un nuevo husped .................................................................................................... 93 Hora de borrar .......................................................................................................... 96 Solo cuando de verdad se borre ................................................................................ 97 Conclusiones ............................................................................................................. 98 Anexo 1. Test de pruebas ................................................................................... 99 Preguntas .................................................................................................................. 99 Respuestas .............................................................................................................. 103 Agradecimientos del captulo .................................................................................. 103 Anexo 2. Dobles de Prueba y Mockito ...............................................................104 Dobles de prueba .................................................................................................... 104 Tipos de dobles de prueba ...................................................................................... 106 Una breve introduccin a Mockito .......................................................................... 108 Ejemplos con Mockito ............................................................................................. 108 Errores comunes trabajando con dobles de pruebas ............................................... 112 Alternativas a los dobles de prueba ......................................................................... 112 Para terminar .......................................................................................................... 114 Bola Extra. FitNesse ................................................................................................. 114 Referencias ............................................................................................................. 114 Agradecimientos del anexo ..................................................................................... 115
10
Captulo 1. Desarrollo Dirigido por Pruebas. Lo Mnimo que necesitas saber
Como veremos ms adelante, uno de los pilares del desarrollo dirigido por pruebas (TDD por sus siglas en ingls) es escribir siempre el mnimo cdigo posible para que una prueba se ejecute con xito. En este libro vamos a aplicar la misma filosofa a la teora de TDD, por eso en este captulo encontrars los conceptos mnimos necesarios para empezar a aplicar TDD a partir del captulo siguiente. No vas a encontrar aqu reflexiones sobre la bondad o no de TDD, ni discusiones filosficas sobre si TDD est ms centrada en el diseo que en pruebas, o cualquier otro tema habitual de debate sobre TDD porque damos por hecho que, si lees esto, ests lo suficientemente convencido para darle una oportunidad a TDD. Solo lo mnimo para entrar en accin. Aun as, si este captulo se te hace aburrido, no lo leas. Ve directamente a los ejemplos y comienza a practicar TDD. Vuelve aqu de vez en cuando para repasar y terminar de afianzar los conceptos bsicos. Recuerda que para aprender bien TDD hay que practicarlo. Como complemento a estas pldoras de teora de TDD, encontrars al final de cada ejemplo prctico reflexiones sobre la manera de aplicar TDD que te ayudar a profundizar en esta buena prctica.
11
Para empezar a aprender y practicar TDD solo necesitas conocimientos bsicos sobre prueba del software. Hemos incluido un conjunto de ejercicios para que tambin puedas practicar y evaluar tus propios conocimientos. Encontrars estos ejercicios en los apndices de este libro. Cuanto ms aprendas y practiques sobre prueba del software mejor aplicars TDD.
Un ejemplo de Test-Driven Development Empecemos con un ejemplo. Supongamos que estamos escribiendo una clase que funciona como un carrito de la compra que an no ha sido escrita. Como queremos aplicar TDD debemos escribir una prueba antes de poder empezar a implementar la clase. Un posible caso de prueba podra ser el mostrado en el cdigo 1.1. Cdigo 1.1
Si analizamos detenidamente la prueba vemos que ya hemos tomado decisiones que tienen un impacto muy grande sobre el cdigo que tendremos que escribir (en este caso la clase carrito). Las primeras decisiones que tomamos al escribir una prueba estn en la lnea: cmo creamos un objeto carrito? Cul es el nombre de la clase carrito? Tendr un constructor sin parmetros? Despus, en la lnea 9, seguimos tomando decisiones, por ejemplo cmo aadimos elementos al carrito: usaremos una llamada a un mtodo? Cul ser el nombre de ese mtodo? Qu parmetros tendr y de qu tipo sern? Por ltimo, en la lnea 11, tomamos decisiones sobre cmo comprobamos que el carrito ha almacenado correctamente el producto, por ejemplo: qu mtodos incluimos? Cul es el resultado esperado? Hay que escribir antes una prueba para dicho mtodo? Con TDD, antes de escribir el cdigo nos centramos cmo queremos usarlo y nuestras primeras pruebas son requisitos o declaraciones de cmo debe ser dicho uso. 12
El diario de diseo El paso previo antes de empezar con un ciclo TDD (que veremos en la prxima seccin) es decidir sobre qu vamos a trabajar: acceso a datos? validaciones? disear nuevas clases? implementar lgica de negocio? Mejorar cosas que se nos han quedado pendientes? Para centrar nuestro trabajo utilizaremos el diario de diseo. Este diario es la lista de tareas que tenemos pendientes de hacer. Estas tareas sern, por lo general, funcionalidad o futuras pruebas a escribir. El principal objetivo del diario de diseo es mantenernos en todo momento con el foco centrado. No te ha pasado nunca que te has concentrado mucho en escribir una prueba y luego no recordabas muy bien qu queras implementar? El diario nos ayudar con esto. Utilizaremos el diario principalmente en dos momentos: el primero ser cuando ya tengamos una funcionalidad implementada (y gracias a TDD probada) para decidir la siguiente. El segundo momento ser cuando, durante un ciclo de TDD, se nos ocurra nueva funcionalidad o escenarios adicionales (por ejemplo: nulos, cadenas o colecciones vacas, nmeros negativos, etc.) a implementar en nuestro cdigo. Figura 1.1. Ejemplo de diario de diseo.
En la figura 1.1 tienes un ejemplo de diario de diseo utilizada por Kent Beck en su libro Test Driven Development: By Example para implementar una clase que represente una cantidad en un sistema monetario. Como puedes ver en la figura 1.1, el diario de diseo no es una lista ordenada ni priorizada. En el momento de elegir una nueva tarea para trabajar, tienes libertad de elegir aquella que consideres ms importante. Tampoco tienes que limitarte por posibles dependencias entre tareas, recuerda que las pruebas unitarias rompen las dependencias por lo que stas no condicionan la eleccin. 13
Veremos ejemplos de diarios de diseo en los ejemplos de los prximos captulos. El ciclo de Test-Driven Development El ciclo de TDD es el conjunto de pasos que seguimos para implementar un nuevo fragmento de cdigo de produccin. El ciclo de TDD utiliza las pruebas para definir qu resultado queremos obtener del cdigo que vamos a escribir. Estas pruebas nos ayudan a tomar las decisiones de diseo de cdigo ms adecuadas. El ciclo completo de TDD se muestra en la figura 1.1. A continuacin, veremos cada elemento con ms detalle. Figura 1.2. Ciclo TDD.
El primer paso para empezar con el ciclo TDD consiste en escribir una prueba que ponga de relieve la funcionalidad que queremos implementar. Esto nos obliga a definir qu es lo que queremos obtener con el cdigo que vamos a escribir. A continuacin ejecutamos la prueba y vemos como falla. Si la prueba no falla estudiamos qu est sucediendo y elegimos otra. Despus, escribimos el cdigo mnimo (ms corto) para que la prueba pase con xito. No nos preocupamos de escribirlo bonito, tenemos libertad para tomar atajos. A continuacin ejecutamos de nuevo todas nuestras pruebas si todo ha ido bien la prueba que fall antes ahora debera ejecutarse con xito y todas las pruebas que antes se ejecutaban con xito ahora lo seguirn haciendo. Si no debemos parar e investigar qu ha sucedido en el paso anterior. Por ltimo, quitamos los atajos, refactorizamos el cdigo y las pruebas y ejecutamos las pruebas para asegurar que todo sigue funcionando correctamente y que 14
no hemos introducido ningn error en la refactorizacin. Vers varios ejemplos de refactorizaciones en los ejemplos de los prximos captulos. Esto es todo TDD, a partir de aqu ya puedes empezar a aplicarlo. Sin embargo es interesante que conozcamos un poco ms del por qu hacemos las cosas de esta manera. Cdigo mnimo para pasar una prueba En el ciclo de Test-Driven Development, a la hora de implementar nuestro cdigo y hacer que la prueba pase, intentamos implementar slo el mnimo cdigo necesario y tan sencillo como sea posible. A esto lo llamamos avanzar en pequeos pasos, o babysteps. Puede ser frustrante tener la implementacin de un mtodo en la cabeza y tener que parar porque no tenemos todas las pruebas que necesitamos para poder escribirla. Sin embargo siempre podemos introducir un error en el cdigo sin darnos cuenta, por lo que es mejor avanzar ms despacio pero ms seguro. Tambin podemos tener una idea muy buena en la cabeza de cmo tiene que ser un mtodo o un API pero luego descubrir que no es la manera ms cmoda de utilizar. Por eso es mejor avanzar ms despacio pero comprobando que nuestro cdigo es usable. Aunque la primera vez nos parezca que ese cdigo no tenga valor, s que lo tiene. Esto lo iremos descubriendo a medida que ganemos experiencia aplicando TDD. Cuando algo falle nos ser de mucha ayuda acotar el problema en un pequeo fragmento de cdigo. Adems, el tener mtodos que no tengan demasiadas lneas de cdigo es una heurstica de buen diseo. Tambin debemos aplicar babysteps a las pruebas. Las pruebas tambin deben implementar el mnimo cdigo necesario y cuanto menos mejor. As no gastamos demasiado tiempo en escribirlas y podemos volver rpidamente a continuar trabajando en la funcionalidad y, algo tambin muy importante, podemos entender el objetivo de la prueba rpidamente ojeando el cdigo Un ltimo motivo para escribir pruebas cortas y el mnimo cdigo posible es no perder la concentracin sobre lo que estamos trabajando (o no perder el foco). Uno de los puntos clave de TDD es ejecutar las pruebas muy a menudo. Si nos detenemos demasiado en una implementacin o intentamos abarcar demasiado probablemente surjan muchos errores que nos lleven a entrar en un bucle de ejecutar las pruebas, hacer un pequeo cambio en el cdigo (una condicin de un if, el lmite de un for, etc.), volver a ejecutar las pruebas, volver a cambiar, etc. Esto es montono, aburrido y hace que nos olvidemos qu funcionalidad estamos implementando. Veremos muchos ejemplos de cdigo mnimo a lo largo de este libro. 15
Empezar fallando Otro aspecto que puede resultar desconcertante en TDD es el ejecutar la prueba en primer lugar y que falle. Vamos a detenernos en ver los motivos. Las pruebas tienen que ser simples ya que no escribimos pruebas que prueben las pruebas. Por ello las pruebas tienen que ser rpidas y pequeas y deben fallar. Una prueba que funciona nada ms escribirla puede indicar o bien que el cdigo no funciona como pensamos o bien que ya hace lo que debera. En ambos casos puede merecer la pena replantear el prximo paso. Una excepcin a esto son pruebas que son redundantes, es decir, que ya verifican algn escenario contemplado por otras pruebas. Aunque no parece lgico aadir pruebas redundantes, pueden ser tiles para documentar bien el cdigo, exponer escenarios ms complejos o, simplemente, aumentar nuestra sensacin de seguridad y control a la hora de hacer cambios. Siempre que mantengamos el cdigo de pruebas actualizado no habr ningn problema al contar con pruebas redundantes. TDD no dice nada sobre estas pruebas as que puedes aadirlas si lo deseas siempre que te comprometas a refactorizarlas y a mantenerlas como al resto del cdigo. Vamos a hablar de esto justo a continuacin. La importancia de la refactorizacin de todo el cdigo Refactorizar es cambiar la estructura interna del cdigo para mejorar su calidad, pero sin cambiar su funcionalidad e interfaces externas. Refactorizar es una tarea que algunos desarrolladores pueden olvidar presionados por el tiempo. Sin embargo es un punto vital para que funcione TDD ya que esta buena prctica trabaja de manera incremental. Veamos las razones. Un desarrollador no pasa la mayor parte de su tiempo escribiendo cdigo nuevo, sino que pasa la mayor parte del tiempo diseando cdigo, modificando cdigo y haciendo que fragmentos de cdigo funcionen bien juntos. Por tanto un desarrollador tiene que ser capaz de leer y entender el cdigo. Las refactorizaciones nos ayudan a pulir y mejorar el cdigo para que sea ms sencillo de entender y actualizar. Las pruebas nos dan la tranquilidad de que el cdigo va a seguir funcionando correctamente. Veamos un ejemplo, cul de las dos pruebas del cdigo 1.1 se entiende mejor? En la primera prueba exponemos detalles de bajo nivel que no todos tienen que conocer, por ejemplo la manera de crear productos. Si repetimos la creacin de productos en muchas pruebas y luego la creacin de un producto cambia (lo que pasa con ms frecuencia de lo que uno se imagina) tendremos que arreglar muchas pruebas. Adems, en esta prueba no nos interesa conocer los detalles del producto.
16
Cdigo 1.1 Prueba A
Prueba B
Otro ejemplo: Por qu el total del carrito debe ser exactamente 118.5? Qu significa ese nmero? Si vemos la prueba B comprobamos que el total del carrito debe ser tres veces el precio del producto porque hemos aadido 3 copias. Si no refactorizamos el cdigo de nuestra aplicacin, cada vez ser ms difcil de entender, ms difcil de modificar y ms difcil corregir los errores. Eso nos empujar a buscar alternativas o rodeos que empeorarn la situacin. Si tampoco refactorizamos el cdigo de pruebas tambin ser ms difcil de entender y modificar con el paso del tiempo. Cuando una prueba falle nos costar ms esfuerzo descubrir qu est fallando. Adems cuando el cdigo cambie nos costar ms esfuerzo cambiar las pruebas y, poco a poco, tenderemos a dejarlas de lado con lo que nuestro cdigo no tendr la red de proteccin de las pruebas y perderemos la confianza en l. 17
Cmo continuar aplicando el ciclo TDD En el ciclo de TDD, primero tenemos que escribir una prueba que falle, pero qu podemos hacer despus? Existen tres estrategias: fake, triangulacin e implementacin obvia. No hay ningn orden para aplicar estas estrategias, aunque es muy comn comenzar fake y luego evolucionar a triangulacin o Implementacin Obvia. La estrategia fake consiste en simular el resultado o comportamiento esperado de la forma ms rpida y sencilla posible. Para ello podemos incrustar el valor esperado en el cuerpo de la funcin. Utilizamos la estrategia fake cuando no tenemos muy claro cmo implementar un mtodo, o cuando la implementacin es demasiado larga o compleja para hacerla en un nico paso o cuando queremos priorizar la creacin de interfaces fciles de usar antes de entrar en los detalles de sus implementaciones. La estrategia triangulacin consiste en aadir nuevas pruebas a una misma funcionalidad buscado situaciones no implementadas an. Con esta tcnica podemos evolucionar los fakes hasta la implementacin en pequeos pasos (o babysteps). En algunas ocasiones, el cdigo a escribir para superar una prueba sigue un patrn muy claro y conocido, o bien es lo suficientemente pequeo para poder escribirlo en poco tiempo. En estos casos se puede implementar el cdigo final directamente. Esta es la Implementacin Obvia. Algunos ejemplos de implementaciones obvias son recorrer colecciones, mtodos get/set, etc. Heursticas TDD En un desarrollo real no tenemos que obsesionarnos por aplicar TDD al pie de la letra. Al contrario, lo ms beneficio es adaptar TDD a nuestra manera de trabajar, complementarlo con otras tcnicas para obtener los mejores resultados. Sin embargo, si estamos aprendiendo o practicando TDD s es til saber si lo estamos aplicando bien o no, para no coger ningn mal hbito. A continuacin veremos algunas ideas para poder autoevaluar si estamos aplicando TDD con xito. Empecemos repasando las mtricas de cdigo. Algunas mtricas pueden ayudarnos a detectar si estamos obteniendo buenos resultados o no. A continuacin se muestra un listado de mtricas fciles de medir. Tamao de los mtodos (pocas lneas). Pocos parmetros en los mtodos (no ms de 3). Clases e interfaces con pocos mtodos. Alta cohesin y bajo acoplamiento. Alta cobertura de pruebas (90% o ms). 18
Cantidad de cdigo duplicado. La cobertura es la cantidad de cdigo fuente que es ejecutado por las pruebas. El valor del 90% es una referencia y puede variar dependiendo del proyecto / tecnologa. Algunas herramientas para calcular mtricas y cobertura de cdigo que puedes utilizar en Java son PMD, CheckStyle, EclEmma o Sonar. PMD incluye, adems, una herramienta llamada CPD para detectar cdigo duplicado.
Hay que tener cuidado con las mtricas ya que si nos centramos demasiado en ellas podemos desplazar el foco de nuestro trabajo a conseguir buenos nmeros en vez de buscar la felicidad de nuestros clientes y usuarios. Por ejemplo podemos tener una alta cobertura de cdigo o mtodos pequeos y clases poco cohesionadas pero un cdigo mal diseado, difcil de entender y de modificar. Ejecutar las pruebas con mucha frecuencia y dejar transcurrir poco tiempo entre ejecucin y ejecucin del conjunto de pruebas son dos indicadores de que estamos aplicando TDD adecuadamente, aunque pueden ser ms difciles de medir que las mtricas anteriores. Formato del cdigo A partir de aqu encontrars mucho cdigo fuente. Para ayudarte a interpretar y entender el cdigo seguimos las siguientes convenciones. Todo el cdigo est en un cuadro con dos nmeros en formato X.Y, el nmero X indica el captulo y el nmero Y es nico en cada captulo. Algunas veces, adems, se aade un encabezado que resume el contenido del cdigo. En cualquier caso, siempre encontrars una explicacin del cdigo muy cerca de dnde aparezca. Adems, encontrars cuadros con cdigo de prueba y cuadros con cdigo de produccin (cdigo que implementa el problema). Puedes distinguirlos fcilmente porque el cdigo de pruebas tiene un fondo gris. Tambin encontrars cuadros, tablas y figuras que siguen la misma regla de numeracin. Para terminar Si es la primera vez que te acercas a TDD utiliza todo lo que has visto en este captulo para hacerte ms fcil aplicarlo. A medida que ganes experiencia probablemente descubras pequeas variantes, o alternativas, o mejoras para adaptar TDD a tu manera de trabajar. No dudes en aplicarlas y recuerda que TDD es una buena prctica y el 19
contenido de este captulo solo es una ayuda, nunca una norma de obligado cumplimiento. Si ya conocas TDD, compara tu experiencia con todo lo que hemos visto en este captulo. Seguro que encuentras algo en lo que puedas mejorar. Ests preparado para empezar a aplicar TDD? Si lo ests ve al prximo captulo y si no, ve tambin al prximo captulo!
En este libro utilizamos la palabra japonesa kata que significa literalmente forma y que se utiliza para nombrar a una serie de movimientos utilizada en la prctica de muchas disciplinas, como el teatro kabuki o las artes marciales En la comunidad global de TDD este trmino se emplea con mucha frecuencia para designar ejercicios pensados para mejorar nuestras habilidades programando.
Enlaces y referencias En este captulo hemos visto, de manera resumida, los conceptos bsicos del desarrollo dirigido por pruebas y buenas prcticas para aplicarlo con xito. A partir de este captulo, continuaremos aprendiendo ms sobre TDD mediante ejemplos prcticos. Mi consejo como autor de este libro es que comiences a aplicar estos conocimientos y a practicar TDD ahora mismo con los ejemplos que encontradas en los prximos captulos. Si quieres saber ms de TDD o si durante los ejemplos encuentras alguna laguna que quieras completar, puedes utilizar las siguientes referencias. Diseo gil con TDD http://www.dirigidoportests.com/el-libro Testing Unitario con Microsoft Fakes https://vsartesttoolingguide.codeplex.com/downloads/get/720407 Materiales del curso on-line TDD desde Cero http://www.iwt2.org/web/opencms/IWT2/formacion/catalogo/curso0006.html?locale=es Blog Aprendiendo TDD http://aprendiendotdd.wordpress.com/ Blog Hola TDD http://holatdd.com/ Agradecimientos del captulo A todos los alumnos de la primera edicin del curso Desarrollo Dirigido por Pruebas desde Cero porque sus ejercicios, dudas y reflexiones me han ayudado mucho a darle forma a este captulo. 20
Captulo 2. Primeros ejemplos de desarrollo dirigido por pruebas
Antes de empezar con ejemplos de mayor tamao vamos a ver ejemplos pequeos y sencillos de desarrollo de software dirigido por pruebas para empezar a aplicar todo lo visto en el captulo anterior. Estos ejemplos servirn para introducir los fundamentos de TDD que aplicaremos en los ejercicios de los prximos captulos. Listas palndromas (en Java) En nuestro primer ejercicio vamos a implementar un mtodo que verifique si una lista es palndroma, es decir, si el primer elemento coincide con el ltimo, el segundo con el penltimo, etc. El algoritmo que implementaremos recorrer las dos mitades de la lista a la vez, la primera mitad de orden creciente y la segunda mitad en orden decreciente, comparando los elementos de ambas mitades. Podemos empezar definiendo dos casos a tener en cuanta, cuando la lista tiene nmero de elementos pares, y cuando la lista tiene nmero de elementos impares, en cuyo caso el elemento central lo ignoramos. Todo lo anterior junto con ms ideas que se nos ocurran, como por ejemplo algunos especiales, lo anotamos en el diario de diseo. Este diario se muestra en el cuadro 2.1.
21
Cuadro 2.1. Diario de diseo inicial para las listas palndromas. Caso especial: lista vaca no es palndroma Caso especial. Lista con un nico elemento no es palndroma. Comprobar listas con nmero de elementos pares. Comprobar listas con nmeros impares
Para empezar elegimos la entrada de nuestro diario de diseo que se convertir en nuestra primera prueba. Vamos a comprobar listas de nmeros de elementos pres, as que escribimos nuestra primera prueba (cdigo 2.1). Esta prueba nos sirve para definir cmo va a ser la funcin: Qu parmetros necesita? Cmos e llamar? Etc. Cdigo 2.1 @Test public void testPalindromaDeEnteros_Par() { List<Integer> l = Arrays.asList(1,2,2,1); Boolean b = ListasPalindromas.isPalindroma(l);
assertTrue(b); }
Vamos a escribir ahora el mnimo cdigo que pasa la prueba (cdigo 2.2). En concreto aplicamos la estrategia fake vista en el captulo anterior. Cdigo 2.2 public static <T >boolean isPalindroma(List<T> lista){ return true; }
La prueba pasa con xito. Vamos a triangular y, para ello, aadimos una segunda prueba en la que el mtodo detecta que una lista no es palndroma. Una posible prueba se muestra en el cdigo 2.3. Cdigo 2.3 @Test public void testNoPalindromaDeEnteros_Par() { List<Integer> l = Arrays.asList(1,2,3,1); Boolean b = ListasPalindromas.isPalindroma(l);
assertFalse(b); }
A partir de las dos pruebas anteriores vamos a implementar el ncleo de la comparacin. El cdigo mnimo que hace que ambas pruebas se ejecuten con xito se muestra en el cdigo 2.4.
22
Cdigo 2.4. public static <T >boolean isPalindroma(List<T> lista){ for(int i = 0; i < lista.size() / 2; i++){ if (lista.get(i) != lista.get(lista.size()-1 - i)){ return false; } } return true; }
Antes de continuar, vamos a refactorizar para que el algoritmo sea ms sencillo de entender. Compara el cdigo del cuadro 2.4 con el cdigo del cuadro 2.5. Es ms sencillo de entender? Qu cambios haras para mejorar la legibilidad? En el primer captulo repasamos el por qu es muy importante que el cdigo sea fcil de leer cuando aplicamos TDD. Cdigo 2.5. Cdigo refactorizado. public static <T >boolean isPalindroma(List<T> lista){ int mitad = lista.size() / 2; int ultimoelemento = lista.size()-1;
for(int i = 0; i < mitad; i++){ if (lista.get(i) != lista.get(ultimoelemento - i)){ return false; } } return true; }
La solucin anterior funciona con listas de elementos pares. Nuestro siguiente paso ser aadir pruebas para que el cdigo trabaje tambin con listas con un nmero impar de elementos. Aadimos dos pruebas con listas impares (cdigo 2.6 y cdigo 2.7). Cdigo 2.6. Ejemplo de lista palndroma con tamao impar. @Test public void testPalindromaDeEnteros_Impar() { List<Integer> l = Arrays.asList(1,2,3,2,1); Boolean b = ListasPalindromas.isPalindroma(l);
assertTrue(b); }
Cdigo 2.7. Ejemplo de lista no palndroma con tamao impar.. @Test public void testNoPalindromaDeEnteros_ImPar() { List<Integer> l = Arrays.asList(1,2,3,3,1); Boolean b = ListasPalindromas.isPalindroma(l); 23
assertFalse(b); }
El conjunto de pruebas sigue funcionando correctamente, incluidas las pruebas de los cuadros 2.6 y 2.7. Busquemos ahora pruebas que nos hagan escribir nuevo cdigo de produccin. Por ejemplo podemos implementar la gestin de una lista vaca. Para ello escribimos la prueba del cdigo 2.8. Cdigo 2.8. @Test public void testListaVacia() { List<Integer> l = Arrays.asList(); Boolean b = ListasPalindromas.isPalindroma(l);
assertFalse(b); }
Y, ahora s, la nueva prueba falla y podemos escribir cdigo adicional. El resultado se muestra a continuacin. Cdigo 2.9. public static <T >boolean isPalindroma(List<T> lista) { if (lista.size() == 0 ) { return false; } int mitad = lista.size() / 2; int ultimoelemento = lista.size()-1;
for(int i = 0; i < mitad; i++) { if (lista.get(i) != lista.get(ultimoelemento - i)) { return false; } } return true; }
Sin embargo si la lista tiene un nico elemento tampoco ser posible comprobar si es o no palndroma, por lo que aadimos una nueva prueba. En este caso tomo la decisin de que una lista con un nico elemento no es palndroma.
24
Cdigo 2.10. @Test public void testListaUnElemento() { List<Integer> l = Arrays.asList(1); Boolean b = ListasPalindromas.isPalindroma(l);
assertFalse(b); }
A continuacin, modificamos el cdigo para que todas las pruebas que tenemos hasta ahora se ejecuten con xito. Cdigo 2.11. public static <T >boolean isPalindroma(List<T> lista) { if (lista.size() < 2 ) { return false; }
int mitad = lista.size() / 2; int ultimoelemento = lista.size()-1;
for(int i = 0; i < mitad; i++) { if (lista.get(i) != lista.get(ultimoelemento - i)) { return false; } } return true; }
Y ya est terminado. Despus de revisar el diario de diseo vemos que hemos acabado con todas las tareas por lo que podemos pasar al siguiente ejercicio. Tipo de dato lista (en Python) En esta seccin vamos a ver un ejemplo de cmo utilizar las pruebas como requisitos con una clase muy sencilla que incluya alguna de las operaciones del tipo de dato lista. No vamos a empezar pensando en qu mtodos tiene que tener la lista ni en los detalles de su implementacin sino en ejemplos de cmo queremos utilizarla y los documentamos en nuestro diario de diseo: Cuadro 2.2. Diario de diseo. El nmero de elementos de una lista vaca es cero. Al aadir un elemento a una lista, el nmero de elementos se incrementa en uno. En una lista vaca, a la que se aaden, los elementos A,B, y C, el elemento A tiene asociado el ndice 0, el elemento B tiene asociado el ndice 1 y el elemento C tiene asoacido el ndice 2
25
Podemos codificar estos ejemplos como pruebas en Python para tomar decisiones sobre cmo vamos a utilizar la clase lista y para ir verificando que todo funciona correctamente a medida que la programamos. Para ello utilizamos el mdulo unittest que ya viene incluido en la distribucin base de Python. Una posible implementacin de los ejemplos anteriores se muestra a continuacin.
Cdigo 2.12. Pruebas en Python class TestsCasesForMyList(unittest.TestCase): def test_GivenANewListThenItHasZeroElements(self): lista = MyList() self.assertEqual(0, lista.size())
def test_GivenANewListWhenIAddAnElementThenItHasOneElement(self): lista = MyList() lista.add("A") self.assertEqual(1, lista.size())
Fijmonos en las decisiones de diseo y requisitos que hemos establecido puede que sin darnos cuenta. Por ejemplo, si nos fijamos en la tercera prueba vemos que, aunque nadie lo haba mencionado, nos resulta muy cmodo indicar un grupo de elementos al momento de crear la lista, en vez de aadirlo uno a uno. Si no hubiramos puesto este ejemplo puede que no lo hubiramos descubierto. La implementacin de las pruebas anteriores, siguiendo la regla del mnimo cdigo se muestra a continuacin. Cdigo 2.13. Implementacin de la lista en Python class MyList: def __init__(self, *val): self.elements = 0
def size(self): return self.elements
def add(self, elem): self.elements += 1
def get(self, index): if index == 0: return "A" if index == 1: return "B" return "C"
26
Cul sera tu siguiente paso para continuar aplicando TDD a partir de aqu? Tienes la solucin al final de este captulo. Para reflexionar Vamos a comentar algunas decisiones que hemos tomado en los ejercicios anteriores. Empezamos repasando el mnimo cdigo de produccin necesario para que una prueba funcione. Aplicando este principio hemos escrito el cdigo 2.14 para implementar la primera prueba del ejercicio de listas palndroma. Cdigo 2.14 public static <T >boolean isPalindroma(List<T> lista){ return true; }
No es este cdigo demasiado trivial para aportar algo? No, este cdigo es valioso, porque, en primer lugar me permite pasar la prueba de fallo a xito y me da seguridad para seguir avanzando. En segundo lugar sirve como comprobante de que la prueba funciona. En tercer y ltimo lugar me permite usar mi propio cdigo y darme cuenta si el nombre del mtodo, parmetros, tipo devuelto, etc. son la mejor eleccin. Como vimos en el captulo anterior, a partir de este cdigo podemos tomar tres caminos: cambiar el fake por el cdigo autntico, escribir una nueva prueba que me obligue a cambiar el cdigo para que las pruebas pasen, o escribir una prueba que me lleve a implementar otro mtodo. La tercera opcin no tiene sentido aqu ya que no estamos construyendo un API ni similares, pero podra tener sentido si quisiramos modelar una interfaz externa cmoda de usar antes de entrar en la implementacin. La primera opcin, personalmente, no convence en este caso. Creo que el cdigo real de produccin es muy complejo para implementarlo en un nico pequeo paso (babystep). Por este motivo eleg la segunda opcin y aad una segunda prueba como vimos al principio del captulo. Esta es una opcin personal que dicta la prctica, cualquier otra opcin probablemente sera igual de interesante ya que conduce a aumentar la experiencia y, por tanto, la capacidad de reflexionar y aprender. Recuerda tambin que buscamos avanzar en la implementacin con pequeos pasos. Un error comn al principio es escribir una implementacin obvia como la del cdigo 2.14 y triangular para intentar implementar 30 o 40 lneas cdigo de golpe. Busca que la triangulacin te lleve a implementar solo unas pocas lneas de cdigo. Aunque pienses que es una implementacin obvia, intenta no escribir demasiado cdigo de produccin, recuerda que tambin tenemos que ejecutar las pruebas muy a menudo 27
Otro aspecto interesante para reflexionar es qu hacer con pruebas innecesarias. Hemos visto en el primer ejemplo que las pruebas de los cuadros 2.6 y 2.7 no nos hacan escribir nuevo cdigo, es decir, funcionan con el cdigo actual. Por qu escribirlas? Desde el punto de vista de TDD estas pruebas sobraran y no deberamos haberlas escrito. No estamos obligados aplicar TDD durante todo el desarrollo y podemos escribir pruebas por otros motivos adems de para escribir nuevo cdigo. Podemos utilizar las pruebas para documentar adecuadamente escenarios adicionales. Observa que, aunque el cdigo tal y como lo tenemos se comporta correctamente, puede no ser obvio cual es el resultado esperado para las listas impares. Las pruebas de los cuadros 2.6 y 2.7 hacen ms claro ese resultado. En general, salimos del ciclo de TDD cuando queremos aadir pruebas adicionales, cuando no seguimos escribiendo nuevo cdigo, por ejemplo para dedicar ms tiempo a refactorizar, o cuando consideramos que TDD no nos aporta ningn valor en el cdigo a escribir. Nunca, nunca, nunca (s, tres veces) debes abandonar TDD porque una prueba te resulte difcil. Justo al contrario, si una prueba te resulta difcil e escribir TDD est haciendo bien su trabajo avisndonos de que debemos volver a pensar en el diseo del cdigo. Solucin al ejercicio planteado Una posible solucin para continuar con el ejercicio de la lista en Python est en el cuadro 2.15. Completar. Cdigo 2.15. (copia del 2.13). class MyList: def __init__(self, *val): self.elements = 0
def size(self): return self.elements
def add(self, elem): self.elements += 1
def get(self, index): if index == 0: return "A" if index == 1: return "B" return "C"
El mtodo get es un fake, hemos incrustado los valores que tiene que devolver. Sin embargo es difcil hacer evolucionar esta implementacin ya que no hemos definido una prueba que nos lleve a incluir en MyList una manera de guardar los elementos.
28
Agradecimientos del captulo A la comunidad Solveet (www.solveet.com) y al excelente trabajo que hace Rubn Bernrdez con su mantenimiento. A Juan Luis Cano y al blog de Pybonacci (pybonacci.wordpress.com) por su apoyo a la hora de escribir los ejemplos en Python.
29
Captulo 3. Criba de Eratstenes en Java
En este captulo realizaremos un ejemplo de mayor tamao que implementaremos tambin aplicando el ciclo de desarrollo dirigido por pruebas que vimos en el primer captulo.
El problema Implementar, aplicando TDD, una versin muy sencilla (y poco optimizada) del algoritmo de la Criba de Eratstenes para calcular la lista de los nmeros primos desde 2 hasta un nmero n indicado. El algoritmo de la criba de Eratstenes se describe a continuacin. 1. Se crea una lista con los nmeros desde 2 hasta n. 2. Se elige el siguiente nmero x. 3. Se marcan todos los mltiplos de dicho nmero (x*2, x*3, etc.). 4. Se repite desde el paso 2 mientras queden nmeros. Cuando se ha terminado con todos los nmeros aquellos que queden sin marcar son primos. Veamos las primeras evoluciones aplicando TDD. Empezamos por un mal camino Para empezar a aplicar TDD escribimos nuestra primera prueba. Esta prueba la puedes ver en el cdigo 3.1. Una vez que probamos que falla (aunque Java ni siquiera permite ejecutarla) la implementamos con el cdigo mnimo posible, con un fake, que tambin se muestra en el cdigo 3.1. 30
Cdigo 3.1 Prueba public class TestCribaEratostenes { @Test public void testCalculaConValorInicialUno() { List<Integer> l = CribaDeEratstenes.Calcula(1); assertTrue(l.isEmpty()); } }
Implementacin static class CribaDeEratstenes { public static List<Integer> Calcula(int i) { return new ArrayList<Integer>(); } }
En este caso hemos empezado definiendo qu sucede cuando intentamos calcular los nmeros primos hasta el 1. Buscamos ahora una nueva prueba que nos haga evolucionar mediante triangulacin, por ejemplo buscando los primos del nmero 2, cuyo resultado esperado debe ser el propio nmero 2. Aplicamos un nuevo ciclo TDD con el par prueba-cdigo de produccin del cdigo 3.2.
Cdigo 3.2 Prueba @Test public void testCalculaConValorInicialDos() { List<Integer> l = CribaDeEratstenes.Calcula(2); assertEquals(1, l.size()); assertEquals(new Integer(2), l.get(0)); }
Implementacin public static List<Integer> Calcula(int i) { List<Integer> l = new ArrayList<Integer>();
if (i >= 2) l.add(2);
return l; }
La prueba del cdigo 3.2 verifica que el resultado es un nico nmero y que dicho nmero es el 2, que es el resultado esperado al calcular los primos de 2. Esta prueba falla, ya que el cdigo de produccin siempre devuelve una lista vaca. A continuacin escribimos el mnimo cdigo para superar esta y todas las dems pruebas que ya tenemos escritas (cdigo 3.2). Al final de la traza veremos una manera ms cmoda de escribir este tipo de asserts utilizando las libreras del propio Java. Vamos a continuar triangulando para aadir ms cdigo al mtodo. La siguiente prueba verifica los resultados de calcular los primos de 3, para lo cual la lista resultante debe tener un nuevo elemento (cdigo 3.3).
31
Cdigo 3.3 Prueba @Test public void testCalculaConValorInicialTres() { List<Integer> l = CribaDeEratstenes.Calcula(3);
Implementacin public static List<Integer> Calcula(int i) { List<Integer> l = new ArrayList<Integer>();
if (i >= 2) { l.add(2); if ( i >= 3) { l.add(3); }
return l; }
En este nuevo paso puedes ver que hemos entrado en un ciclo que no nos aporta nada. Si ahora hiciramos una prueba con el valor 5, el mnimo cdigo sera aadir el valor 5 a la lista devuelta. Pero en este caso no estamos calculando nada. Este ciclo se puede romper haciendo una refactorizacin. Con ella, en vez aadir uno a uno los valores esperados, los calculamos. Para hacer esta refactorizacin Tendramos que dar un paso muy grande con muchos cambios que pueden salir mal para implementar el cdigo del algoritmo. No estaramos sacndole partido a TDD. Llegados a este punto ya nos damos cuenta de que los casos de prueba no nos ayudan a evolucionar el cdigo. Una refactorizacin nos llevara a implementar todo el mtodo de una vez, sin aplicar TDD ni avanzar en pequeos pasos (babysteps). Hemos encontrado un mal camino de aplicar TDD, es decir, estamos trabajando de una manera que una manera que nos no lleva a ningn buen resultado y que nos empuja a probar otras maneras distintas de trabar. Vamos a cambiar la manera de aplicar TDD. No tires el cdigo que hemos escrito hasta ahora. Podremos aprovecharlo ms adelante. Un nuevo comienzo El primer paso del nuevo comienzo va a ser redactar el diario de diseo (cuadro 3.1). En este ejemplo el diario es los pasos del algoritmo. 3.1. Diario de diseo Construir una lista con los nmeros desde 2 hasta n en el que ningn elemento est marcado. Marcar los mltiplos de cada nmero x de la lista (x*2, x*3, etc.). Construir una lista con todos los nmeros no marcados (la lista de nmeros primos).
32
Para avanzar pasos ms diminutos, cada paso del algoritmo lo implementaremos en un mtodo con sus propias pruebas. Aunque dichos mtodos deberan ser privados, para probarlos con comodidad los pondremos con el mbito de visibilidad menos restrictivo. El primer paso que vamos a abordar es crear una matriz de booleanos para indicar qu nmeros estn marcados y cules no. Escribimos una prueba y, despus haremos una implementacin obvia (cdigo 3.4).
Cdigo 3.4 Prueba @Test public void testCreaListaDeNumerosSinMarcar() { int tope = 4; List<Boolean> booleanos = CribaDeEratstenes.CreaListaDeNumerosSinMarcar(tope);
assertEquals((tope+1), booleanos.size()); for (Boolean b: booleanos) { assertFalse(b); } }
Implementacin
public static List<Boolean> CreaListaDeNumerosSinMarcar(int i) { List<Boolean> lb = new ArrayList<Boolean>(); for (int c=0; c<=i; c++) lb.add(false); return lb; }
En la implementacin obvia del cdigo 3.4, se ha incrementado el tope en 1 ya que para que el nmero 4 aparezca en la lista de marcados, es necesario que la lista tenga 5 elementos (del 0 al 4). Ignoraremos las posiciones 0 y 1 que siempre sern false, ya que no intervienen en el algoritmo. No vamos a tener en cuenta la posibilidad de recibir como parmetro un valor inferior a dos, ya que los valores incorrectos sern filtrados por el mtodo que llamar al mtodo CreaListaDeNumerosSinMarcar. Aun as, no es una mala idea aadirlo, lo cul te propongo que ejercicio adicional. El primer paso de nuestro diario ya est implementado (cuadro 3.2) y lo tachamos. 3.2. Diario de diseo actualizado. Construir una lista con los nmeros desde 2 hasta n en el que ningn elemento est marcado. Marcar los mltiplos de cada nmero x de la lista (x*2, x*3, etc.). Construir una lista con todos los nmeros no marcados (la lista de nmeros primos). 33
Continuamos con la funcionalidad para marcar todos los mltiplos de un nmero dado. Antes de empezar la implementacin vamos a reflexionar qu significa esta funcionalidad diseando algunos ejemplos como los que se muestran a continuacin o En el clculo de los nmeros primos hasta el nmero 2, no queda marcado ningn nmero. o En el clculo de los nmeros primos hasta el nmero 3, no queda marcado ningn nmero. o En el clculo de los nmeros primos hasta el nmero 4, queda marcado el nmero 4. o En el clculo de los nmeros primos hasta el nmero 5, queda marcado el nmero 4. Con los ejemplos anteriores ya tenemos ms claro la funcionalidad que vamos a implementar. Adems vamos a utilizar los ejemplos que consideremos interesantes como casos de prueba. Vamos a empezar escribiendo una prueba que marque los valores para calcular los primos de 4, ya que es el primer nmero en el que hay un cambio en la lista de nmeros marcados (cdigo 3.5). Si usamos el 2 o el 3, al no marcarse ningn nmero, no habra sido necesario escribir cdigo, por lo que no son valores de prueba adecuados para hacer un ciclo de TDD. Cdigo 3.5
@Test public void testMarcarMultiplosHasta4() { List<Boolean> booleanos = CribaDeEratstenes.CreaListaDeNumerosSinMarcar(4);
La implementacin obvia que pasa la prueba anterior se muestra en el cdigo 3.6.
Cdigo 3.6
// Cdigo public static void MarcarMultiplos(List<Boolean> l) { for (int num = 2; num < l.size(); num++) { for (int mul = (num*2); mul < l.size(); mul += num) { l.set(mul, true); } } }
34
La prueba funciona. Ya tenemos implementada otra caracterstica del diario de diseo y actualizamos el cuadro 3.3.
3.3. Diario de diseo actualizado. Construir una lista con los nmeros desde 2 hasta n en el que ningn elemento est marcado. Marcar los mltiplos de cada nmero x de la lista (x*2, x*3, etc.). Construir una lista con todos los nmeros no marcados (la lista de nmeros primos).
La siguiente caracterstica a implementar ser la creacin de la lista de nmeros primos, es decir, aquellos que no han sido marcados. Una prueba de esta funcionalidad est en el cdigo 3.7.
Cdigo 3.7
@Test public void testCrearListaDePrimosHasta4() { List<Boolean> l = CribaDeEratstenes.CreaListaDeNumerosSinMarcar(4); CribaDeEratstenes.MarcarMultiplos(l);
La implementacin obvia que pasa la prueba anterior con xito est en el cdigo 3.8.
Cdigo 3.8
public static List<Integer> CreaListaDePrimos(List<Boolean> lb) { List<Integer> l = new ArrayList<Integer>(); for (int c = 2; c < lb.size();c++) { if (!lb.get(c)) { l.add(c); } } return l; }
Con este cdigo terminamos los pasos del algoritmo. Veamos cmo ponerlo todo junto en la siguiente seccin. 35
Integrando Los pasos del algoritmo ya estn implementados y probados. Ahora es el momento de dichos pasos en un mtodo que calcule la criba de Eratstenes. 3.1. Diario de diseo Construir una lista con los nmeros desde 2 hasta n en el que ningn elemento est marcado. Marcar los mltiplos de cada nmero x de la lista (x*2, x*3, etc.). Construir una lista con todos los nmeros no marcados (la lista de nmeros primos). Crear el mtodo principal que llame a los tres mtodos auxiliares.
Vamos a reutilizar las primeras pruebas que escribimos al principio de este captulo (cdigo 3.1, 3.2 y 3.3), por lo que ya contamos con un conjunto de pruebas para implementar el mtodo Calcula. La implementacin de este mtodo (implementacin obvia) est en el cdigo 3.9. Cdigo 3.9,
Las pruebas siguen funcionando por lo que ya podemos dar por terminada la implementacin. Sin embargo podemos aadir algunas pruebas ms por ejemplo la prueba 3.10. Cdigo 3.10.
@Test public void testGeneraPrimosHastaDoce() { List<Integer> l = CribaDeEratstenes.Calcula(12); Assert.assertEquals(l, Arrays.asList(2, 3, 5, 7, 11)); }
La prueba anterior calcula todos los nmeros primos hasta el 12 (inclusive) y, adems, utiliza Arrays.asList para crear la lista de nmeros esperada lo que facilita la comprobacin final. Tachamos el ltimo elemento de nuestro diario de diseo, lo que significa que ya hemos terminado. 36
Conclusiones Una limitacin que se ha visto en este ejercicio es cmo esconder los mtodos auxiliares para que solo puedan llamarse desde el mtodo principal, pero, a la vez, permitir que se prueben de manera independiente. Todo lo que queramos probar eran mtodos privados, salvo un mtodo director encargado de llamar a los dems (mtodo Calcula) que es pblico. Es posible probar mtodos privados en Java, por ejemplo invocarlo mediante introspeccin o crear una clase hija que defina mtodos pblicos que invoquen a estos elementos privados. Estas dos alternativas requieren trabajo adicional y hacen a las pruebas ms frgiles. Como no existe la solucin perfecta en este caso, como decisin personal, he optado por abrir la visibilidad de los mtodos y he documentado que dichos mtodos son de uso interno. Con ello he hecho que las pruebas sean lo ms sencilla posibles. En ningn momento de este ejemplo hemos refactorizado el cdigo, porque no hemos visto que fuera necesario, pero esto es muy poco frecuente. En este ejemplo, hemos implementado el diario de diseo siguiendo los mismos pasos que emplea la Criba de Eratstenes, pero recuerda que no hay limitacin a la hora de elegir la siguiente caracterstica a implementar. Para reflexionar. Hemos empezado el ejercicio tomando un camino que, con el tiempo, hemos descubierto que no era el correcto. Los malos olores que hemos encontrado han sido realizar varios fakes seguidos y no refactorizar. Es difcil aplicar TDD para implementar mtodos largos porque, como hemos visto al principio, es difcil ir aadiendo fragmentos de cdigo a un mtodo a medida que aadimos ms pruebas. En cambio, TDD funciona muy bien descomponiendo el problema en pasos pequeos y encapsulndolos en mtodos. El diario de diseo tambin nos ayuda a ello cmo has visto a lo largo de este captulo. Aunque hayamos empezando por un camino incorrecto no ha sido una prdida de tiempo. Hemos descubierto una manera de trabajar que no nos funcionaba y que nos ha ayudado a buscar otro camino distinto y hemos aprovechado las primeras pruebas. Es muy comn aplicando TDD descubrir que nuestros primeros pasos no van por buen camino. TDD no nos indica el camino correcto, pero nos ayuda a detectar rpidamente los caminos incorrectos. En el ejemplo anterior se dan algunas indicaciones de ejercicios adicionales para realizar. A continuacin te proponemos uno ms. Es posible optimizar el algoritmo parando cuando ya no queden ms nmeros por marcar. Intenta aadir esta mejora al 37
cdigo que se ha visto en este captulo. Tendra sentido aplicar TDD para implementar esta mejora?. Tambin es posible desarrollar nuevas implementaciones utilizando estructuras ms eficientes, como Bitset, o las nuevas funciones lambda y los streams de Java 8.
38
Captulo 4. Dos soluciones para el Mancala
En este captulo vamos a resolver la kata del juego Mancala. Uno de los primeros problemas que tendremos que afrontar, antes de implementar la lgica de negocio, es cmo modelar la informacin y qu interfaz externa es la ms cmoda para interactuar con el Mancala. Este captulo tiene una organizacin distinta a los captulos anteriores. Despus de exponer el problema que vamos a abordar, veremos dos soluciones. En la primera aplicaremos TDD pero de una manera descuidada. En la segunda aplicaremos TDD mucho mejor y as, t, lector, podrs comparar ambas estrategias y sacar tus propias conclusiones. El problema El Mancala (o Awale o Awir, etc.) es la familia de juegos ms antigua que se conoce. Sus doscientas variantes se extendieron por frica y parte de Asia. En esta kata vamos a implementar el movimiento de una de estas variantes. El tablero de juego consta de 6 pozos para cada jugador y dos chozas (una para cada jugador). Cada pozo tendr cuatro semillas y las chozas estarn vacas.
39
Imagen 1. Tablero de un Mancala clsico
En cada turno, el jugador selecciona uno de sus 6 pozos (nunca la choza), saca todas las semillas de l y las planta una a una en los pozos consecutivos siguiendo el sentido contrario de las agujas del reloj. No se puede seleccionar un pozo si no hay semillas en l. En la siguiente imagen acaba de empezar la partida y el jugador A ha seleccionado su pozo 3, por lo que ha puesto una semilla en los pozos 4, 5, 6 y en su choza. Imagen 2. Distribucin de los pozos
Con todo lo anterior la kata ya tiene una dificultad elevada, pero si an quieres implementar ms funcionalidad, aade las siguientes caractersticas. Si se da una vuelta completa, no se planta ninguna semilla en el pozo original. Si la ltima semilla la plantas en un pozo vaco de tu propiedad, coge todas las semillas del pozo de enfrente y gurdalas en tu choza. Cuando le toque mover a un jugador y no tenga semillas en sus pozos la partida termina. El jugador contrario guarda todas sus semillas en su choza y el ganador es el jugador con mayor nmero de semillas.
40
A continuacin, resolveremos este problema aplicando TDD pero no lo haremos bien. No tomes las siguientes secciones como ejemplo a seguir porque son justo lo contrario, cosas que deberas evitar cuando apliques TDD. Cuando leas esta primera parte intenta buscar fallos en la manera de aplicar TDD. Al final de esta primera encontrars una retrospectiva dnde analizaremos estos fallos.
Cada semilla en su pozo Ya tenemos claro el enunciado, as que no perdamos el tiempo y vamos con nuestra primera prueba. De golpe se nos ocurre representar los pozos y chozas con una clase que gestionen las semillas que contienen, as el cdigo ser ms sencillo de escribir. Para poder crear esta clase necesitamos una prueba, as que vamos por ello (cdigo 4.1). Cdigo 4.1. public class TestCell { int seeds = 4;
@Test public void testIndicateTheNumberOfSeedsWhenCreatingACell() { Cell c = new Cell(seeds);
assertEquals(seeds, c.getSeeds()); } }
Y a continuacin, en el cdigo 4.2, tenemos nuestra esperada, y esperemos que til, clase Cell. Cdigo 4.2. public class Cell {
int seeds;
public Cell(int seeds) { this.seeds = seeds; }
public int getSeeds() { return seeds; }}
El cdigo es lo suficientemente sencillo y rpido para considerarlo un babystep y escribirlo de una nica vez. De hecho la mayora del cdigo del cuadro 4.2 puede generarlo Eclipse (u otros IDEs) automticamente. Ahora que ya tenemos las celdas para guardar semillas vamos a utilizarlas para crear el tablero. Como el tablero tiene una configuracin inicial definida (4 semillas en cada pozo y las chozas vacas) vamos a escribir una prueba que recoja todo lo anterior y que nos empuje a crearlo (cdigo 4.3). 41
Cdigo 4.3. public class TestBoard {
@Test public void testNewBoardHas12CellsWith4SeedsEach() { Board b = new Board(); int cellCount = 0;
for (Cell c: b.getCells()) { cellCount++; assertEquals(c.getSeeds(), 4); }
assertEquals(12, cellCount); } }
Con el cdigo 4.3 comprobamos que una instancia de Board tenga 12 pozos (objetos de la clase Cell) cada una con 4 semillas. Ahora implementamos la clase Board para que la prueba anterior pase con xito, pero no nos vamos a parar a ver ese cdigo, sino que vamos a ir a la siguiente prueba. En la siguiente prueba, cdigo 4.4, comprobamos que el tablero tambin tiene dos chozas con cero semillas. Cdigo 4.4 @Test public void testNewBoardHas2HousesWithoutSeeds() { Board b = new Board();
for (Cell c: b.getHouses()) { assertEquals(c.getSeeds(), 0); }
assertEquals(2, b.getHouses().size()); }
Ahora s, veamos, en el cdigo 4.5, cul es el mnimo cdigo de la clase Board que permite que las dos pruebas anteriores pasen. Cdigo 4.5 public class Board {
List<Cell> cells; List<Cell> houses;
public Board() { cells = new ArrayList<Cell>(); for (int i = 0; i < 12; i++) { cells.add(new Cell(4)); } houses = new ArrayList<Cell>(); for (int i = 0; i < 2; i++) { houses.add(new Cell(0)); } }
public List<Cell> getCells() { return cells; 42
}
public List<Cell> getHouses() { return houses; }
}
Las pruebas de los cdigos 4.3 y 4.4 son muy similares. Adems, en el constructor de la clase Board estamos repitiendo el mismo fragmento de cdigo 2 veces. Todo lo anterior son malos olores que nos animan a refactorizar. El cdigo refactorizado de las pruebas se muestra en el cdigo 4.6. Hemos creado un mtodo assert local assertAllCellsHasSameSeeds para evitar el cdigo duplicado. Cdigo 4.6 public class TestBoard {
Board b;
@Before public void setUp() { b = new Board(); }
@Test public void testNewBoardHas12CellsWith4SeedsEach() { assertAllCellsHasSameSeeds(b.getCells(), 4); assertEquals(12, b.getCells().size()); }
@Test public void testNewBoardHas2HousesWithoutSeeds() { assertAllCellsHasSameSeeds(b.getHouses(), 0); assertEquals(2, b.getHouses().size()); }
void assertAllCellsHasSameSeeds(List<Cell> l, int seeds) { for (Cell c: l) { assertEquals(c.getSeeds(), seeds); } } }
Y el cdigo refactorizado de la clase Board se muestra en el cdigo 4.7. En esta refactorizacin hemos movido el cdigo duplicado al mtodo privado createListOfCells. Cdigo 4.7 public class Board {
List<Cell> cells; List<Cell> houses;
private List<Cell> createListOfCells(int cellsNumber, int value) { List<Cell> cells = new ArrayList<Cell>(); for (int i = 0; i < cellsNumber; i++) { cells.add(new Cell(value)); } return cells; } 43
public List<Cell> getHouses() { return houses; } }
Ahora que ya tenemos nuestro tablero, chozas y pozos, vamos a dar el siguiente paso y vamos a implementar la operacin de sembrar. Sembrar para el futuro Cuando sembramos, desencadenamos varios efectos. Uno de ellos es que el pozo original queda sin semillas. Vamos a escribir una prueba que ponga esto de manifiesto. Cdigo 4.8 @Test public void testSeeding4SeedsFromCell0_CellHas0Seeds() { b.seed(0);
En este caso, estamos identificando los pozos por su ndice. Uno de los jugadores tendr el pozo de 0 a 5 y el otro de 6 a 11. Para implementar esta prueba tenemos que aadir el mtodo seed a la clase Board (cdigo 4.9). Cdigo 4.9. Clase Board
public void seed(int i) { this.cells.get(i).removeSeeds(); }
Para que la prueba funcione, tenemos que implementar el mtodo removeSeeds en Cell para poder quitar las semillas de ese pozo. Su implementacin se muestra en el cdigo 4.10 Cdigo 4.10. Clase Cell
public void removeSeeds() { seeds = 0; }
44
La prueba original (cdigo 4.8) nos ha empujado a implementar tambin un mtodo en la clase Cell aunque no hemos escrito ninguna prueba para este mtodo. La prueba del mtodo removeSeed es la propia prueba del cdigo 4.8 ya que si el mtodo removeSeed de Cell no estuviera bien implementado, esta prueba fallara. Nuestro siguiente paso ser aadir las semillas que hemos quitado al resto de los pozos. Haced sitio Otro cambio en el tablero, cuando sembramos es que los pozos (y chozas) adyacentes ganan una semilla. Vamos a escribir una prueba que siembre las semillas del pozo 0 y verifique que el nmero de semillas de los pozos 1, 2, 3 y 4 se ha incrementado en uno. Cdigo 4.11. Clase Cell @Test public void testSeeding4SeedsFromCell0_Cells_1_2_3_4_HasOneMoreSeeds() { b.seed(0);
Con la prueba del cdigo 4.11, triangulamos el mtodo seed. Tenemos que aadir cdigo adicional al mtodo seed para que esta prueba pase con xito. Vamos a ver cmo quedara (cdigo 4.12). Hay que volver a aadir un nuevo mtodo a la clase Cell. Este mtodo tambin se muestra en 4.12 Cdigo 4.12 Mtodo seed de la clase board public void seed(int cellIndex) { int seeds = this.cells.get(cellIndex).removeSeeds(); for (int i = cellIndex+1; i <= seeds; i++) { this.cells.get(i).addSeed(); }
}
Nuevo mtodo para la clase Cell public void addSeed() { seeds++; }
public int removeSeeds() { int tmp = this.seeds; seeds = 0; return tmp; }
45
Tambin hemos tenido que modificar el mtodo removeSeed para que devuelva las semillas que haba en el pozo antes de quitarlas. En este caso, no estamos dando babysteps, sino que hemos avanzamos ms de lo conveniente. Discutiremos este aspecto en la retrospectiva (ms adelante en este captulo). Ahora, vamos a continuar con nuestra implementacin. Una pausa para refactorizar Hemos hecho dos ciclos de TDD, que nos han permitido quitar las semillas del pozo y plantarlas en los pozos consecutivos, pero no hemos refactorizado. No vamos a avanzar ms sin mejorar la calidad de nuestro cdigo. Vamos a comenzar por quitar los cuatro assertEquals de la prueba del cdigo 4.11. Como las celdas se almacenan en una lista, podemos aprovechar el mtodo sublist de la interfaz java.util.List y el mtodo assertAllCellsHasSameSeeds que creamos al principio del captulo, para escribir una prueba ms clara y compacta en el cdigo 4.13. Cdigo 4.13. Prueba refactorizada
@Test public void testSeeding4SeedsFromCell0_Cells_1_2_3_4_HasOneMoreSeeds() { b.seed(0);
Tambin usamos variables locales (expectedSeeds y affectedCells) para indicar qu papel juega los parmetros de assertAllCellsHasSameSeeds. Vamos a continuar refactorizando Las pruebas que hemos escrito, dependen de que la clase Board tenga un mtodo que devuelva una lista de objetos Cell. Si esto cambiara, afectara a todas las pruebas que hemos escrito. Uno de los puntos fuertes que nos da TDD es que hace fcil cambiar el cdigo, sin embargo en este caso estamos poniendo impedimentos al cambio. Vamos a solucionar esto centralizando en el mtodo setUp de la prueba el acceso a la lista de celdas. Si en el futuro decidimos que la clase Board no devuelve una lista de objetos Cell, podremos incluir el cdigo para crear dicha lista el mtodo setUp. Cdigo 4.14. Un nico mtodo para obtener la lista de celdas. public class TestBoard {
Board b; List<Cell> cells;
@Before public void setUp() { b = new Board(); cells = b.getCells(); 46
}
@Test public void testNewBoardHas12CellsWith4SeedsEach() { assertAllCellsHasSameSeeds(cells, Board.Default_Seeds);
assertEquals(12, cells.size()); }
@Test public void testNewBoardHas2HousesWithoutSeeds() { assertAllCellsHasSameSeeds(b.getHouses(), 0);
assertEquals(2, b.getHouses().size()); }
@Test public void testSeeding4SeedsFromCell0_CellHas0Seeds() { b.seed(0);
assertEquals(0, cells.get(0).getSeeds()); }
@Test public void testSeeding4SeedsFromCell0_Cells_1_2_3_4_HasOneMoreSeeds() { b.seed(0);
public void assertAllCellsHasSameSeeds(List<Cell> l, int seeds) { for (Cell c: l) { assertEquals(c.getSeeds(), seeds); } } }
Terminamos aqu la refactorizacin y continuamos avanzando en la funcionalidad. Nada que plantar Qu sucede si en el pozo que hemos elegido no hay ninguna semilla? Parece lgico decidir que no debe pasar nada, es decir, que el siguiente pozo no cambie el nmero de semillas que contiene. El cdigo 4.15 expresa esto en una prueba. Cdigo 4.15. Prueba refactorizada @Test public void testSeedingEmptyCell_Cell_1_DoesnotChange() { cells.get(0).removeSeeds();
La prueba anterior funciona sin necesidad de escribir cdigo adicional, ya que el cdigo que necesitamos lo escribimos al triangular el mtodo seed en el cdigo 4.12. Dejamos esta prueba para completar el conjunto de pruebas de la clase Board y buscamos algo nuevo para implementar De vuelta a la choza Te has dado cuenta de que, hasta ahora, no hemos tenido en cuenta las chozas para nada? Es el momento de que entren en juego. En el Mancala, vamos plantando cada semilla en un pozo o choza siguiendo el sentido inverso de las agujas del reloj. Teniendo los pozos y chozas en dos listas separadas, cuando llegamos al final de los pozos de un jugador, hemos de ir a la lista de chozas para plantar una semilla en la choza de ese jugador y luego volver a la lista de pozos para confinar plantado las semillas restantes en los pozos del otro jugador. Dos listas complican el cdigo y no apreciamos ninguna ventaja a tener pozos y chozas por separado, as que vamos a almacenar pozos y chozas en una sola lista. Para ello, modificamos las pruebas que verifican el estado inicial para reflejar este cambio. La prueba original y la nueva prueba estn en el cdigo 4.14 Cdigo 4.14 Pruebas que borramos @Test public void testNewBoardHas12CellsWith4SeedsEach() { assertAllCellsHasSameSeeds(b.getCells(), 4);
assertEquals(12, b.getCells().size()); }
@Test public void testNewBoardHas2HousesWithoutSeeds() { assertAllCellsHasSameSeeds(b.getHouses(), 0);
assertEquals(2, b.getHouses().size()); }
Prueba que aadimos @Test public void testNewBoardHas12CellsWith4SeedsEach() { assertAllCellsHasSameSeeds(cells.subList(0, 6), Board.Default_Seeds); assertAllCellsHasSameSeeds(cells.subList(7, 13), Board.Default_Seeds);
assertEquals(14, cells.size()); }
@Test public void testNewBoardHas2HousesWithoutSeeds() { assertEquals(0, cells.get(6).getSeeds()); assertEquals(0, cells.get(13).getSeeds()); }
48
Cdigo 4.15. Pozos y celdas en la misma lista public class Board {
List<Cell> cells; public static final int Default_Seeds = 4;
List<Cell> createListOfCells(int cellsNumber, int value) { List<Cell> cells = new ArrayList<Cell>(); for (int i = 0; i < cellsNumber; i++) { cells.add(new Cell(value)); } return cells; }
Con este cambio, la primera fila de pozos tendrn los ndices de 0 a 5, el ndice 6 ser una choza, la segunda fila de pozos tendrn los ndices de 7 a 12 y la segunda choza ser el ndice 13. Ya podemos cambiar el cdigo para que pozos y chozas se guarden en la misma lista. No uses las chozas Vamos a escribir ahora una prueba que verifique que trabajamos adecuadamente con las chozas. La primera prueba verificar que, si se selecciona una choza, no debe cambiar ninguna semilla de sitio (cdigo 4.16). Cdigo 4.16. @Test public void testSeedingHouseCell_BoardDoesNotChange() { int houseFirstPlayer = 6; b.seed(houseFirstPlayer);
La primera vez que hice y ejecut esta prueba funcion correctamente cuando no debera haberlo hecho. Tuve que desarrollar varias pruebas para darme cuenta de cul haba sido el error. Eres capaz de encontrar el error en el cdigo 4.12? Recuerda que la prueba del cdigo 4.11 funciona correctamente. 49
Vamos a explicar el error del cdigo 4.12 a continuacin. Como el parmetro cellIndex siempre vale 4, en la prueba solo plantbamos desde el pozo 1 hasta la 4. Si quisiramos sembrar las semillas del pozo 1 en vez de el pozo 0, el cdigo solo plantaba en los pozos 2, 3 y 4. Si intentramos sembrar las semillas del pozo 5 en adelante, el cdigo no plantara en ningn pozo ya que el bucle for no llegaba a dar ninguna vuelta. Encontrarme este fallo me hizo pensar de nuevo en algo que ya hemos mencionado. Hemos escrito demasiado cdigo cuando implementamos el bucle de seed? No podramos haber dado pasos ms pequeos? Retomaremos estas preguntas en la retrospectiva. Una vez arreglado el error e implementado el cdigo necesario para que la prueba del cdigo 4.16 pase con xito, nos queda el cdigo 4.17. Cdigo 4.17. public void seed(int cellIndex) { if (cellIndex == 6) return;
int seeds = this.cells.get(cellIndex).removeSeeds();
int initialCell = cellIndex+1; int finalCell = cellIndex + seeds;
for (int i = initialCell; i <= finalCell; i++) { this.cells.get(i).addSeed(); }
}
A continuacin podramos escribir una prueba similar para evitar que se utilice la otra choza. No vamos a mostrar dicho cdigo. Ya estamos a punto de acabar con toda la funcionalidad. Vamos a darle un ltimo empujn De la ltima a la primera La ltima funcionalidad pendiente es que, cuando plante en la ltima celda (el pozo del segundo jugador) y an queden semillas por sembrar, se contine plantando por la primera celda. En otras palabras, despus del objeto Cell del ndice 13 vendra el objeto Cell del ndice 0. El nmero de vueltas del bucle viene determinado por las semillas a plantar. Vamos a definir una prueba que muestre este escenario en el cdigo 4.18. Cdigo 4.18. Seleccionamos el ltimo pozo y la semillas se plantan en los primeros @Test public void testSeedingLasCell_SeedsArePlantedInFirstPits() { b.seed(12);
Solo tenemos que aadir una condicin al mtodo seed para que, si llega al final vuelva al principio. El resultado se muestra en el cdigo 4.19. Cdigo 4.19. public void seed(int cellIndex) { if (cellIndex == 6) return;
int seeds = this.cells.get(cellIndex).removeSeeds();
int initialCell = cellIndex+1; int finalCell = cellIndex + seeds;
int index; for (int i = initialCell; i <= finalCell; i++) { index = i % 14; this.cells.get(index).addSeed(); }
}
Ya hemos terminado con todo lo que tenamos hacer, ya no hay ms funcionalidad nueva que implementar. La ltima refactorizacin Las pruebas anteriores son muy dependientes de detalles de implementacin, como el rango de ndices necesarios para un grupo de pozos. Por este mismo motivo, nuestras pruebas tambin son difciles de entender. Si empezamos a trabajar en otro proyecto y volvemos a este cdigo pasada una semana, entenderemos qu significa cells.sublist(0, 6)? Por qu una sublista? Por qu exactamente ese rango? Podemos corregir esto buscando nuevas abstracciones. Una de ellas es el concepto de jugador. El primer jugador ser propietario de algunos de los pozos (del 0 al 6 para incluya su choza) y el segundo jugador ser propietario de los dems pozos (del 7 al 13). Vemos como queda esta refactorizacin en el 4.18. Cdigo 4.17 Prueba utilizando ndices @Test public void testNewBoardHas12CellsWith4SeedsEach() { List<Cell> cellsFirstPlayer = cells.subList(0, 6); assertAllCellsHasSameSeeds(cellsFirstPlayer, Board.Default_Seeds);
Misma prueba utilizando jugadores @Test public void testNewBoardHas12CellsWith4SeedsEach() { assertAllCellsHasSameSeeds(b.getCellsFirstPlayer(), Board.Default_Seeds); assertAllCellsHasSameSeeds(b.getCellsSecondPlayer(), Board.Default_Seeds);
assertEquals(14, cells.size()); }
Podemos hacer lo mismo para las chozas, como se muestra en el cdigo 4.18. Cdigo 4.18. Prueba utilizando ndices @Test public void testNewBoardHas2HousesWithoutSeeds() { assertEquals(0, cells.get(6).getSeeds()); assertEquals(0, cells.get(13).getSeeds()); } Mima prueba utilizando jugadores @Test public void testNewBoardHas2HousesWithoutSeeds() { assertEquals(0, b.getHouseFirstPlayer().getSeeds()); assertEquals(0, b.getHouseSecondPlayer().getSeeds()); }
Vamos a utilizar esta abstraccin para introducir ndices relativos a los jugadores. Es decir, ahora tendremos el pozo 0 del primero jugador y el pozo 0 del segundo jugador (que ser el ndice 7 de la lista de la clase Board). De esta manera las pruebas quedaran as (cdigo 4.19). Cdigo 4.19 Prueba utilizando ndices @Test public void testSeeding4SeedsFromCell0_Cells_1_2_3_4_HasOneMoreSeeds() { b.seed(0);
Podramos continuar refactorizando el cdigo. A continuacin se exponen algunas ideas de posibles refactorizaciones. Podramos aadir un mtodo que devuelva slo los pozos de un jugador y otro mtodo que devuelva slo las chozas, pero si quisiramos escribir una prueba que comenzara plantando en el pozo de un jugador y terminara plantando en el pozo del otro jugador, tendramos que hacer tres llamadas, una obtener los pozos del primer jugador y comprobar el incremento de semillas, otra para obtener la choza y otra para obtener los pozos del segundo jugador. Otra alternativa podra ser ocultar todo lo anterior tras un mtodo assert que, por ejemplo, tomara como entrada la celda original y la cantidad de celdas (incluida la choza) que queremos verificar.
Si despus de terminar de escribir cdigo con TDD necesitamos una gran refactorizacin como la que hemos hecho en esta seccin, es probable que no hayamos aplicado bien TDD.
A pesar de estas refactorizaciones, ni el proceso que hemos seguido ha sido todo lo sencillo y elegante que debera ser aplicando TDD, ni la solucin ha quedado sencilla. Vamos a analizar los fallos y vamos a intentar corregirlos. Retrospectica de la primera solucin Los problemas que hemos encontrado y que vamos a analizar en esta seccin son. Exponer detalles de implementacin y dependencia de los ndices. Confundir el concepto de pocas lneas de cdigo con el concepto de babysteps. Implementar dos clases a la vez con un nico conjunto de pruebas. No detectar el mal olor de incumplir la ley de Demeter. Separacin del dominio del problema y de la solucin. No hemos utilizado un diario de diseo. Durante todo el desarrollo hemos sido muy dependientes de listas e ndices. Hemos necesitado saber, por ejemplo, los ndices que correspondan a las chozas y los 53
pozos. Hemos expuesto detalles de implementacin desde el principio (la lista de celdas). Tampoco hemos codificado en babysteps de manera estricta. Hemos adelantado demasiado e implementamos cdigo no cubierto por pruebas. Por ejemplo en la seccin Haced sitio, slo tenamos que escribir el cdigo para quitar las semillas del pozo 0 y aadirlas en los pozos 1, 2, 3, y 4. Sin embargo escribimos una solucin general para cualquier escenario. Esta solucin funciona para todos los pozos? Es la solucin ms adecuada? Hemos necesitado ms pruebas para verificar que la implementacin del mtodo seed funciona en otros escenarios (que ya podemos ver que no funciona porque no tiene en cuenta las chozas). No cuenta solo la cantidad del cdigo sino la complejidad del mismo. Este caso ilustra como unas pocas lneas complejas pueden dar lugar a errores inesperados, como nos pas, ya que no comprobamos que estuviramos calculando correctamente el lmite del bucle. Una solucin ms adecuada hubiese sido empezar con la estrategia fake y aadir una segunda prueba para evolucionar el cdigo (estrategia de triangulacin). Esta segunda prueba habra detectado el error en el bucle. Hemos estado trabajando con dos clases simultneamente, las clases: Board y Cell. Sin embargo, solo hemos escrito pruebas para la clase Board (salvo la primera prueba de la kata) y no hemos escrito pruebas unitarias para la clase Cell a pesar de aadir y modificar su cdigo durante la kata. Hemos utilizado las pruebas de Board para probar el cdigo de Cell. Haber escrito una prueba para Cell antes de implementar la prueba para Board no hubiese sido la solucin ms adecuada. Veamos porqu. Desde el principio, el autor de esta solucin tena un diseo en su mente, con una clase Board y una clase Cell y ha querido implementarlo aplicando TDD. Empezar a aplicar TDD con un posible diseo en la mente no es malo. TDD nos avisa de si este diseo funciona o no. El problema es cuando nos centramos en nuestra idea y no escuchamos a TDD, y esto es lo que ha pasado. En las secciones anteriores, TDD nos estaba avisando mediante las pruebas (y el mal olor que veremos continuacin) de que un cambio en Board implicaba un cambio en Cell. Ambas clases estn tan ligadas que no tiene sentido considerarlas por separado, por lo que Cell sera una clase interna de Board.
TDD te obliga a escribir todo el cdigo en una nica clase para extraerlo, mediante refactorizaciones. TDD es muy til cuando no tenemos claro qu clases construir, pero si ya sabemos las clases que necesitamos: adelante. Con TDD descubrirs si tu diseo es fcil de usar y probar.
Por si lo anterior no fuera suficiente, mantener ambas clases separadas incumple la ley de Demeter e introduce un mal olor en nuestro cdigo que nos ha acompaado durante todo el desarrollo. Puedes ver este mal olor, por ejemplo (por 54
ejemplo en el cdigo 4.8) o en el cdigo 4.2 que repetimos a continuacin. Repasemos el cdigo 4.2 dnde tambin aparece este mal olor. Cdigo 4.2. @Test public void testSeeding4SeedsFromCell0_Cells_1_2_3_4_HasOneMoreSeeds() { b.seed(0);
La ley de Demeter nos dice, de manera resumida, que un objeto solo debe comunicarse con sus amigos inmediatos, es decir, con los objetos de los que tiene dependencia directa, como parmetros y atributos. En el cdigo 4.2, el objeto que contiene los mtodos de prueba tiene una dependencia del objeto de la clase Board. Este objeto, a su vez, depende de objetos de la clase Cell (imagen 03). Sin embargo la prueba no depende de Cell por lo que no debera comunicarse con estos objetos, cosa que s est pasando en el cdigo 4.2. Imagen 03. Dependencias de las clases
Este mal olor aparece cuando separo la lgica de negocio los datos que dicha lgica de negocios necesita. Es fcil de detectar cuando descubrimos que tenemos que llamar a un mtodo que devuelve un objeto sobre el que llamamos a otro mtodo que devuelve otro objeto, y as varias veces hasta que, al final, llegamos al valor que buscbamos. Esto aumenta la dependencia de otros objetos, lo que hace al cdigo ms difcil de utilizar y de cambiar. Una buena estrategia para evitar este mal olor es mover la lgica a quin tiene los datos. Lo correcto hubiera sido que el propio tablero nos devolviera el nmero de semillas, por ejemplo con un mtodo getSeedInCell(int index), en lugar del propio objeto Cell. Otro problema que puede parecer trivial, pero no lo es en absoluto, es la separacin entre conceptos del dominio del problema y el dominio de la solucin. En el enunciado de la kata hablamos de chozas y pozos, pero la clase que las implementan se llama Cell. Te has perdido en algn momento de la explicacin o te has confundido porque hablamos de objetos Cell en vez de pozos (o viceversa)? Si esto sucede esto en un 55
ejemplo tan sencillo imagnate en un proyecto con varias personas y clientes de verdad. Si cada uno habla en su propia jerga ser difcil entenderse y los malentendidos estarn al orden del da, adems del tiempo que se pierde traduciendo conceptos que, en verdad, son los mismos. Lo mejor es intentar utilizar los mismos conceptos del problema en el cdigo. Por ltimo, no hemos utilizado el diario de diseo en ningn momento. No s si t, lector, te habrs sentido perdido en algn momento durante las secciones anteriores. Te puedo asegurar que yo s me he sentido perdido en algn momento y que, a veces, al final de una implementacin o una refactorizacin, he tenido que volver a consultar el enunciado para hacerme una idea de qu quedaba por implementar. Como ves, hay varias cosas que no han funcionado a pesar de haber aplicado TDD. Esto no es malo, al contrario, hemos aprendido muchos detalles interesantes. Vamos a repetir el ejercicio aprovechando la experiencia que ya tenemos y aplicando mejor las ideas de TDD que vimos en el primer captulo y en los ejercicios anteriores. Un nuevo comienzo. Vamos a repetir la kata Mancala, poniendo en prctica lo que hemos aprendido, evitando los errores que hemos cometido y aplicando mejor los consejos que vimos en el primer captulo. Por ello, lo primero que hacemos es un diario de diseo con todo lo que queremos implementar en esta cada. 4.1. Diario de diseo Inicialmente, el tablero tiene 6 pozos por cada jugador, 12 en total y dos chozas por cada jugador (para un total de 14 casillas). Cada pozo tiene 4 semillas y las chozas estn vacas. El primer jugador puede plantar cualquiera de sus pozos que tenga al menos una semilla. El segundo jugador puede plantar cualquiera de sus pozos que tenga al menos una semilla. Cuando un jugador planta un pozo, ese pozo queda sin semillas. Cuando se planta en el ltimo pozo (choza del segundo jugador) y an quedan semillas, se contina plantando desde el principio (primer pozo del primer jugador). Si se intenta sembrar un pozo no vlido, el Mancala no cambia.
Al igual que en la implementacin anterior, vamos a comenzar definiendo el estado inicial del Mancala. Nuestra segunda primera prueba. Empezamos escribiendo una prueba que verifique el estado inicial del tablero, pero ahora vamos a intentar independizarnos lo mximo posible de la implementacin y los atributos del tablero. Cmo podemos hacerlo? 56
En Java todos los objetos tienen un mtodo toString cuya misin es devolver una cadena de texto que represente al objeto. Podemos utilizar esta cadena de texto para representar el estado del Mancala, cmo muestra el cdigo 4.20. Cdigo 4.20 Prueba. public class TestMancala { @Test public void testManacalaInicial() { String expected = "0, 4, 4, 4, 4, 4, 4\n" + "4, 4, 4, 4, 4, 4, 0";
Mancala m = new Mancala(); String mancalaState = m.toString();
En el cdigo 4.20 hemos utilizado la estrategia fake y hemos declarado directamente la cadena de texto que representa el Mancala inicial en el mtodo toString para que la prueba pase con xito. Utilizando una cadena de texto nos aislamos de los detalles de implementacin de la clase Mancala y definimos en una sola prueba el nmero de casillas, las semillas iniciales y el estado de las chozas. Ahora podramos refactorizar para construir esa cadena a partir del estado de los pozos y las chozas, pero vamos a dejarlo as y vamos a ver, en la prxima seccin un error muy comn cuando se empieza a programar en TDD. Un error comn. Triangular el fake Vamos a abordar ya la prueba de sembrar y vamos a comenzar, de nuevo, por comprobar que, cuando sembramos, el pozo elegido se queda sin semillas. Escribimos una prueba utilizando la representacin del estado del Manca como una cadena de texto en el cdigo 4.21. Cdigo 4.21.
public void seed(Player firstPlayer, int i) { s = "0, 4, 4, 4, 4, 4, 4\n" + "0, 4, 4, 4, 4, 4, 0";
} }
Aunque la prueba pasa correctamente, el cdigo no ha evolucionado bien. En este caso (cdigo 4.22) hemos triangulado el propio fake, en lugar de triangular el cdigo real. La manera de arreglar este error es refactorizar antes y quitar el fake, aadiendo un autntico estado a la clase Mancala. Refactorizar y avanzar Volvamos al punto en el que estbamos justo despus de terminar el primer ciclo de TDD (cdigo 4.20) y refactoricemos. Es importante que esta refactorizacin no introduzca apenas cdigo nuevo, ya que si usamos las refactorizaciones para escribir el cdigo que no hemos escrito en el momento de hacer que una prueba pase de rojo a verde estaremos aplicando mal TDD. En este caso, lo ms sencillo parece utilizar una tabla. Cada posicin de la tabla representa a un jugador y contiene una segunda tabla con todos los pozos de dicho jugador, incluyendo las chozas. Los ceros representan las chozas y las dos series de 4s los pozos de ambos jugadores con sus semillas iniciales. Cdigo 4.23.
@Override public String toString() { return Arrays.toString(pits); } }
Java no genera directamente una cadena con los elementos de un array, as que utilizamos el mtodo toString de la clase Arrays que ya viene en el API de Java. Sin embargo, este mtodo devuelve una cadena con un formato distinto a la cadena que habamos escrito en la prueba, por lo que tenemos que modificar la prueba como muestra el cdigo 4.24. Cdigo 4.24. Cambiamos la cadena de texto para que la prueba pase con xito
Mancala m = new Mancala(); String mancalaState = m.toString();
assertEquals(mancalaState, expected); } }
Ya podemos tachar nuestras primeras tareas del diario de diseo. 4.1. Diario de diseo Inicialmente, el tablero tiene 6 pozos por cada jugador, 12 en total y dos chozas por cada jugador (para un total de 14 casillas). Cada pozo tiene 4 semillas y las chozas estn vacas. El primer jugador puede plantar cualquiera de sus pozos que tenga al menos una semilla. El segundo jugador puede plantar cualquiera de sus pozos que tenga al menos una semilla. Cuando un jugador planta un pozo, ese pozo queda sin semillas. Las y Cuando un jugador planta un pozo, el resto de pozos y chozas gana una Cuando se planta en el ltimo pozo (choza del segundo jugador) y an quedan semillas, se contina plantando desde el principio (primer pozo del primer jugador). Si se intenta sembrar un pozo no vlido, el Mancala no cambia. Triangulando la siembra Ahora ya podemos escribir una prueba para sembrar semillas y comenzar a triangular la siembra. La prueba que escribimos justo antes de triangular el fake est en el cdigo 4.25.
Mancala m = new Mancala(); m.seed(Player.FIRST_PLAYER, 0); String mancalaState = m.toString();
assertEquals(mancalaState, expected); }
Vamos a comentar algunos detalles de esta prueba. En primer lugar estamos utilizando ndices individuales para cada celda, es decir, el primer jugador tendr sus celdas en los ndices de 0 a 5 y el segundo jugador tambin tendr sus celdas en esos mismos ndices. En el array, las celdas del primer jugador sern las primeras (ndices del 1 al 7) y las del segundo las segundas (ndices del 9 al 14). Esta prueba no es correcta del todo, ya que adems de quedar a cero el pozo elegido, los cuatro pozos siguientes incrementan sus semillas en 1, pero es suficiente para continuar avanzando. Ms adelante aadiremos ms informacin a esta prueba. El cdigo mnimo para la prueba anterior es el cdigo 4.26 (recuerda que el primer pozo del primer jugador est en el ndice 1 de la tabla pits). Cdigo 4.26. public void seed(Player player, int pitIndex) { this.pits[1] = 0; }
Sin embargo la prueba del cdigo 4.25 no pasa con xito ya que hemos declarado la tabla pit como constante y sus valores no se modifican. Vamos a dejar un momento esta prueba de lado y a refactorizar la creacin de la tabla como se muestra en el cdigo 4.27. Tambin vamos a refactorizar la prueba para calcular el ndice a partir del parmetro de entrada Cdigo 4.27. Refactorizacin para crear un array modificable. int [] pits; private final static int PLAYERS = 2; private final static int PITS = 7; private final static int INITIALSEEDS = 4;
public Mancala() { pits = new int[PLAYERS * PITS]; Arrays.fill(pits, INITIALSEEDS); pits[0] = 0; pits[7] = 0; } Refactorizacin para calcular el ndice a partir del parmetro. public void seed(Player player, int pitIndex) { this.pits[pitIndex + 1] = 0; } 60
Con esta refactorizacin las dos pruebas que tenemos pasan con xito y terminamos otra de las tareas de nuestro diario de diseo. 4.1. Diario de diseo Inicialmente, el tablero tiene 6 pozos por cada jugador, 12 en total y dos chozas por cada jugador (para un total de 14 casillas). Cada pozo tiene 4 semillas y las chozas estn vacas. El primer jugador puede plantar cualquiera de sus pozos que tenga al menos una semilla. El segundo jugador puede plantar cualquiera de sus pozos que tenga al menos una semilla. Cuando un jugador planta un pozo, ese pozo queda sin semillas. Cuando se planta en el ltimo pozo (choza del segundo jugador) y an quedan semillas, se contina plantando desde el principio (primer pozo del primer jugador). Si se intenta sembrar un pozo no vlido, el Mancala no cambia. Triangulando pruebas y cdigo Vamos a implementar la funcionalidad de aadir las semillas sembradas en los pozos siguientes. No tiene sentido escribir una nueva prueba, ya que la condicin de la prueba 4.25 nunca ser cierta porque no estamos comprobando el incremento de los pozos. En este caso es mejor modificar dicha prueba para incorporar la nueva funcionalidad, como hemos hecho en el cdigo 4.28. Cdigo 4.28. Aadimos que los 4 pozos siguientes tengan 1 semilla ms. @Test public void testPrimerJugadorSiembraSuPrimerPozo_EsePozoQuedaSinSemillas_y_LosCuatroP ozosSiguientesPasanA5() { String expected = "[0, 0, 5, 5, 5, 5, 4, " + "4, 4, 4, 4, 4, 4, 0]";
La implementacin en babystep es un fake (cdigo 4.29). La prueba pasa correctamente. Cdigo 4.29. public void seed(Player player, int pitIndex) { this.pits[pitIndex + 1] = 0;
Las prximas pruebas van a triangular el mtodo seed, por lo que vamos a refactorizarlo para que sea ms sencillo de modificar. El primer cambio es reemplazar los cuatro incrementos por un bucle. A diferencia de la solucin anterior, las cuatro lneas repetidas del cdigo 4.29 me indican cules son los ndices de inicio y fin correctos para el bucle (cdigo 4.30). Cdigo 4.30. Aadimos un bucle para icnrementar los pozos. public void seed(Player player, int pitIndex) {
this.pits[pitIndex + 1] = 0;
for (int i = pitIndex + 2; i <= (pitIndex + 4); i++) { this.pits[i]++; } }
Esta prueba solo funcionar cuando sembremos un pozo que contenga exactamente cuatro semillas, as que no podemos dar esta funcionalidad por concluida. Vamos a seguir triangulando con una nueva prueba, por ejemplo que intente sembrar un pozo sin semillas (cdigo 4.31). Cdigo 4.31. Sembramos un pozo sin semillas. @Test public void testPrimerJugadorSiembraUnPozoConCeroSemillas_ElMancalaQuedaIgual() { String expected = "[0, 0, 5, 5, 5, 5, 4, " + "0, 4, 4, 4, 4, 4, 4]";
La prueba falla, por lo que es el momento de modificar el mtodo seed para que solo se siembren las semillas del pozo de origen indicado por pitInde4. El resultado se muestra en el cdigo 4.32. Cdigo 4.32.. public void seed(Player player, int pitIndex) { int seeds = this.pits[pitIndex + 1]; this.pits[pitIndex + 1] = 0;
for (int i = pitIndex + 2; i <= (pitIndex + 4); i++) { this.pits[i]++; } }
Vamos a hacer dos refactorizaciones para terminar. En la primera, movemos el cdigo que quita las semillas del pozo a un mtodo auxiliar y, en la segunda, aadimos una nueva variable local que incremente el ndice del pozo en 1. El cdigo 4.33 muestra lo que llevamos hasta ahora despus de hacer esas refactorizaciones. 62
Cdigo 4.33. Mtodo seed refactorizado public void seed(Player player, int pitIndex) { int pit = pitIndex + 1; int seeds = removeSeedsFrom(pit);
for (int i = pit +1; i <= (pit + seeds); i++) { this.pits[i]++; } }
private int removeSeedsFrom(int pit) { int seeds = this.pits[pit]; this.pits[pit] = 0;
return seeds; }
Hasta aqu hemos conseguido que el primer jugador pueda plantar sus pozos, pero este cdigo no funcionar con el segundo jugador. 4.1. Diario de diseo Inicialmente, el tablero tiene 6 pozos por cada jugador, 12 en total y dos chozas por cada jugador (para un total de 14 casillas). Cada pozo tiene 4 semillas y las chozas estn vacas. El primer jugador puede plantar cualquiera de sus pozos que tenga al menos una semilla. El segundo jugador puede plantar cualquiera de sus pozos que tenga al menos una semilla. Cuando un jugador planta un pozo, ese pozo queda sin semillas. Cuando se planta en el ltimo pozo (choza del segundo jugador) y an quedan semillas, se contina plantando desde el principio (primer pozo del primer jugador). Si se intenta sembrar un pozo no vlido, el Mancala no cambia.
Implantando el segundo jugador Para completar nuestro cdigo y que soporte al segundo jugador vamos a escribir una prueba que siembre un pozo del segundo jugador (cdigo 4.34). Cdigo 4.34. Primera prueba para el segundo jugador @Test public void testSegundoJugadorSiembraSuPrimerPozoCon_4_Semillas() { String expected = "[0, 4, 4, 4, 4, 4, 4, " + "0, 0, 5, 5, 5, 5, 4]";
La prueba anterior falla porque el mtodo seed siembra el pozo del primer jugador. Para corregir esto, el mtodo debe detectar si queremos sembrar en el pozo del segundo jugador y modificar el ndice de la tabla pits. En este caso hacemos una implementacin obvia que se muestra en el cdigo 4.35. Cdigo 4.35. Un condicional cambia el ndice si es el segundo jugador. public void seed(Player player, int pitIndex) { int pit = pitIndex + 1;
if (player == Player.SECOND_PLAYER) { pit += 7; }
int seeds = removeSeedsFrom(pit);
for (int i = pit +1; i <= (pit + seeds); i++) { this.pits[i]++;
} }
Con el aadido del cdigo 4.34 el segundo jugador ya puede sembrar sus pozos. Como refactorizacin, vamos a extraer la funcionalidad de calcular el ndice correcto a un mtodo auxiliar (cdigo 4.36). Cdigo 4.36. public void seed(Player player, int pitIndex) { int pit = calculatePit(player, pitIndex); int seeds = removeSeedsFrom(pit);
for (int i = pit +1; i <= (pit + seeds); i++) { this.pits[i]++;
} }
private int calculatePit(Player player, int pitIndex) { int pit = pitIndex + 1;
Actualizamos el diario de diseo antes de continuar. 4.1. Diario de diseo Inicialmente, el tablero tiene 6 pozos por cada jugador, 12 en total y dos chozas por cada jugador (para un total de 14 casillas). Cada pozo tiene 4 semillas y las chozas estn vacas. El primer jugador puede plantar cualquiera de sus pozos que tenga al menos una semilla. El segundo jugador puede plantar cualquiera de sus pozos que tenga al menos una semilla. Cuando un jugador planta un pozo, ese pozo queda sin semillas. 64
Cuando se planta en el ltimo pozo (choza del segundo jugador) y an quedan semillas, se contina plantando desde el principio (primer pozo del primer jugador). Si se intenta sembrar un pozo no vlido, el Mancala no cambia. Del final al principio Ya queda poco por implementar en nuestro diario de diseo. En esta seccin vamos a implementar la funcionalidad necesaria para que siga plantando por el principio cuando llegue al final y an quedan semillas. Esto pasa, por ejemplo, si desde el Mancala inicial, el segundo jugador planta su primer pozo (prueba del cdigo 4.37). Cdigo 4.37. El segundo jugador planta su ltimo pozo. @Test public void testSiSeLlegaAlUltimoPozoYAunQuedanSemillasSeContinuaPlantandoPorElPrime rPozo() { String expected = "[1, 5, 5, 5, 4, 4, 4, " + "0, 4, 4, 4, 4, 4, 0]";
La prueba falla ya que el ndice de la tabla pits se dale de los lmites. Como ya vimos en la solucin anterior una manera sencilla de calcular el ndice correcto, vamos a reutilizarla y la aadirnos como una implementacin obvia al mtodo seed (cdigo 4.38). Cdigo 4.38. public void seed(Player player, int pitIndex) { int pit = calculatePit(player, pitIndex); int seeds = removeSeedsFrom(pit);
for (int i = pit +1; i <= (pit + seeds); i++) { this.pits[(i % 14)]++;
} }
Con esta modificacin todas las pruebas pasan correctamente y ya solo nos queda una ltima funcionalidad en nuestro diario de diseo. 65
Solo pozos vlidos La ltima funcionalidad que nos queda en el diario de diseo es verificar que el mtodo seed recibe el ndice un pozo vlido. En este caso el ndice es vlido si est comprendido entre 0 y 5 ambos inclusive. 4.1. Diario de diseo Inicialmente, el tablero tiene 6 pozos por cada jugador, 12 en total y dos chozas por cada jugador (para un total de 14 casillas). Cada pozo tiene 4 semillas y las chozas estn vacas. El primer jugador puede plantar cualquiera de sus pozos que tenga al menos una semilla. El segundo jugador puede plantar cualquiera de sus pozos que tenga al menos una semilla. Cuando un jugador planta un pozo, ese pozo queda sin semillas. Cuando se planta en el ltimo pozo (choza del segundo jugador) y an quedan semillas, se contina plantando desde el principio (primer pozo del primer jugador). Si se intenta sembrar un pozo no vlido, el Mancala no cambia.
Para implementar esta funcionalidad en nuestro cdigo, primero escribiramos una prueba con un ndice menor que cero, la haramos pasar con xito y luego repetiramos con una prueba con un ndice mayor que 5. Como es una tarea sencilla, vamos a ver ambas pruebas juntas en el cdigo 4.39. Cdigo 4.39. Pruebas para verificar que no pasa nada con un pozo incorrecto ndice menor que cero. @Test public void testSiSeleciconoUnPozoMenorQueCeroNoPasaNada() { String expected = "[0, 4, 4, 4, 4, 4, 4, " + "0, 4, 4, 4, 4, 4, 4]";
El cdigo (ya refactorizado) que hace pasar con xito las pruebas anteriores se muestra en el cdigo 4.40. Cdigo 4.40. public void seed(Player player, int pitIndex) { if (!isValid(pitIndex)) return;
int pit = calculatePit(player, pitIndex); int seeds = removeSeedsFrom(pit);
for (int i = pit +1; i <= (pit + seeds); i++) { this.pits[i]++;
Y con esto terminamos la segunda implementacin de la kata Mancala. Vamos a resumir las conclusiones de esta segunda solucin y, despus, compararemos ambas implementaciones. Retrospectica de la segunda solucin En esta segunda solucin todo ha ido mejor que en la primera. Hemos aplicado TDD de manera ms correcta y hemos tenido menos dificultades escribiendo pruebas e implementando el cdigo. Tambin hemos encontrado menos errores inesperados. Pero parte de este xito tambin se debe a lo que aprendimos implementando la kata Mancala por primera vez. Gracias a la primera solucin por ejemplo, hemos separado desde el principio el ndice identificaba una celda del ndice interno, o hemos utilizado directamente la frmula para calcular los ndices. No debes desanimarte cuando un fragmento de cdigo no salga a la primera. Un fallo es una oportunidad para mejorar tu experiencia con TDD y para descubrir caractersticas importantes del cdigo que ests implementando y que podr sutilizar en un segundo intento. Si queremos que todo salga bien a la primera y no estamos dispuestos a quitar el cdigo que ha salido mal y volverlo a intentar nos estamos cerrando la puerta a mejorar. El principal problema principal de esta segunda solucin es la dependencia de las cadenas de texto. Aunque usar una cadena de texto nos asla de los detalles de implementacin, tambin introduce una fragilidad en las pruebas. Un pequeo cambio en la cadena (una coma, un espacio) invalida todas las pruebas. Este no es un motivo de peso para no usar esta estrategia, sino para mejorarla con tcnicas y herramientas adicionales, por ejemplo TestText. Puede seguir practicando con esta estrategia implementado la kata MineSweeper que puedes encontrar en Internet. 67
Comparacin entre ambas soluciones. Vamos a comparar ambas soluciones (la primera solucin utilizando una lista y la clase Cell y la segunda solucin utilizando una tabla de enteros). Para ello, hemos calculado el conjunto de mtricas de cdigo que se muestra en la tabla 1. El primer nmero indica el valor medio y el segundo indica el valor mximo Tabla 1. Mtricas de las dos soluciones. Mtrica Primera solucin Segunda solucin Average Block Depth 0.96 / 2 0.95 / 2 Cyclomatic Complexity 1.13 / 3 1.23 / 3 Lines Of Code Per Method 5.17 / 12 6.29 / 9 Num. of Methods Per Type 5.25 / 11 5.00 / 9 Number of Parameters 0.23 / 2 0.40 / 2 Efferent Couplings 2 2 Lines of Code 143 130 Number of Methods 21 15 Test cases 10 (1 de Cell) 8
Segn las mtricas de profundidad de bloque y la complejidad ciclomtica, ambas soluciones presentan una complejidad similar, aunque la segunda solucin tiene una complejidad ciclomtica ligeramente mayor. En cuanto al tamao la segunda solucin tiene menos lneas de cdigo y menos mtodos, a pesar de haber refactorizado ms que la primera solucin. Los mtodos de la segunda tienen ms cdigo que la primera con una media de 6,29, pero dicho cdigo est distribuido de manera ms homogneo ya que el mtodo ms largo de la segunda solucin slo tiene 9 lneas por 12 lneas de la primera solucin Respecto al acoplamiento, ambas soluciones presentan valores similares y bastante bajos. Estos resultados reflejan que utilizar una clase para encapsular la lgica de gestionar cada uno de los pozos, como la clase Cell de la primera solucin, no supone una mejora apreciable en la solucin final. Para reflexionar Un detalle a tener en cuenta es que no hemos escrito ningn comentario en el cdigo, ni en los casos de prueba ni en el cdigo de produccin. Los has echado de menos? Crees que hubieran sido necesarios? Si crees que, en el futuro, te costara entender alguna arte del cdigo de esta kata piensa cmo mejorar su legibilidad con buen refactorizacin, o con una prueba adicional, antes que con un comentario. Uno de los principales errores a la hora de aplicar TDD y que hemos cometido en la primera solucin es querer ir demasiado rpido escribiendo el cdigo. A veces tenemos el cdigo en la cabeza y queremos volcarlo rpidamente en el entorno de desarrollo. Pero nuestro cerebro no es un compilador y puede tener cdigo que no 68
funcione y que sea difcil y costoso hacerlo funcionar. Al aplicar TDD de manera ms estricta en la segunda solucin hemos escrito menos cdigo. TDD nos lleva a buscar la solucin ms sencilla posible, por lo que escribimos menos cdigo. Conclusiones En este captulo hemos visto dos soluciones distintas a una misma kata. Hemos tenido el coraje para descartar nuestro cdigo y volver a empezar y, como resultado, hemos obtenido una solucin mejor: ms simple y ms pequea. Aunque hemos completado el enunciado que planteamos al principio del captulo, el juego del Mancala no est completo. A continuacin tienes una lista de la funcionalidad que falta por implementar para completarlo. Si a la hora de sembrar se da una vuelta completa al Mancala, se omite el pozo de origen. Es decir, despus de una siembra el pozo de origen est vaco, independientemente de las vueltas que hayamos dado al Mancala sembrando. El juego termina cuando el jugador al que le toca mover no puede por no tener semillas. Cuando el juego termina porque un jugador no puede mover, el otro jugador coge todas las semillas de sus pozos y las guarda en su choza. El ganador es el jugador con ms semillas en su choza. Agradecimientos del captulo A Antonio Martnez por inspirar la idea para este captulo y por sus revisiones de los primeros borradores.
69
Captulo 5. Ejercicio TDD para presentar y usar mocks / doubles, etc.
70
Captulo 6. Cifrado Esctala y Refactoriacin
La legibilidad del cdigo es un aspecto clave en TDD ya que vamos aadiendo nuevo cdigo de manera incremental. Para mejorar la legibilidad aplicamos refactorizaciones despus de escribir el mnimo cdigo que pasa con xito una prueba. Este captulo tiene una orientacin diferente a la de captulos anterior. Ahora, no empezaremos desde cero sino que partiremos de una solucin al problema que, aunque correcta, presenta un buen nmero de malos olores que reautorizaremos. El Problema El cifrado Esctala es una tcnica utilizada en la antigua Grecia para comunicar mensajes secretos. Para implementarla, el emisor coga una tira de cuero, la enrollaban alrededor de una vara y escriban el mensaje en ella a lo largo de la vara. El receptor del mensaje deba tener una vara de grosor similar para enrollar nuevamente el mensaje y poder leerlo. El Cdigo A continuacin puedes ver una implementacin de la codificacin y decodificacin utilizando la tcnica de la Exctala.
71
Cdigo 6.1
public class Escitala { private int caras; private String frase; private String[][] escitala; private int largo;
public String encrypt() { if (isValid()) {//comprobamos la frase las caras etc largo = (frase.length() % caras) == 0 ? frase.length() / caras: frase.length() / caras + 1; //Si el mensaje dividido entre las caras de la escitala da resto cero no sobraran espacios escitala = new String[caras][largo]; //las caras representan las columnas de la escitala //el largo representa las filas
int pivote = 0; //nos servir de pivote para sealar el caracter actual en la frase
for (int columna = 0; columna < largo; columna++) { //recorremos todas las filas DE CADA COLUMNA for (int fila = 0; fila < caras; fila++) { //recorremos todas las columnas if (pivote < frase.length()) { //Si no hemos recorrido toda la frase escitala[fila][columna] = String.valueOf(frase.charAt(pivote)); //Almacenamos el caracter que toque dentro del Array pivote++; //sumamos uno al pivote para apuntar al siguiente caracter de la cadena de texto a encrypt } else //Si hemos llegado al final de la frase pero an quedan datos que cumplimentar en el array escitala[fila][columna] = aleatorio(); //como ya no quedan letras de la frase generamos un caracter aleatorio para cumplimentar los elementos del array } } return toText(caras, largo); //llamamos con la cantidad de filas } return null; }
public String decrypt(String frase2) { if (isValid()) { //comprobamos la frase las caras etc largo = (frase.length() % caras) == 0 ? frase.length() / caras: frase.length() / caras + 1; //Si el mensaje dividido entre las caras de la escitala da resto cero no sobraran espacios escitala = new String[largo][caras];
int pivote = 0; //Nos servir de pivote para la frase
for (int columnas = 0; columnas < caras; columnas++) { //OJO esta vez la cantidad de columnas lo determinarn las caras for (int filas = 0; filas < largo; filas++) { //OJO esta vez las la cantidad de filas vendr determinada por el largo. escitala[filas][columnas] = String.valueOf(frase2.charAt(pivote)); //vamos escribiendo caracter a caracter en el array bidimensional pivote++; //avanzamos el pivote una posicin para que apunte al siguiente caracter del String a convertir. } } return toText(largo, caras); //llamamos con la cantidad de filas, columnas. OJO hay que pasar como parmetro la variable que indique el mximo de filas y luego la 72
} //que indique el mximo de columnas. return null; }
private String aleatorio() { int aleatorio = (int) (Math.random() * 100) + 1; //calculamos un aleatorio de 1 a 100. Podramos haber pusto otro rango.
return String.valueOf((char) aleatorio); //Devolvemos como String el char que representa a ese nmero generado aleatorimente. (Cdigo ASCII) }
private String toText(int f, int c) { StringBuilder sb = new StringBuilder(); //Ya que no trabajamos con hilos creamos un StringBuilder que es ms eficiente en estos casos
for (int filas = 0; filas < f; filas ++) { //Recorremos el array for (int columnas = 0; columnas < c; columnas++) { sb.append(escitala[filas][columnas]); //y vamos agregando al StringBuilder caracter a carater } } return sb.toString(); //Devolvemos como String el contenido del StringBuilder }
if (caras > 0 && frase != null && frase.length() > caras) { correcto = true; } else System.out.println("Las cara deben ser un nmero entero positivo comprendido y la frase no puede ser nula. \n" + ".Adems la cantidad de letras de la frase no puede ser menor al nmero de caras"); return correcto; }
public int getCaras() { return caras; }
//MTODOS GETTERS AND SETTERS TPICOS. public void setCaras(int caras) { if (caras > 0) this.caras = caras; }
public String getFrase() { return frase; }
public void setFrase(String frase) { this.frase = frase; } }
Vamos a trabajar con el cdigo anterior a lo largo de este captulo. Podemos leerlo? Es difcil trabajar con un cdigo difcil de leer y el cdigo anterior lo es. Las lneas son muy largas y, adems los comentarios se incluyen en la misma lnea. Hay un nmero excesivo de comentarios que no facilitan, sino dificultan entender el cdigo. 73
El primer paso que daremos ser eliminar comentarios superfluos (como recorremos todas las columnas) y reorganizar el formato cdigo para que sea fcil de leer. Haremos eso con cuidado, ya que an no tenemos pruebas que nos adviertan si hemos introducido algn error. Cdigo 6.2 package escitala;
public class Escitala { private int caras; private String frase; private String[][] escitala; private int largo;
public String encrypt() { if (isValid()) { //Si el mensaje dividido entre las caras de // la escitala // da resto cero no sobraran espacios largo = (frase.length() % caras) == 0 ? frase.length() / caras: frase.length() / caras + 1; //las caras representan las columnas de // la escitala //el largo representa las filas escitala = new String[caras][largo];
// pivote para sealar el caracter actual // en la frase int pivote = 0;
for (int columna = 0; columna < largo; columna++) { for (int fila = 0; fila < caras; fila++) { if (pivote < frase.length()) { escitala[fila][columna] =
String.valueOf(frase.charAt(pivote)); pivote++; } else //como ya no quedan letras de //la frase generamos un caracter // aleatorio para cumplimentar //los elementos del array escitala[fila][columna] = aleatorio(); } } return toText(caras, largo); } return null; } 74
public String decrypt(String frase2) { if (isValid()) { largo = (frase.length() % caras) == 0 ? frase.length() / caras: frase.length() / caras + 1; escitala = new String[largo][caras];
int pivote = 0;
//la cantidad de columnas lo determinarn las caras for (int columnas = 0; columnas < caras; columnas++) { for (int filas = 0; filas < largo; filas++) { escitala[filas][columnas] =
if (caras > 0 && frase != null && frase.length() > caras) { correcto = true; } else System.out.println("Las cara deben ser un nmero " + "entero positivo " + "comprendido y la frase no puede ser nula. \n" + ".Adems la cantidad de letras de la frase no " + "puede ser " + "menor al nmero de caras"); return correcto; 75
}
public int getCaras() { return caras; }
public void setCaras(int caras) { if (caras > 0) this.caras = caras; }
public String getFrase() { return frase; }
public void setFrase(String frase) { this.frase = frase; } }
En el cdigo 6.2 hay ms lneas de cdigo de las que tenamos en el 6.1. A cambio, hemos mejorado su legibilidad. Ahora podemos hacer una lista de malos olores. Por ejemplo mensajes de texto incrustados en el texto o bloques de cdigo sin llaves o Cdigo duplicado. Tambin podemos ver, leyendo los comentarios, que la mayora de los nombres parecen inadecuados, ya que hace falta explicar qu hace cada variable, cuando eso debera quedar claro solo por su nombre. Volveremos sobre esto ms adelante, porque, sin pruebas, es mejor no modificar el cdigo. Vamos a crear nuestro diario de diseo para identificar las mejores que vamos a aplicar al cdigo. Cuadro 6.3 Diario de diseo Escribir pruebas. Evitar el cdigo duplicado en los mtodos encript y decript. Quitar el mensaje del constructor y aadirlo al mtodo encript Quitar los mtodos get / set no utilizados Nombres ms descriptivos para variables y mtodos Quitar mensajes. Todos los bloques de cdigo entre llaves.
En los prximos apartados veremos cmo corregir estos malos olores, peor antes vamos a escribir un conjunto de pruebas. Escribiendo Pruebas Por qu tenemos que escribir pruebas si damos por hecho que el cdigo funciona? Las pruebas sern nuestra garanta de que no introducimos errores a la hora de cambiar el cdigo. Asumimos que el cdigo funciona, por lo que comenzamos escribiendo pruebas de aceptacin. Una de ellas (utilizando la sintaxis Gerkin) se muestra en el cdigo 6.4 76
Cdigo 6.4. Escenario.
La implementacin de esta prueba se muestra en el cdigo 6.5. Cdigo 6.5
Si esta prueba diera error, tendramos que escribir pruebas para los mtodos auxiliares buscando acotar el fragmento de cdigo dnde est el error. En este caso, la prueba 6.5 funciona correctamente, por lo que pasamos a la siguiente prueba. Vamos a escribir una prueba para el mtodo de desencriptar. Esta prueba es la misma que la anterior cambiando los mensajes. La implementacin est en el cdigo 6.6. Cdigo 6.6
Esta prueba tambin funciona, por lo que es el momento de empezar a hacer cambios. Cuadro 6.7. Diario de diseo Escribir pruebas. Evitar el cdigo duplicado en los mtodos encript y decript. Quitar el mensaje del constructor y aadirlo al mtodo encript. 77
Sacar los mtodos encript y decript del bloque if. Quitar los mtodos get / set no utilizados Nombres ms descriptivos para variables y mtodos Quitar mensajes de texto. Todos los bloques de cdigo entre llaves. Excepciones en vez de resultados nulos.
Vamos a actualizar el diario de diseo con los cambios que iremos haciendo a continuacin (cuadro 6.7). Desbloqueando a los mtodos principales. Actualmente el cdigo de los mtodos est escondido en un bloque if (cdigo 6.8) Cdigo 6.8.
Tener todo el cuerpo de un mtodo dentro de if lo hace ms difcil de leer y entender, por lo que vamos a cambiar la condicin del if para sacar el cdigo fuera del bloque. El mtodo encript quedara como se muestra en el cdigo 6.9 y le mtodo decript quedara como se muestra en el cdigo 6.10.
78
Cdigo 6.9.
Ejecutamos las pruebas para comprobar que no hemos introducido ningn error y ya podemos tachar esta tarea en nuestro diario de diseo y continuar con la siguiente tarea. Cuadro 6.10. Diario de diseo Escribir pruebas. Evitar el cdigo duplicado en los mtodos encript y decript. Quitar el mensaje del constructor y aadirlo al mtodo encript. Sacar los mtodos encript y decript del bloque if. Quitar los mtodos get / set no utilizados Nombres ms descriptivos para variables y mtodos Quitar mensajes de texto. Todos los bloques de cdigo entre llaves. Excepciones en vez de resultados nulos.
79
Cambiando mensajes por excepciones El mensaje de texto del cdigo actual nos indica que el nmero de caras no es vlido. Sin embargo este cdigo es un componente de otro cdigo por lo que no debe mostrar mensajes directamente. La responsable de enviar mensajes ser la interfaz de usuario. Vamos a cambiar el mensaje por una excepcin. El primer paso ser escribir una prueba que indique el objetivo que buscamos (cdigo 6.11). Cdigo 6.11. Prueba.
La prueba no se puede ejecutar porque nos falta la clase excepcin. Esta clase podemos crearla de manera automtica con los asistentes de los IDEs ms habituales. Un ejemplo, generado con Eclipse se muestra en el cdigo 6.12. Cdigo 6.12. Clase excepcin generada con Eclipse.
Ahora la prueba falla porque, cuando detecta el error, no se lanza la excepcin. Vamos a cambiar eso en el cdigo. Vamos a hacer que la excepcin la lance el mtodo 80
pblico para no dar detalles de mtodos privados. La modificacin se muestra en el cdigo 6.13. Cdigo 6.13. Lanza la excepcin en lugar de devolver nulo
Ahora, vamos a implementar un mensaje descriptivo en la excepcin. De tal manera que, cuando se lance, el mensaje nos indique qu falla y cmo podemos corregirlo. Para ello, empezamos escribiendo una prueba (cdigo 6.14) que nos permita definir la informacin del mensaje. Cdigo 6.14. Lanza la excepcin en lugar de devolver nulo
La prueba anterior, aunque breve, es difcil de entender. Al final del captulo la refactorizaremos. El primer paso para implementar esta prueba es crear un constructor que admita un entero. Esa modificacin se muestra en el cdigo 6.15. Cdigo 6.15. Constructor con el nmero de caras
81
Hemos quitado el resto de constructores del cdigo 6.12 para que siempre que se cree una excepcin se indique el nmero de caras. Vamos a incluir ahora el mensaje y, para ello, aprovechamos el mensaje del cdigo original. El resultado est en el cdigo 6.16. Cdigo 6.16.
Con el cdigo anterior, la prueba del cdigo 6.14 pasa con xito, pero el cdigo de la clase Esctala da un error, ya que hemos quitado constructores. Para ello hacemos aadimos el nmero de caras en el momento de lanzar la excepcin (cdigo 6.17) y comprobamos que todo vuelve a funcionar.
Cdigo 6.17.
Para terminar vamos a refactorizar el mtodo isValid para que ya no muestre el mensaje de error. Adems, cambiaremos el nombre de la funcin para que sea ms claro qu se comprueba y cuando es vlido. El resultado se muestra en el cdigo 6.18. Cdigo 6.18.
Por ltimo ejecutamos todas las pruebas para verificar que el cdigo sigue funcionando. 82
An quedan ms cosas que contar en este captulo.
Conclusiones
Es errneo pensar que cuanto ms comentarios pongamos a nuestro cdigo ms fcil ser de entender. Ahora t mismo puedes comprobarlo comparando el cdigo del principio de este captulo con el resultado final. Qu cdigo entiendes mejor? Tambin puedes comprobar lo tiles que nos han resultado las pruebas que escribimos al principio ya que, junto a los ciclos TDD hechos para cada cambio, nos han dado la seguridad de que no estbamos introduciendo nuevos errores. Despus de cada cambio, el cdigo ha seguido encriptando y desencriptando correctamente. Slo hemos escrito pruebas para los mtodos pblicos y no para los mtodos privados. Vamos a ver una breve justificacin. Los mtodos privados con internos de la clase por lo que no deberan ser probados, ni tampoco contener una lgica demasiado compleja. Adems Java no ayuda a la hora de probar mtodos privados ya que hay que dar rodeos (introspeccin, herencia y delegacin, etc.) para poder acceder a ellos. En el ejemplo que hemos probado los mtodos privados a travs de los mtodos pblicos. Un fallo en un mtodo privado desencadena que no se encripte o desencripte correctamente.
Agradecimientos del captulo
Al usuario Sie7e de Solveet (www.solveet.com) por desarrollar la implementacin original que hemos utilizado de base en este captulo.
83
Captulo 7. Suscriptores y Dobles de Prueba
En este captulo vamos a plantear un problema que implementaremos utilizando dobles de prueba con la librera Mockito. Si quieres conocer cmo funciona Mockito y ver ejemplos que te servirn para entender el cdigo de este captulo puede ir a los anexos de este libro. El problema Tenemos una estructura de datos, por ejemplo, una cola con los mtodos del cdigo 7.1. Modificaremos esta cola para que acepte subscriptores que sern avisados cuando se aada o borre un nuevo elemento a la cola. Cdigo 7.1. API de la cola class Queue<T> { public Queue() {} public void add(T e) {} public T remove(){} public Iterator<T> iterator(){} public int size(){} }
Un subscriptor es cualquier clase que implemente una interfaz concreta (que definiremos a medida que vayamos realizando la implementacin con TDD). La propia 84
cola almacenar todos los subscriptores que tiene. Por simplicidad no tendremos en cuenta la posibilidad de dar de baja a un subscriptor.
El API de Java, desde la versin 1.0 incluye dos interfaces para implementar el patrn observador / observable cuya misin es la misma que el mecanismo de subscripcin que vamos a crear. Estas interfaces son java.util.Observable y java.util.Observer.
Veamos algunos ejemplos de cmo funciona este mecanismo de subscripcin en la tabla 7.1. Tabla 7.2. Funcionamiento de la subscripcin a la cola. Escenario Resultado esperado Se aade un nuevo elemento Se notifica a todos los suscriptores que se ha aadido el elemento junto con el propio elemento. Se borra un elemento Se notifica a todos los suscriptores que se ha borrado el elemento junto con el propio elemento. Se pide borrar un elemento pero la cola est vaca No se notifica nada a los subscriptores. Cualquier otra operacin de la cola Nos e notifica nada a los subscriptores.
A partir de la informacin y los ejemplos anteriores vamos a crear nuestro diario de diseo (cuadro 7.1). 7.1. Diario de diseo. Primera versin. Permitir a un objeto subscribirse a la cola (clase Queue). Enviar una notificacin cuando se aada un elemento. Enviar una notificacin cuando se borre un elemento.
Este problema es muy adecuado para aplicar un doble de prueba. El resultado esperado de la funcionalidad de subscripcin no es un resultado que podamos verificar con un assert. En este caso el resultado esperado, y lo tendremos que comprobar para decidir si la prueba pasa o falla es que el mtodo correcto se haya llamado con los parmetros adecuados. Vamos a comenzar a implementar la funcionalidad de suscripcin eligiendo el caso ms sencillo. 85
La primera prueba de subscripcin Elegimos empezar implementando todo lo necesario para que un objeto pueda subscribirse a la cola y reciba la notificacin correspondiente cuando se aada un nuevo elemento. Comenzamos escribiendo la prueba del cdigo 7.2. Cdigo 7.2. Primera prueba. @Test public void whenAnelementIsAddedOneSubscriptorReceivesACallInMethod_newEvent_() { Subscriber sub = mock(Subscriber.class);
Queue<String> q = new Queue<String>(); q.subscribe(sub);
q.add("new string");
verify(sub).newEvent(); }
Analicemos esta prueba en detalle para ver qu decisiones hemos tomado y qu decisiones estamos posponiendo para el futuro. La primera decisin que hemos tomado ha sido definir el nombre de la interfaz Subscriber. Esta interfaz, que an no existe, la implementarn todos aquellos objetos que se subscriben a la cola. Adems, hemos utilizado la herramienta Mockito para crear un objeto subscriptor sin necesidad de crear una clase que implemente la interfaz. La segunda decisin que hemos tomado ha sido permitir aadir subscriptores a la cola mediante el mtodo subscribe, que tampoco existe an. La tercera, y ltima decisin que hemos tomado al escribir esta prueba es que los suscriptores de una cola van a recibir una llamada al mtodo Subscriber::newEvent cuando se aada un nuevo elemento. Hemos intentado no tomar demasiadas decisiones ahora y posponerlas para el futuro. Por ejemplo, no definimos an qu parmetros acompaaran al mtodo newEvent.
Intenta empezar por el caso ms sencillo y posponer todas las decisiones que puedas. Cuanto ms tardes en tomar una decisin ms informacin tendrs para decidir. Una decisin tomada pronto puede ser una piedra colgando del cuello que vayas arrastrando todo el proyecto.
86
Parece que estamos rompiendo la regla de avanzar con pequeos pasos (o babysteps) y estamos abarcando mucha funcionalidad en esta prueba. Sin embargo hay un buen motivo para hacerlo. Vamos a suponer que comenzamos implementando todo lo necesario para la subscripcin. Cmo podramos probarlo si no hemos definido una manera de aadir suscriptores? Supongamos tambin que comenzamos implementando slo el mecanismo de suscripcin. Cmo podramos verificar que la subscricin se ha realizado correctamente? No hay necesidad de aadir ningn mecanismo pblico. Aunque podramos poner algn mtodo que nos permitiera obtener el estado de las suscripciones, por ejemplo un mtodo getSubscriptires o un mtodo toString, no es necesario para lo que queremos implementar. Las suscripciones por s mismas no tiene valor, el valor los tienen las notificaciones. Las subscripciones son un mecanismo necesario para realizar las notificaciones. Por este motivo, nuestra primera prueba involucra las notificaciones. Para implementar la prueba del cdigo 7.2 tenemos que dar varios pasos, pero todos pequeos y automticos en la mayora de IDEs. El primero de ellos es crear la interfaz Subscriber y el mtodo newEvent (cdigo 7.3). Cdigo 7.3. Primera implementacin de la interfaz public interface Subscriber {
void newEvent();
}
Despus, implementamos el mnimo cdigo posible en la clase Queue para hacer que nuestra prueba se ejecute. Primero optamos por un fake para implementar el mecanismo para aadir un subscriptor (cdigo 7.4). Cdigo 7.4. Subscriptores en la cola
Subscriber sub;
public void subscribe(Subscriber sub) { this.sub = sub; }
El ltimo paso es modificar el mtodo add de la cola para que realice la notificacin. Tambin optamos por un fake para triangular ms adelante (cdigo 7.5).
87
Cdigo 7.5. Notificacin en el mtodo Queue::add
public void add(T e) { // ... Cdigo original ... this.sub.newEvent(); }
Con todo lo anterior, la prueba funciona. En total hemos escrito 6 lneas de cdigo (dos para declarar la interfaz, dos para el mtodo subscribe, una para el atributo sub y otra en el mtodo add) sencilla, por lo que hemos hecho un buen baystep. Vamos a actualizar el diario de diseo con todo lo que hemos aprendido en esta primera prueba. Las decisiones que hemos propuesto las anotamos como tareas para hacer (cuadro 7.2). 7.2. Diario de diseo despus de la primera prueba Permitir a un objeto subscribirse a la cola (clase Queue). Enviar una notificacin cuando se aada un elemento. Permitir subscripciones de varios objetos Enviar la notificacin de aadir a todos los objetos subscritor Notificar el tipo de evento Notificar el elemento aadido o borrado Enviar una notificacin cuando se borre un elemento.
Aunque podramos refactorizar algunas partes del cdigo esperando futuras modificaciones, no lo vamos a hacer an, pero lo haremos pronto. Ahora podemos continuar mejorando los parmetros o bien podemos hacer que se avise a todos los suscriptores. Vamos a hacer esto segundo con la siguiente prueba. Dos son multitud Vamos a evolucionar nuestro cdigo para que admita ms de un subscriptores. Para ello elegimos esa tarea de nuestro diario de diseo y escribimos una prueba que refleje lo que queremos conseguir y que nos permita triangular el cdigo de la cola. Tal y como tenemos organizado el cdigo no tiene mucho sentido considerar slo la posibilidad de aadir ms de un subscriptor sin incluir tambin la verificacin de que las notificaciones llegan a todos los subscriptores, por lo que implementamos ambas tareas en la siguiente prueba (cdigo 7.6).
88
Cdigo 7.6. Prueba con dos subscriptores @Test public void testWhenAnelementIsAddedTwoSubscriptorsReceivesACallInMethod_newEvent_() { Subscriber sub01 = mock(Subscriber.class); Subscriber sub02 = mock(Subscriber.class);
Queue<String> q = new Queue<String>(); q.subscribe(sub01); q.subscribe(sub02);
Realmente esta prueba es muy sencilla, basta con en duplicar el cdigo de la primera prueba que escribimos (cdigo 7.2). Esto es un mal olor que refactorizaremos cuando ambas pruebas funcionen correctamente. Para implementar esta prueba, tenemos que completar el cdigo de Queue que escribimos en la prueba anterior. Primero substituimos el tipo del atributo sub por una lista de subscriptores. Despus el mtodo subscribe aade el nuevo suscriptor a la lista. Por ltimo, cuando se aada un nuevo elemento a la cola, recorreremos la lista de suscriptores llamando al mtodo newEvent de cada uno. Estos cambios estn recogidos en el cdigo 7.7 Cdigo 7.7. Queue soporta ms de un subscriptor List<Subscriber> subs = new ArrayList<Subscriber>();
public void subscribe(Subscriber sub) { this.subs.add(sub); }
public void add(T e) { // Cdigo anterior
for (Subscriber sub: this.subs) { sub.newEvent(); } }
Con el cdigo anterior, ambas pruebas funcionan correctamente. Es el momento de refactorizar para quitar malos olores. 89
Refactorizando pruebas repetidas La segunda prueba que hemos escrito en el cdigo 7.6, hace que la primera prueba que (cdigo 7.32) no sea necesaria, ya que si esta segunda prueba funciona correctamente la primera tambin funcionar. S nos ha sido til escribir ambas pruebas porque ambas nos han permitido que nuestro cdigo progrese y crezca pero ahora no hay necesidad de mantener ambas, por lo que podemos borramos la primera prueba. Tambin vamos a refactorizar la prueba. Sabemos, porque lo tenemos en el diario de diseo del cuadro 7.2, que vamos a modificar el mtodo newEvent de la interfaz subscriptor, por tanto nos interesa aislar todo lo que tenga que ver con este mtodo para hacer las modificaciones en un nico punto y no tener que hacer cambios por todo el cdigo. Si nos fijamos en la prueba del cdigo 7.6 vemos que se comprueba la llamada al mtodo 2 veces. Cuando cambien los parmetros, esta comprobacin tambin cambiar, as que vamos a refactorizar para aislar esta comprobacin en un nico punto. El resultado se muestra en el cdigo 7.8. Cdigo 7.8. @Test public void testWhenAnelementIsAddedTwoSubscriptorsReceivesACallInMethod_newEvent_() { Subscriber sub01 = mock(Subscriber.class); Subscriber sub02 = mock(Subscriber.class);
Queue<String> q = new Queue<String>(); q.subscribe(sub01); q.subscribe(sub02);
Ahora, cuando el mtodo newEvent cambie solo tendremos que cambiar una lnea en el mtodo verifyNewEventIsCalled. Adems, vamos a hacer una rpida refactorizacin de la clase Queue para inicializar el nuevo atributo en el constructor, en lugar de en la declaracin. El resultado se muestra en el cdigo 7.9. 90
Cdigo 7.9. Refatorizacin de la creacin de la lista de subscriptores.
List<Subscriber> subs;
public Queue() { //... subs = new ArrayList<Subscriber>(); }
Vamos a retomar nuestro diario de diseo y a continuar aadiendo pruebas y funcionalidad. Completando las subscripciones Veamos el diario de diseo para saber qu nos queda por hacer y elegir la prxima caracterstica a implementar (cuadro 7.3). 7.3. Diario de diseo despus de la primera prueba Permitir a un objeto subscribirse a la cola (clase Queue). Enviar una notificacin cuando se aada un elemento. Permitir subscripciones de varios objetos Enviar la notificacin de aadir a todos los objetos subscritor Notificar el tipo de evento Notificar el elemento aadido o borrado Enviar una notificacin cuando se borre un elemento.
De entre todas las tareas an nos quedan, elegimos incluir el tipo de evento en la notificacin. Como solo tenemos un mtodo para ambas operaciones, el mtodo newEvent. Vamos a incluir el tipo de evento, si se aade o borra elemento, y dejaremos la notificacin del objeto aadido o borrado para ms adelante, y as podemos avanzar en babysteps y tenemos la oportunidad de ir refactorizando nuestro cdigo. La prueba que define lo que queremos obtener se muestra en el cdigo 7.10. Cdigo 7.10. Prueba que verifica el tipo del evento. @Test public void testWhenAnElementIsAddedSubsCritorReceives_Add_Event() { Subscriber sub = mock(Subscriber.class);
Queue<String> q = new Queue<String>(); q.subscribe(sub);
q.add("new string");
verify(sub).newEvent(Queue.Event.Add); } 91
En la prueba del cdigo 7.10 hemos decidido que cada evento se clasifica en base al enumerado Event adems, este enumerado se define dentro de la clase Queue. El mtodo newEvent recibe cmo parmetro el tipo de evento. En la prueba verificamos que se llama al mtodo newEvent del subscriptor y, adems, el primer parmetro es el evento Add. De nuevo tenemos que escribir cdigo en distintos lugares para implementar esta prueba. En primer lugar implementamos el cdigo del enumerado Event en el cdigo 7.11. Cdigo 7.11. Enumerado con los tipos de eventos.
public class Queue<T> { public static enum Event {Add}; // .. }
Despus, modificamos el mtodo add para pasar por parmetro el tipo de evento a cada subscriptor. En este caso utilizamos la implementacin obvia ya que el mtodo add siempre usar el mismo tipo de evento (cdigo 7.12). Cdigo 7.12. Enumerado con los tipos de eventos.
public void add(T e) { //... for (Subscriber sub: this.subs) { sub.newEvent(Event.Add); } }
Adems, al aadir un parmetro al mtodo newEvent es necesario modificar a interfaz subscribe de la siguiente manera. Esta modificacin est en el cdigo 7.13. Cdigo 7.13. Modificacin de la interface Subscriber.
public interface Subscriber { void newEvent(Event event); }
Cuando trabajamos con dobles de prueba podemos hacer evolucionar la interfaces que utilizamos para crear los dobles. Con ello vamos adaptando los servicios que simulan los dobles a nuestras necesidades. Al final, tendremos una interfaz que se ajustar con bastante precisin a lo que queremos. Observa tambin que, gracias a trabajar con dobles de prueba, un cambio en la interfaz no implica tener que cambiar ninguna clase, ya que los dobles de prueba generan la implementacin al vuelo, lo que hace ms gil la escritura de pruebas. 92
Con los cambios anteriores la nueva prueba funciona, pero la prueba anterior (llamada testWhenAnelementIsAddedTwoSubscriptorsReceivesACallInMethod_newEvent_) falla porque espera llamadas al mtodo newEvent sin parmetro y ahora se recibe un parmetro. Si nos fijamos en el cdigo de ambas pruebas vemos que es el mismo y que ambas pruebas verifican la misma condicin, esto es, la llamada correcta a newEvent. Por tanto podemos prescindir de la prueba que hemos creado y modificar la anterior. Los cambios se muestran en el cdigo 7.14. Cdigo 7.14.
Lo que ha pasado aqu es normal. Tenemos una prueba que verifica la llamada de un mtodo y ese mtodo cambia, por tanto la prueba tambin va a cambiando a la misma vez y no es necesario escribir pruebas nuevas. Esto aparecer de nuevo ms adelante cuando aadamos el elemento (aadido o borrado) al mtodo newEvent. Refactorizando las pruebas Recuerda que las pruebas son tan importantes como el cdigo de produccin y que tambin debemos refactorizarlas para que sean fciles de entender y modificar. Aunque slo tenemos una nica prueba, tenemos que modificarla a medida que vamos completando el mtodo newEvent, por lo que es importante que la prueba sea fcil de entender. En el cdigo 7.15 tienes toda la clase de pruebas tal y como est ahora mismo. Cdigo 7.15. public class TestSubscription {
@Test public void whenElementIsAdded2SubsReceivesACallIn_newEvent() { Subscriber sub01 = mock(Subscriber.class); Subscriber sub02 = mock(Subscriber.class); Queue<String> q = new Queue<String>(); q.subscribe(sub01); q.subscribe(sub02);
Si has ido siguiendo el resto de captulos de este libro vers fcilmente qu refactorizaciones podemos hacer. El resultado lo tienes en el cdigo 7.16. Cdigo 7.16.
Hemos refactorizado el cdigo que crea la cola y los dobles de prueba para tener menos cdigo en la prueba y que esta se centre en la accin y las verificaciones, sin tener que incluir detalles de cmo crean los objetos que participan (ahora se crean en el mtodo setUp). Adems hemos cambiado algunos nombres para que transmitan con ms claridad qu son, por ejemplo ahora el objeto cola se llama cola con dos subscriptores. An podramos hacer alguna refactorizacin ms al cdigo de prueba, pero la dejaremos para ms adelante. No vemos ningn mal olor lo suficientemente fuerte en la cola como para refactorizar por lo que seguimos adelante. Un nuevo husped El estado de nuestro diario de diseo es el siguiente (cuadro 3.4).
94
3.4. Diario de diseo despus de la primera prueba. Permitir a un objeto subscribirse a la cola (clase Queue). Enviar una notificacin cuando se aada un elemento. Permitir subscripciones de varios objetos Enviar la notificacin de aadir a todos los objetos subscritor Notificar el tipo de evento Notificar el elemento aadido o borrado Enviar una notificacin cuando se borre un elemento.
Vamos a completar el mtodo newEvent implementando la notificacin del elemento aadido. Como ya tenemos escrita una prueba que verifica la llanada a newEvent despus de aadir un elemento, no tiene sentido escribir una nueva prueba. En su lugar cambiamos la verificacin de la prueba que tenemos ahora mismo (cdigo 7.17). Cdigo 7.17. private void verifyNewElementIsCalled(Subscriber<String> sub) { verify(sub).newEvent(any(Queue.Event.class), any(String.class));
}
El cdigo 7.17 verifica que el mtodo newEvent ha sido llamado con cualquier evento y con una cadena de texto concreta, la misma cadena de texto que hemos aadido. Las modificaciones que tenemos que hacer para que la prueba pase con xito son muy similares a las modificaciones que hicimos para aadir el tipo de evento. Vamos a comenzar modificando la interfaz (cdigo 7.18). Cdigo 7.18. public interface Subscriber<T> { void newEvent(Event event, T element); }
Al aadir un parmetro genrico a la interfaz hemos de modificar cualquier referencia a la misma que tenemos. En la clase Queue hemos de modificar la declaracin del atributo, el constructor y el mtodo subscribe. Las nuevas implementaciones de estos tres elementos se muestran en el cdigo 7.19. Cdigo 7.19. List<Subscriber<T>> subs;
public void subscribe(Subscriber<T> sub) { this.subs.add(sub); }
95
public Queue() { //. subs = new ArrayList<Subscriber<T>>(); }
El nico cdigo nuevo que hay que aadir es modificar el mtodo add de la clase Queue para que incluya el elemento aadido a la llamada a los subscriptores (cdigo 7.20). Cdigo 7.20. public void add(T e) { //. for (Subscriber<T> sub: this.subs) { sub.newEvent(Event.Add, e); } }
Con el cambio anterior, la prueba ya funciona y ya hemos implementado la funcionalidad de nuestro diario de diseo. Con los cambios anteriores todas las pruebas funcionan correctamente. Veamos qu podemos refactorizar. En las pruebas hemos utilizado una cadena de texto concreta. Este caso, podemos cambiar dicha cadena por un atributo ya que no nos inters tanto el valor concreto de la cadena, ni el tipo de dato que almacena la cola, sino el papel que juega en la prueba. Cdigo 7.21. public class TestSubscription { String anElement = "new string"; //.
@Test public void testWhenAnelementIsAddedTwoSubscriptorsReceivesACallInMethod_newEvent_() {
Continuamos con lo que nos queda en el diario de diseo. 96
Hora de borrar El estado actual de nuestro diario de diseo se muestra en el cuadro 3.5. La ltima funcionalidad que nos queda por implementar es la notificacin del borrado de un elemento de la cola. 3.5. Diario de diseo despus de la primera prueba Permitir a un objeto subscribirse a la cola (clase Queue). Enviar una notificacin cuando se aada un elemento. Permitir subscripciones de varios objetos Enviar la notificacin de aadir a todos los objetos subscritor Notificar el tipo de evento Notificar el elemento aadido o borrado Enviar una notificacin cuando se borre un elemento y no cuando se llame al mtodo borrar peor nos e borre un ejemplo.
Vamos a dividir la ltima funcionalidad en dos, primero nos preocuparemos de la notificacin y, despus, comprobaremos que no se enve la notificacin si nos e ha borrado ningn elemento. Vamos a escribir una prueba muy similar a la prueba de add (tal y como qued en el cdigo 7.21), como ya refactorizamos las pruebas apenas hay que repetir cdigo (cdigo 7.22). Cdigo 7.22. @Test public void testWhenAnElementIsRemovedSubscriptorsReceivesThe_Remove_EventAndThatEleme nt() { queueWithTwoSubscribers.add(anElement); queueWithTwoSubscribers.drop();
Observa que, en esta prueba, estamos llamando al mtodo add, que genera una notificacin y al mtodo drop, que queremos que genere otra notificacin. No dara esta prueba un falso error al confundir la notificacin del mtodo add con la notificacin esperada de drop?. Por suerte Mockito es capaz de localizar lo que buscamos. Mockito almacena todas las llamadas que recibe un mtodo. En este caso la prueba verifica que alguna de las llamadas haya sido pasando como parmetro el evento event.Drop, cosa que no sucede porque an no hemos escrito el cdigo, por lo que la prueba falla. Cuando ejecutamos la prueba, Mockito busca la llamada que coincida con los parmetros indicados entre todas las llamadas a ese mtodo. Vamos a hacer que la prueba funcione. En este caso, el mtodo drop es prcticamente igual que el mtodo add por lo que usaremos una implementacin obvia. 97
Adems, hemos aadido el evento event.Drop al enumerado con los tipos de eventos. La implementacin la tienes en el cdigo 7.23. Cdigo 7.23. public static enum Event {Add, Drop};
public T drop() { T e = // Elemento borrado
for (Subscriber<T> sub: this.subs) { sub.newEvent(Event.Drop, e); }
// . }
Con el cdigo anterior la prueba funciona, pero hemos introducido, al menos, un mal olor. El mtodo drop es muy similar a add, lo cual es un aviso de que debemos refactorizar para evitar el cdigo duplicado. El resultado de esta refactorizacin se muestra en el cdigo 7.24. Cdigo 7.24. public void add(T e) { //. sendEvent(Event.Add, e); }
public T drop() { T e = //
sendEvent(Event.Drop, e); //. }
private void sendEvent(Queue.Event event, T value) { for (Subscriber<T> sub: this.subs) { sub.newEvent(event, value); } }
En el cdigo 7.24 hemos creado un nuevo mtodo privado que contiene el cdigo compartido por add y remove. Ya estamos a punto de terminar, pero an queda un ltimo detalle que trataremos en la siguiente seccin. Solo cuando de verdad se borre La funcionalidad a implementar es lanzar el evento de borrado de elementos solo cuando se ha borrado el elemento, pero si la cola est vaca y se intenta borrar, qu 98
sucede? Este detalle depende de la implementacin de la cola, pero podemos documentarlo en una prueba. Vamos a escribir una prueba que ejecute este escenario y comprobemos qu sucede (cdigo 7.25). Cdigo 7.25. @Test public void whenThereAraNotElementsToRemove_SubscriptorsDontReceiveNotification() { queueWithTwoSubscribers.drop();
En el cdigo 7.25 utilizamos Mockito para verificar que un mtodo no ha sido llamado nunca Al ejecutar la prueba comprobamos que la clase Queue lanza una excepcin si se intenta borrar un elemento y la cola est vaca, por lo que ya podemos dar por concluido este ejercicio. Borramos la prueba del cdigo 7.25, ya que ni aporta ni documenta nada, dado que el comportamiento de la cola es distinta del especificado en la prueba y la cola nos e comporta como la prueba indica. Tampoco tiene sentido modificar la prueba para verificar que se lanza la excepcin porque esa prueba debe estar en el conjunto de pruebas de la cola, no en el conjunto de pruebas relacionadas con los suscritores y las notificaciones. Tachamos la ltima lnea de nuestro diario de diseo y ya hemos terminado. Conclusiones En este captulo hemos visto un ejemplo de uso de un doble de prueba como mock (el objeto que Mockito crea implementando la interfaz Subscriptor). Este mock ha sido utilizado como espa con el que hemos verificar que el comportamiento de la cola era el esperado y se llamaba al mtodo correspondiente con los parmetros vlidos. El doble de prueba nos ha servido tambin para posponer decisiones de diseo e ir incorporando detalles poco a poco. Como hemos visto en el captulo, hemos modificado la interfaz pero no hemos necesitado modificar su implementacin ya que Mockito la crea para nosotros al ejecutar las pruebas. En otros captulos este libro encontrars otros de los ejemplos de uso de los dobles de prueba que se describen en el anexo y seguiremos trabajando con Mockito.
99
Anexo 1. Test de pruebas
Este test te ayuda a aprender conceptos bsicos de la prueba del software y es una continuacin del reto de testing publicado en BetaBeers. Si despus de hacer este test quieres aprender ms, pasa por este enlace y resuelve el reto de testing: http://betabeers.com/test/testing-16/ Preguntas 1. Por cules de los siguientes motivos elegiras utilizar algn tipo de double? a) Porque estamos haciendo pruebas unitarias b) Porque tenemos una dependencia de una clase que an no existe. c) Porque tenemos una dependencia de una clase u es muy lenta. d) Todas las anteriores.
2. Qu pruebas debern ejecutarse en primer lugar? a) Las pruebas que busquen los errores ms frecuentes b) Las pruebas que busquen errores con un mayor impacto econmico c) Las pruebas indicadas por los usuarios finales d) Las prueba sindicadas en el plan de pruebas
100
3. Cuntas particiones son necesarias para probar un mtodo mediante la tcnica de particiones equivalentes? a) Tantas particiones como parmetros de entrada tenga un mtodo. b) Tantas particiones como parmetros de entrada y atributos de la clase que le afecten c) Tantas particiones como posibles valores de entrada d) Tantas particiones como posibles salidas tenga el mtodo.
4. Actualmente, una de las aplicaciones ms habituales de la computacin en la nube aplicada a la prueba del software es: a) Ejecutar las pruebas unitarias en la nube para evitar pausas al escribir cdigo. b) Contratar recursos para simular mltiples usuarios sobre el sistema bajo prueba. c) Realizar pruebas A/B d) Almacenar toda la informacin sobre la ejecucin de pruebas
5. Cundo debemos probar interacciones en lugar de estados en una prueba unitaria? a) Siempre que podamos b) Nunca c) Cuando el cdigo bajo prueba no genere ningn resultado verificable d) Cuando el mtodo a probar no devuelva nada
6. El contenido ms habitual de un plan de pruebas es. a) Elementos bajo prueba, criterios de aceptacin, entornos de pruebas, responsables y planificacin b) Elementos bajo prueba, criterios de aceptacin, entornos de pruebas, responsables, planificacin, resultados de las pruebas c) Iteracin, historias de usuario a probar, definicin de hecho, entornos de pruebas, responsables, planificacin d) Elementos bajo prueba, criterios de aceptacin y entornos de pruebas
101
7. Un conjunto de pruebas unitarias consume mucho tiempo en su ejecucin. Cul de las siguientes acciones elegira para aumentar la velocidad? a) Ejecutar dichas pruebas en un servidor dedicado, por ejemplo un servidor de integracin continua b) Intentar sustituir algn mdulo por un doubl / mock /stub c) Comprar un ordenador ms potente d) Quitar las pruebas que menos importancia tengan
8. Un compaero de trabajo te hace llegar un fragmento de cdigo fuente indicndote que falla. Qu haras? a) Preguntar cul es el resultado esperado despus de la ejecucin de dicho cdigo b) Ejecutar el cdigo paso a paso con un depurador c) Escribir varias pruebas unitarias para localizar el error d) Revisar cuidadosamente el cdigo en busca del error
9. Es recomendable introducir estructuras de control como if o for en las pruebas unitarias? a) S porque as se pueden escribir pruebas ms potentes y verstiles que ahorren tiempo b) No porque dichas estructuras hacen la prueba ms difcil de entender. c) S porque con ellas se pueden verificar mejor los resultados de una prueba, con lo que la eficiencia de la prueba aumenta. d) No, porque una prueba debe ser simple ya que no escribimos pruebas para probar pruebas
102
10. Cules seran los valores lmite para probar el siguiente fragmento de cdigo? method(int a, int b) { int c = 0;
if ( a > 5) { c++; } if ( b > 6> { c++: } }
a) Para a sera -1, 0 y 1 y los mismos para b b) Para a sera MAX_INTEGER, - MAX_INTEGER y 0 y los mismos para b c) Para a sera 5 y 6 y para b 6 y 7 d) Para a sera 4, 5 y 6 y para b 5, 6 y 7
103
Respuestas Pregunta Respuesta Pregunta Respuesta 1 D 6 A 2 D 7 B 3 D 8 A 4 B 9 D 5 C 10 c
Agradecimientos del captulo A Miquel Camps por su estupenda iniciativa BetaBeers y por incluir el reto de testing que le propusimos y que podis acceder en este enlace: http://betabeers.com/test/testing-16/. A Antonio Martnez por sus estupendos y valiosos comentarios sobre el reto de testing, los cules nos han animado a inclus este anexo.
104
Anexo 2. Dobles de Prueba y Mockito
Este anexo explica el papel que juegan los dobles de prueba a la hora de escribir pruebas y los tipos de dobles de prueba ms habituales. El anexo incluye tambin ejemplos de Mockito, una librera multilenguaje que permite crear dobles de prueba de manera dinmica. Con estos ejemplos no tendrs ningn problema en comprender los captulos de este libro que utilizan dobles de prueba y Mockito. Dobles de prueba Vamos a justificar la utilidad de los dobles de prueba un ejemplo. Supongamos que queremos probar el cdigo A2.1. Cdigo A2.1. Mtodo bajo prueba
Cul es el resultado esperado del mtodo descuentoPorPedido? Observa que este mtodo no devuelve ningn valor, sino que lo que se espera que haga es llamar al 105
mtodo de contar de pedido con el descuento adecuado. Adems, el mtodo necesita un objeto pedido, un objeto user un objeto descuento y un objeto database o no podremos ejecutar ninguna prueba del mtodo descuentoPorPedido. No es prctico que no podamos probar este mtodo hasta que no hayamos escrito todos los objetos necesarios y que, para escribir una prueba, tengamos que establecer un nmero de pedidos concreto en la base de datos accedida por el objeto database. Los dobles de prueba nos van ayudar en este caso, tanto para verificar que se ha llamado al parmetro correcto como para reemplazar otros objetos que el cdigo bajo prueba necesite. Un doble de prueba (test double en ingls) es una implementacin falsa de una interfaz. Esta implementacin falsa se utiliza durante la ejecucin de pruebas para reemplazar la implementacin verdadera. Hay varios escenarios en los que podemos sacarle partido a los dobles de prueba, algunos ya los hemos visto en el ejemplo anterior. Veamos algunos: Con los dobles de prueba podemos reemplazar cdigo que accede a sistemas o servidores externos, por ejemplo bases de datos, API Rest, redes sociales, etc. En este caso, las pruebas se ejecuten de manera mucho ms rpida al simular un acceso, por ejemplo, a un servidor de bases de datos en vez de tener que realizar el acceso real. Otro uso es poder controlar el contexto de la prueba. Podemos configurar el doble de prueba para que devuelva determinados datos o para que lance un error como respuesta y as probar el comportamiento de nuestro cdigo. Un tercer uso de los dobles de prueba es cuando necesitamos probar interacciones, como en el ejemplo que hemos visto al principio. En determinados casos, el comportamiento de un cdigo puede ser no generar un valor o cambiar los atributos de una clase, sino realizar una o varias llamadas a los mtodos de otra clase. Gracias a los dobles de prueba podemos verificar que dichas llamadas se han ejecutado en el orden correcto y con los parmetros adecuados. Un cuarto uso de los dobles de prueba es representar cdigo que an no ha sido escrito. Por ejemplo en una clase que contengan toda la lgica de conexin a un servidor externo, acceso a su informacin y desconexin. En este caso, el uso de dobles de prueba puede ayudarnos a definir la interfaz con precisin y a determinar si estamos desarrollando una interfaz cmoda y sencilla de usar antes de entrar en los detalles de implementacin. Todos estos usos de los dobles de prueba han dado lugar a distintos tipos. Vamos a ver algunos de estos tipos con ms detalle a continuacin. 106
Tipos de dobles de prueba Desde hace aproximadamente 5 o 6 aos, se ha empezado a estandarizar una clasificacin de tipos de dobles de prueba con el objetivo de que todos los que probamos y escribimos cdigo de prueba tengamos unos conceptos similares y podamos entendernos ms fcilmente Podemos clasificar un doble de prueba en funcin de su complejidad y de la misin que puede desempear. Lo tipos ms comunes de dobles de prueba son: Dummy: Objeto que necesita el cdigo bajo prueba, pero que nunca ser utilizado durante la prueba. Stub: Objetos que devuelven valores predefinidos al llamar a sus mtodos. Fake: Objeto que implementa un atajo y no es vlido para produccin. Mock: Objeto programado con las llamadas a mtodos esperadas. Shim: Este tipo de test doubl est definido en la herramienta Microsoft Fakes y permite sustituir a referencias incrustadas en cdigo ya escrito sin necesidad de modificar dicho cdigo. Es posible desarrollar lo mismo en Java con la herramienta PowerMocks y otras similares. Veamos un ejemplo para aclarar estos conceptos. Queremos simular el cdigo que accede a una base de datos, bien para aumentar la velocidad de muestras pruebas, o para independizarnos del servidor de bases de datos, bien porque el cdigo an no est escrito, o bien por todas estas razones a la vez. Si utilizramos un stub para simular este cdigo, tendramos un objeto que siempre devolver valores prefijados. Si utilizramos un fake tendramos un objeto que simulara el comportamiento de una base de datos, por ejemplo utilizando un map para guardar los valores, usando como clave del map un campo definido como clave primaria. Incluso podra pasar que nos diramos cuenta de que el map es justo lo que necesitamos y aadiramos un mecanismo para persistirlo. En este caso el fake pasara a ser un componente de prueba a cdigo de solucin. Algo similar sucedi durante el desarrollo de FitNesse, como se comenta al final de este anexo. Si utilizramos un mock, en cambio, tendramos un objeto al que programaramos con la secuencia de los mtodos que esperamos (por ejemplo, conectar, realizar una consulta, desconectar) para verificar si el cdigo est utilizando correctamente el acceso a la base de datos. Si utilizramos un dummy, estaramos indicando que el cdigo que vamos a probar no necesita realizar ninguna operacin con la base de datos. Por ltimo, si utilizramos un Shim, estaramos instrumentando un cdigo ya escrito y que no podemos modificar para interceptar las llamadas a mtodos que acceden a la 107
base de datos y redirigirlas a nuestro doble de prueba, por ejemplo, para que se comporte como un stub. Veamos otro ejemplo. Vamos retomar el ejemplo con el que abramos este anexo y vamos a identificar qu tipo de doble de prueba sera ms adecuado para cada una de las dependencias del mtodo que queremos probar. Por comodidad dicho ejemplo se muestra de nuevo en el cdigo A2.2. Cdigo A2.2. Mtodo bajo prueba
Vimos al principio de este apndice as dependencias del mtodo descuentoPorPedido. Veamos ahora, en la tabla A2.1 cul podra ser el tipo de doble de prueba que mejor encajara en cada una de ellas. Tabla A2.3. Ejemplos de dobles de prueba para descuentoPorPedido. Referencia Tipo de mock User Dummy porque el mtodo no lo utiliza. Pedido Mock para poder verificar que se ha realizado la llama al mtodo descontar Database Stub porque devolvera el nmero de pedidos necesario para que el mtodo entre en el if Descuentos Stub porque se nica misin es devolver un descuento concreto
Felicidades. En este punto no solo sabes qu es un doble de prueba y los tipos de dobles ms comunes sino que tambin eres capaz de decidir cul es el ms adecuado en cada situacin y ahrrate tiempo y trabajo desarrollando cosas que no necesitas. En las prximas secciones vamos a ver cmo poner todo esto en prctica. Antes de concluir este anexo quiero hacerte una advertencia. Como vimos al principio de esta seccin, la terminologa utilizada para dobles de prueba se estableci hace 5 aos aproximadamente. Textos ms antiguos o textos a an no hayan adoptado estos trminos pueden utilizar nombres distintos o mezclar algunos de los nombres que hemos visto. Pero no te preocupes, ahora que sabes los nombres y su significado podrs entender fcilmente cualquier texto sobre dobles de prueba aunque usen los trminos de manera distinta. 108
Una breve introduccin a Mockito Siempre que trabajamos con dobles de prueba solemos trabajar con referencias a interfaces, as podemos cambiar el objeto real por el doble y viceversa fcilmente. Si no, podemos emplear herencia y redefinir los mtodos, pero debemos evitar utilizar esta opcin siempre que podamos porque pone de manifiesto un diseo demasiado acoplado. No es necesario ninguna librera para trabajas con dobles de prueba. Podemos escribir nosotros mismos las implementaciones ficticias de estas interfaces para programar nuestros propios dobles de prueba. Esta solucin es vlida pero requiere mucho trabajo y muy repetitivo, a pesar de las opciones de los IDEs que nos permiten escribir implementaciones vacas para los mtodos de una interfaz. Si implementamos los dobles de prueba a mano hemos de implementar y mantener cdigo para cada interfaz, hemos de implementar valores devueltos en aquellos mtodos que devuelvan algo, hemos de implementar comprobantes para verificar que los parmetros son los esperados en los mtodos que tengan parmetros. Todo el trabajo anterior es muy repetitivo y, por tanto automatizable. En la actualidad existen varias herramientas que hacen todo este trabajo por nosotros. En Java, existen Mockito, JMock, EasyMock o PowerMocks entre otras. En este libro trabajaremos slo con Mockito aunque los ejercicios que planteamos se podran resolver con cualquier otra herramienta. Mockito es una librera Java que permite crear automticamente varios tipos de dobles de prueba (principalmente mocks y stubs) a partir de interfaces. Permite indicar el comportamiento de los mtodos de la interfaz cuando sean invocados y tambin evaluar qu mtodos han sido llamados y con qu parmetros. En lugar de describir el API de Mockito, vamos a ver ejemplos de cmo trabajar con Mockito. Varios de estos ejemplos los hemos usado en algunos captulos de este libro, por lo que te ayudarn a entender las pruebas que se han escrito. Ejemplos con Mockito Todos los ejemplos de esta seccin se basan en un doble de prueba de la interfaz java.util.List creado por Mockito y todos se ejecutan correctamente. Utilizaremos esta herramienta tanto para crear stubs que devuelvan valores predeterminados, como para crear mocks que verifiquen llamadas concretas a mtodos. Dejamos al lector que identifique a qu tipo corresponde cada ejemplo. En el primer ejemplo (cdigo A2.3), creamos un doble que implementa la interfaz List y verificamos (con la sentencia verify de Mockito) que se ha llamado al mtodo add con el parmetro a. Si no llamamos al mtodo add o lo llamamos con un parmetro distinto a "a" la prueba fallar. 109
Cdigo A2.3. @Test public void testMethodAddIsExceutedWithRightParameter() { List<String> l = mock(List.class);
l.add("a");
verify(l).add("a"); }
En el segundo ejemplo, cdigo A2.4, vemos que el doble de prueba va almacenando las llamadas y los parmetros recibidos y podemos validarlos en cualquier orden. Cdigo A2.4. @Test public void testMethodAddIsExceutedSeveralTimes() { List<String> l = mock(List.class);
En el ejemplo del cdigo A2.5, definimos el comportamiento del mtodo get para que espere el parmetro 0 y devuelva la cadena a. Esto suceder siempre independientemente del nmero de llamadas, tal y como se muestra en el bucle for. Cdigo A2.5. @Test public void testMethodGetReturns_A_EveryTimeIsCalled() { List<String> l = mock(List.class);
when(l.get(0)).thenReturn("a");
for (int i = 1; i < 100; i++) assertEquals("a", l.get(0)); }
En el ejemplo del cdigo A2.6, comprobamos si se ha llamado al mtodo contains. El primer verify comprueba que la llamada se ha realizado con un parmetro cualquiera y el segundo comprueba que la llamada se ha realizado con un parmetro de 110
tipo String. Esto nos da ms flexibilidad que el tener que indicar el parmetro concreto como en los ejemplos anteriores. Cdigo A2.6. @Test public void testRemoveHasBeenCalled() { List<String> l = mock(List.class);
En el ejemplo del cdigo A2.7, comprobamos que el mtodo get se ha llamado con un parmetro de tipo entero. Cdigo A2.7. @Test public void testIndexOf_IsCalledWithAnyInteger() { List<String> l = mock(List.class);
l.get(3);
verify(l).get(anyInt()); }
En el ejemplo del cdigo A2.8 verificamos que el mtodo clear del doble de prueba no ha sido llamado nunca. Cdigo A2.8. @Test public void testClearMethodWasNotCalled() { List<String> l = mock(List.class);
verify(l, never()).clear(); }
En el ejemplo A2.9 indicamos que, cuando mtodo get reciba el parmetro 0 devuelva la cadena A, y cuando reciba el parmetro 1 devuelva la cadena B. Al igual que en el ejemplo del cdigo A2.5, podemos llamar a get(0) y get(1) tantas veces como queramos.
111
Cdigo A2.9.
@Test public void testGetIndex_0_Returns_A_AndIndex_1_Returns_B_{ List<String> l = mock(List.class);
El ejemplo del cdigo A2.10 configuramos el doble de prueba para que la primera llamada al mtodo get con el parmetro 0 devuelva la cadena A y las posteriores llamadas devuelvan la cadena B. Cdigo A2.10. @Test public void testGetIndex_0_Returns_A_AndThenReturns_B_() { List<String> l = mock(List.class);
El ejemplo del cdigo A2.11 verifica que el mtodo get ha sido llamado una y solo una vez. Si el mtodo no es llamado en ninguna ocasin o es llamado ms de una vez, el verify fallar. Cdigo A2.11. @Test public void testGetIsCalledOnlyOnce() { List<String> l = mock(List.class);
l.get(0);
verify(l, only()).get(anyInt()); }
Con los ejemplos que acabamos de ver, podrs resolver y entender todos los ejemplos de este libro. Pero hay tambin muchos ms ejemplos y posibilidades para utilizar Mockito, no dejes de consultar su documentacin. 112
Errores comunes trabajando con dobles de pruebas Todo es bueno con moderacin (o es dicen). Los dobles de prueba tambin. Hay algunos errores habituales cuando se trabaja con dobles de prueba. Vamos a comentarlos brevemente a continuacin. El primer error es probar los propios dobles de prueba. Observa el cdigo A2.11. Eres capaz de descubrir el fallo? Cdigo A2.11. Ejemplo
Si escribes un caso de prueba en el que verificas un resultado devuelto por un doble de prueba (en vez de verificar el cdigo bajo prueba o que los mtodos fueron llamados), est probando el propio doble de prueba, no tu cdigo real. Los ejemplos que has visto en la seccin anterior te ensean a utilizar Mockito pero nunca debes escribir pruebas as en tu cdigo o estars probando los dobles de prueba. Otro error es escribir dobles de prueba demasiado complejos y detallados. Cuando ms detallado es un doble de prueba, ms rgido es y un pequeo cambio puede invalidar muchas pruebas y ms difcil es entender su comportamiento. Un tercer error es utilizar demasiados dobles de prueba. Si necesitas ms de 3 o 4 mocks para una nica prueba es posible que tengas un mal diseo y que merezca la pena revisar cmo est organizado tu cdigo (como en el ejemplo del cdigo A2.3). Adems, tus pruebas sern ms difciles de entender y ms frgiles. Utilizando dobles de prueba verificas el comportamiento del sistema cuando el cdigo se comporta exactamente igual que el doble de prueba. Si el cdigo exhibe un comportamiento inesperado o cambia el comportamiento pero no se actualizan los dobles de prueba las pruebas no te darn la confianza adecuada en que tu sistema funciona correctamente. Ahora que sabes usar dobles de prueba salos para el bien, no para dispararte en un pie. Alternativas a los dobles de prueba Como hemos visto al principio de este anexo un uso muy habitual de los dobles de prueba es reemplazar cdigo que accede a servidores y servicios externos. As, por 113
ejemplo, en vez lanzar una consulta a una base de datos, reemplazamos esa clase por un stub que devuelve un conjunto de datos predefinidos. Aunque ganamos en velocidad y control de la prueba, nos impide probar el cdigo que accede a la base de datos. Adems, una interaccin compleja con un sistema puede llevarnos a necesitar un doble de prueba muy complejo y frgil. Una alternativa para evitar utilizar dobles de prueba son los servidores en memoria. Por ejemplo, en el caso de un servidor de BBDD podramos tener nuestro propio servidor en memoria que se ejecute y se inicialice con los datos necesarios antes de cada prueba. Si la ejecucin e inicializacin son lo suficientemente rpidas y no requiere cdigo complejo, es una herramienta vlida escribir pruebas que trabajaran con l. Algunas referencias a herramientas que podemos utilizar se citan en los siguientes prrafos. Al final de este anexo se incluye una tabla con los enlaces a todas las herramientas. Para bases de datos podemos utilizar bases de datos en memoria, como Hipersonic SQL (ms conocida por HSQL) o Apache Derby. En este caso, ser necesario lanzar el servidor, crear la base de datos, y llenarla con los datos necesarios para la prueba. Para facilitar esta tarea podemos utilizar herramientas adicionales como DbUnit. Tambin existe un proyecto que adopta la manera de trabajar de esta herramienta para utilizar repositorios de datos no SQL (como Casandra o MongoDB) llamada NoSQLUnit. Para trabajar con correos electrnicos podemos utilizar un servidor de e-mail embebido, es decir, que forma parte de las libreras de prueba y que podemos encender y apagar como parte de las pruebas. Una herramienta que nos permite utilizar un servidor de correo electrnico de una manera similar a lo que hemos visto en el caso anterior para base de datos es Dumbster y Wiser (incluido en SubEtha SMTP). Ademas, estas libreras nos permiten crear rpidamente servidores que acepta correos y los almacenan de manera que pueden ser consultados desde el cdigo de las pruebas. Para trabajar con APIs Rest tenemos varias opciones, entre ellas WireMock, la cual nos permite crear stubs y mocks de APIs Rest, indicando las llamadas esperadas a la API y los datos a devolver en cada llamada. Tambin existen proyectos que se ejecutan en entornos especiales que proporcionan una serie de funcionalidades concretas. Si ejecutamos las pruebas fuera de esos entornos, no tenemos disponible es funcionalidad y el cdigo puede fallar. Por ejemplo, en el caso de Android, existen herramientas como Roboelectric que permite emular todas las APIs que Android pone a nuestra disposicin sin tener que ejecutar las pruebas ni en un emulador ni en un dispositivo android, con la ganancia de velocidad que ello supone. En el caso de los contenedores de aplicaciones, como J2EE y Glashfish, contamos con la herramienta Arquillian que nos permite emular el propio contenedor. 114
Para terminar Hemos visto en este anexo todo lo necesario para comprender qu es un doble de prueba y qu papel juega, y para empezar a utilizar la librera Mockito en nuestras pruebas. Si an no lo has hecho, puedes buscar los captulos de este libro en los que se resuelven katas que involucran dobles de prueba para ver todo lo anterior en accin. Bola Extra. FitNesse FitNesse es un buen ejemplo de cmo utilizar dobles de prueba nos lleva a descubrir que nuestros proyectos pueden funcionar muy bien con soluciones ms sencillas. FitNesse es un servidor web que proporciona un wiki que permite escribir especificaciones que, mediante el cdigo adecuado, se pueden ejecutar de manera automtica sobre el sistema en desarrollo. En su artculo Professionalism and Test-Driven Development 1 , Robert C. Martin, uno de los principales desarrolladores de FitNesse cuenta que, al principio del desarrollo, detectaron que necesitaban una base de datos, pero no tenan informacin suficiente para decantarse por una base de datos concreta y no queran tomar esa decisin tan pronto. Para evitarlo crearon un conjunto de mocks con las funciones de acceso a la base de datos. Ms adelante, a medida que el desarrollo de FitNesse continuaba, tuvieron la necesidad de probar funcionalidad que no podan implementar con los mocks. Para ello crearon un fake consistente en una base de datos en memoria a la que accedan a travs de la misma interfaz que utilizaban para interactuar con los mocks por lo que no hubo necesidad de cambiar nada de lo que ya tenan hecho. Ms adelante necesitaron una autntico mecanismo de persistencia, y para ello crearon un archivo (flat-file database en el original) que utilizaron como almacn de datos. Descubrieron que este fichero era suficiente para sus necesidades y no llegaron a utilizar una base de datos real. Por ltimo, Robert Martin comenta que conoci a personas que necesitaron una autntica base de datos SQL y que pudieron integrarla con FitNesse en un solo da de trabajo. Referencias Dos referencias muy tiles para aprender ms sobre dobles de prueba son el libro de Carlos Bl titulado diseo gil con TDD (http://www.dirigidoportests.com/el-libro) y la traduccin al espaol del libro Testing Unitaro con Microsoft Fakes (https://vsartesttoolingguide.codeplex.com/downloads/get/720407). En el primero vas
a encontrar un captulo entero dedicado a los dobles de prueba y un ejemplo, desarrollado en varios captulos, en el que se utilizan para desarrollar u problema no trivial. En el segundo, como su propio nombre indica, Adems, la tabla A2.2 recoge enlaces a las herramientas mencionadas en este anexo. Tabla A2.4. En laces a las herramientas mencionadas en este apndice. Herramienta Enlace Apache Derby http://db.apache.org/derby/ Arquillian http://arquillian.org/ DBUnit http://dbunit.sourceforge.net/ Dumbster http://quintanasoft.com/dumbster/ EasyMock http://easymock.org/ FitNesse http://fitnesse.org/ Hipersonic SQL (HSQL) http://hsqldb.org/ JMock http://jmock.org/ Mockito http://code.google.com/p/mockito/ NoSQLUnit https://github.com/lordofthejars/nosql-unit PowerMock http://code.google.com/p/powermock/ Roboelectric http://robolectric.org/ SubETha SMTP (y Wiser) http://code.google.com/p/subethasmtp WireMock http://wiremock.org/ Agradecimientos del anexo A Carlos Bl y Juan Mara Lao por sus estupendos libros y traducciones que citamos un poco ms arriba como fuentes de conocimiento para aprender ms de dobles de prueba. A lex Soto por la estupenda charla sobre Mocking en la Codemotion 2.013 y que me ha ayudado muchsimo para recopilar herramientas con las que evitar utilizar mocks (enlace: http://es.slideshare.net/asotobu/resistance-is-futile-mocks-will-be- assimilated).