You are on page 1of 309

Diseo Agil con TDD n

Carlos Bl Jurado y colaboradores. e Prologo de Jos Manuel Beas e

Primera Edicin, Enero de 2010 o www.iExpertos.com El libro se ha publicado bajo la Licencia Creative Commons

Indice general

Base Terica o
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

31
33 35 37 38 40 44 46 47 49 53 56 56 57 58 60 60 60 61 63 64 68 70 71

1. El Agilismo 1.1. Modelo en cascada . . . . . . . . . . . . . . . . . 1.2. Hablemos de cifras . . . . . . . . . . . . . . . . . 1.3. El maniesto gil . . . . . . . . . . . . . . . . . . a 1.4. En qu consiste el agilismo?: Un enfoque prctico e a 1.5. La situacin actual . . . . . . . . . . . . . . . . . o 1.6. Agil parece, pltano es . . . . . . . . . . . . . . . a 1.7. Los roles dentro del equipo . . . . . . . . . . . . . 1.8. Por qu nos cuesta comenzar a ser giles? . . . . e a

2. Qu es el Desarrollo Dirigido por Tests? (TDD) e 2.1. El algoritmo TDD . . . . . . . . . . . . . . . . . . . . . . . 2.1.1. Escribir la especicacin primero . . . . . . . . . . . o 2.1.2. Implementar el cdigo que hace funcionar el ejemplo o 2.1.3. Refactorizar . . . . . . . . . . . . . . . . . . . . . . 2.2. Consideraciones y recomendaciones . . . . . . . . . . . . . . 2.2.1. Ventajas del desarrollador experto frente al junior . . 2.2.2. TDD con una tecnolog desconocida . . . . . . . . a 2.2.3. TDD en medio de un proyecto . . . . . . . . . . . . 3. Desarrollo Dirigido por Tests 3.1. Las historias de usuario . 3.2. Qu y no Cmo . . . . . e o 3.3. Est hecho o no? . . . . a 3.4. El contexto es esencial . de . . . . . . . . Aceptacin o . . . . . . . . . . . . . . . . . . . . . . . . . . . . (ATDD) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

INDICE GENERAL 4. Tipos de test y su importancia 4.1. Terminolog en la comunidad TDD a 4.1.1. Tests de Aceptacin . . . . . o 4.1.2. Tests Funcionales . . . . . . 4.1.3. Tests de Sistema . . . . . . 4.1.4. Tests Unitarios . . . . . . . 4.1.5. Tests de Integracin . . . . . o

INDICE GENERAL 73 74 75 76 76 78 80 83 84

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

5. Tests unitarios y frameworks xUnit 5.1. Las tres partes del test: AAA . . . . . . . . . . . . . . . . .

6. Mocks y otros dobles de prueba 95 6.1. Cundo usar un objeto real, un stub o un mock . . . . . . . 97 a 6.2. La metfora Record/Replay . . . . . . . . . . . . . . . . . . 108 a 7. Diseo Orientado a Objetos n 7.1. Diseo Orientado a Objetos (OOD) . . . . . n 7.2. Principios S.O.L.I.D . . . . . . . . . . . . . . 7.2.1. Single Responsibility Principle (SRP) . 7.2.2. Open-Closed Principle (OCP) . . . . . 7.2.3. Liskov Substitution Principle (LSP) . 7.2.4. Interface Segregation Principle (ISP) . 7.2.5. Dependency Inversin Principle (DIP) o 7.3. Inversin del Control (IoC) . . . . . . . . . . o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 111 112 113 113 114 114 115 116

II

Ejercicios Prcticos a

119
121 157 . . . . . . . . . . . . . . . . 231 233 243 246 247 249 289

8. Inicio del proyecto - Test Unitarios 9. Continuacin del proyecto - Test Unitarios o 10.Fin del proyecto - Test de Integracin o 10.1. La frontera entre tests unitarios y tests de integracin o 10.2. Diseo emergente con un ORM . . . . . . . . . . . . n 10.2.1. Diseando relaciones entre modelos . . . . . n 10.3. La unicacin de las piezas del sistema . . . . . . . . o 11.La solucin en versin Python o o 12.Antipatrones y Errores comunes

INDICE GENERAL

INDICE GENERAL 297 297 299 299 300 302 302 303 304 304 305

A. Integracin Continua (CI) o A.1. Introduccin . . . . . . . . . . . . . . . . . . . . . . o A.2. Prcticas de integracin continua . . . . . . . . . . . a o A.2.1. Automatizar la construccin . . . . . . . . . o A.2.2. Los test forman parte de la construccin . . . o A.2.3. Subir los cambios de manera frecuente . . . . A.2.4. Construir en una mquina de integracin . . . a o A.2.5. Todo el mundo puede ver lo que est pasando a A.2.6. Automatizar el despliegue . . . . . . . . . . . A.3. IC para reducir riesgos . . . . . . . . . . . . . . . . . A.4. Conclusin . . . . . . . . . . . . . . . . . . . . . . . o

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

INDICE GENERAL

INDICE GENERAL

A la memoria de nuestro querido gatito Lito, que vivi con total atencin y o o entrega cada instante de su vida

Prlogo o

Erase una vez que se era, un lejano pa donde viv dos cerditos, Pablo s an y Adrin que, adems, eran hermanos. Ambos eran los cerditos ms listos a a a de la granja y, por eso, el gallo Ivn (el gerente de la misma) organiz una a o reunin en el establo, donde les encarg desarrollar un programa de ordenador o o para controlar el almacn de piensos. Les explic qu quer saber en todo e o e a momento: cuntos sacos de grano hab y quin met y sacaba sacos de a a e a grano del almacn. Para ello slo ten un mes pero les advirti que, en e o an o una semana, quer ya ver algo funcionando. Al nal de esa primera semana, a eliminar a uno de los dos. a Adrin, que era el ms joven e impulsivo, inmediatamente se puso manos a a a la obra. No hay tiempo que perder!, dec Y empez rpidamente a a. o a escribir l neas y l neas de cdigo. Algunas eran de un reciente programa que o hab ayudado a escribir para la guarder de la vaca Paca. Adrin pens que a a a o no eran muy diferentes un almacn de grano y una guarder En el primero e a. se guardan sacos y en el segundo, pequeos animalitos. De acuerdo, ten n a que retocar algunas cosillas para que aquello le sirviera pero bueno, esto del software va de reutilizar lo que ya funciona, no? Pablo, sin embargo, antes de escribir una sola l nea de cdigo comenz acoro o dando con Ivn dos cosas: qu era exactamente lo que podr ver dentro de a e a una semana y cmo sabr que, efectivamente, estaba terminada cada cosa. o a Ivn quer conocer, tan rpido como fuera posible, cuntos sacos de grano a a a a hab en cada parte del almacn porque sospechaba que, en algunas partes del a e mismo, se estaban acumulando sacos sin control y se estaban estropeando. Como los sacos entraban y sal constantemente, no pod saber cuntos an a a hab y dnde estaban en cada instante, as que acordaron ir contabilizndoa o a los por zonas y apuntando a qu parte iba o de qu parte ven cada vez e e a, que entrara o saliera un saco. As en poco tiempo podr tener una idea , an clara del uso que se estaba dando a las distintas zonas del almacn. e 9

Prlogo o Mientras Adrin adelantaba a Pablo escribiendo muchas l a neas de cdigo, o Pablo escrib primero las pruebas automatizadas. A Adrin eso le parec a a a una prdida de tiempo. Slo ten una semana para convencer a Ivn! e o an a Al nal de la primera semana, la demo de Adrin fue espectacular, ten a a un control de usuarios muy completo, hizo la demostracin desde un mvil y o o enseo, adems, las posibilidades de un generador de informes muy potente n a que hab desarrollado para otra granja anteriormente. Durante la demostraa cin hubo dos o tres problemillas y tuvo que arrancar de nuevo el programa o pero, salvo eso, todo fue genial. La demostracin de Pablo fue mucho ms o a modesta, pero cumpli con las expectativas de Ivn y el programa no fall en o a o ningn momento. Claro, todo lo que enseo lo hab probado much u n a simas veces antes gracias a que hab automatizado las pruebas. Pablo hac TDD, a a es decir, nunca escrib una l a nea de cdigo sin antes tener una prueba que le o indicara un error. Adrin no pod creer que Pablo hubiera gastado ms de la a a a mitad de su tiempo en aquellas pruebas que no hac ms que retrasarle a an a la hora de escribir las funcionalidades que hab pedido Ivn. El programa de a a Adrin ten muchos botones y much a a simas opciones, probablemente muchas ms de las que jams ser necesarias para lo que hab pedido Ivn, pero a a an a a ten un aspecto muy profesional. a Ivn no supo qu hacer. La propuesta de Pablo era muy robusta y hac a e a justo lo que hab acordado. La propuesta de Adrin ten cosillas que pulir, an a a pero era muy prometedora. Hab hecho la demostracin desde un mvil! a o o As que les propuso el siguiente trato: Os pagar un 50 % ms de lo que e a inicialmente hab amos presupuestado, pero slo a aquel de los dos que me o haga el mejor proyecto. Al otro no le dar nada.. Era una oferta complicada e porque, por un lado, el que ganaba se llevaba mucho ms de lo previsto. Muy a tentador. Pero, por el otro lado, corr el riesgo de trabajar durante un mes an completamente gratis. Mmmmm. Adrin, tan impulsivo y arrogante como siempre, no dud ni un instante. a o Trato hecho!, dijo. Pablo explic que aceptar slo si Ivn se comproo a o a met a colaborar como lo hab hecho durante la primera semana. A Ivn le a a a pareci razonable y les convoc a ambos para que le ensearan el resultado o o n nal en tres semanas. Adrin se march pitando y llam a su primo Sixto, que sab mucho a o o a y le asegurar la victoria, aunque tuviera que darle parte de las ganancias. a Ambos se pusieron rpidamente manos a la obra. Mientras Adrin arreglaba a a los defectillos encontrados durante la demo, Sixto se encarg de disear una o n arquitectura que permitiera enviar mensajes desde el mvil hasta un websero vice que permit encolar cualquier operacin para ser procesada en paralelo a o por varios servidores y as garantizar que el sistema estar en disposicin de a o dar servicio 24 horas al d los 7 d de la semana. a as 10

Prlogo o Mientras tanto, Pablo se reuni con Ivn y Bernardo (el encargado del o a almacn) para ver cules deber ser las siguientes funcionalidades a desarroe a an llar. Les pidi que le explicaran, para cada peticin, qu benecio obten la o o e a granja con cada nueva funcionalidad. Y as poco a poco, fueron elaborando , una lista de funcionalidades priorizadas y resumidas en una serie de tarjetas. A continuacin, Pablo fue, tarjeta a tarjeta, discutiendo con Ivn y Bernardo o a cunto tiempo podr tardar en terminarlas. De paso, aprovech para anotar a a o algunos criterios que luego servir para considerar que esa funcionalidad an estar completamente terminada y eliminar alguna ambigedad que fuera a u surgiendo. Cuando Pablo pens que, por su experiencia, no podr hacer ms o a a trabajo que el que ya hab discutido, dio por concluida la reunin y se an o dispuso a trabajar. Antes que nada, resolvi un par de defectos que hab o an surgido durante la demostracin y le pidi a Ivn que lo validara. A contio o a nuacin, se march a casa a descansar. Al d siguiente, cogi la primera de o o a o las tarjetas y, como ya hab hecho durante la semana anterior, comenz a a o automatizar los criterios de aceptacin acordados con Ivn y Bernardo. Y lueo a go, fue escribiendo la parte del programa que hac que se cumplieran esos a criterios de aceptacin. Pablo le hab pedido ayuda a su amigo Hudson, un o a coyote vegetariano que hab venido desde Amrica a pasar el invierno. Huda e son no sab programar, pero era muy rpido haciendo cosas sencillas. Pablo a a le encarg que comprobara constantemente los criterios de aceptacin que o o l hab automatizado. As cada vez que Pablo hac algn cambio en su e a , a u programa, avisaba a Hudson y este hac una tras otra, todas las pruebas de a, aceptacin que Pablo iba escribiendo. Y cada vez hab ms. Este Hudson o a a era realmente veloz e incansable! A medida que iba pasando el tiempo, Adrin y Sixto ten cada vez a an ms problemas. Terminaron culpando a todo el mundo. A Ivn, porque no a a les hab explicado detalles important a simos para el xito del proyecto. A la e vaca Paca, porque hab incluido una serie de cambios en el programa de la a guarder que hac que no pudieran reutilizar casi nada. A los inventores de a a los SMS y los webservices, porque no ten ni idea de cmo funciona una an o granja. Eran tantos los frentes que ten abiertos que tuvieron que prescindir an del env de SMS y buscaron un generador de pginas web que les permitiera o a dibujar el ujo de navegacin en un grco y, a partir de ah generar el o a , esqueleto de la aplicacin. Eso seguro que les ahorrar mucho tiempo! Al o a poco, Sixto, harto de ver que Adrin no valoraba sus aportaciones y que ya a no se iban a usar sus ideas para enviar y recibir los SMS, decidi que se o marchaba, an renunciando a su parte de los benecios. Total, l ya no cre u e a que fueran a ser capaces de ganar la competicin. o Mientras tanto, Pablo le pidi un par de veces a Ivn y a Bernardo que le o a validaran si lo que llevaba hecho hasta aquel momento era de su agrado y les 11

Prlogo o hizo un par de demostraciones durante aquellas 3 semanas, lo que sirvi para o corregir algunos defectos y cambiar algunas prioridades. Ivn y Bernardo a estaban francamente contentos con el trabajo de Pablo. Sin embargo, entre ellos comentaron ms de una vez: Qu estar haciendo Adrin? Cmo lo a e a a o llevar?. a Cuando se acercaba la fecha nal para entregar el programa, Adrin se a qued sin dormir un par de noches para as poder entregar su programa. o Pero eran tantos los defectos que hab ido acumulando que, cada vez que a arreglaba una cosa, le fallaba otra. De hecho, cuando lleg la hora de la o demostracin, Adrin slo pudo ensear el programa instalado en su porttil o a o n a (el unico sitio donde, a duras penas, funcionaba) y fue todo un desastre: mensajes de error por todos sitios, comportamientos inesperados... y lo peor de todo: el programa no hac lo que hab acordado con Ivn. a an a Pablo, sin embargo, no tuvo ningn problema en ensear lo que llevaba u n funcionando desde hac mucho tiempo y que tantas veces hab probado. Por a a si acaso, dos d antes de la entrega, Pablo hab dejado de introducir nuevas as a caracter sticas al programa porque quer centrarse en dar un buen manual de a usuario, que Ivn hab olvidado mencionar en las primeras reuniones porque a a daba por sentado que se lo entregar Claro, Adrin no hab tenido tiempo an. a a para nada de eso. Moraleja: Adems de toda una serie de buenas prcticas y un proceso de desarrollo a a gil, Pablo hizo algo que Adrin despreci: acord con Ivn (el cliente) y con a a o o a Bernardo (el usuario) los criterios mediante los cules se comprobar que a a cada una de las funcionalidades estar bien acabada. A eso que solemos llaa mar criterios de aceptacin, Pablo le aadi la posibilidad de automatizar o n o su ejecucin e incorporarlos en un proceso de integracin continua (que es o o lo que representa su amigo Hudson en este cuento). De esta manera, Pablo estaba siempre tranquilo de que no estaba estropeando nada viejo con cada nueva modicacin. Al evitar volver a trabajar sobre asuntos ya acabados, o Pablo era ms eciente. En el corto plazo, las diferencias entre ambos ena foques no parecen signicativas, pero en el medio y largo plazo, es evidente que escribir las pruebas antes de desarrollar la solucin es mucho ms ecaz o a y eciente. En este libro que ahora tienes entre tus manos, y despus de este inusual e prlogo, te invito a leer cmo Carlos explica bien clarito cmo guiar el desao o o rrollo de software mediante la tcnica de escribir antes las pruebas (ms e a conocido como TDD).

12

Agradecimientos

Una vez o a un maestro zen decir que la gratitud que expresa una per sona denota su estado momentneo de bienestar consigo misma. Estoy muy a contento de ver que este proyecto que se empez hace casi ao y medio, ha o n concluido con un resultado tan graticante. Tengo que agradecer cosas a miles de personas pero para no extenderme demasiado, lo har hacia los que han tenido una relacin ms directa con el e o a libro. En primer lugar tengo que agradecer a Dcil Casanova que haya sido a la responsable de calidad nmero uno en el proyecto. Sin ella este libro no u se hubiera escrito de esta forma y en este momento. Quizs no se hubiese a escrito nunca. No slo tengo que agradecer todo lo much o simo que me aporta en lo personal sino que adems me anim constantemente a no publicar el a o libro hasta que estuviera hecho lo mejor posible. Ha revisado mi redaccin o corrigiendo mil faltas de ortograf y signos de puntuacin. a o El toque de calidad que dan los dems coautores al libro, es tambin un a e detallazo. Adems de escribir texto han sido buenos revisores. Estoy profuna damente agradecido a Juan Gutirrez Plaza por haber escrito el cap e tulo 11 y por haber hecho correcciones desde la primera revisin del libro hasta la o ultima. Ha sido un placer discutir juntos tantos detalles tcnicos. Gracias a e Fran Reyes Perdomo tenemos un gran apndice sobre Integracin Cont e o nua que de no haber sido por l no estar en el libro, con lo importante que es e a esta prctica. Mil gracias Fran. Gregorio Mena es el responsable de que el a cap tulo 1 no sea un completo desastre. Ha sido para m el ms dif de a cil escribir de todo el libro y a ningn revisor le acababa de convencer. Gregorio u ha refactorizado medio cap tulo dndole un toque mucho ms serio. Espero a a que sigamos trabajando juntos Gregorio :-) Para terminar con los coautores quiero agradecer a Jos Manuel Beas que haya escrito el prlogo del libro a e o pesar de lo muy ocupado que est liderando la comunidad gil espaola y a a n

13

Agradecimientos haciendo de padre de familia. Un bonito detalle JM ;-D A continuacin quiero agradecer a la decena de personas que han le o do las revisiones del libro previas a la publicacin y que han aportado correccioo nes, palabras de nimo e incluso texto. Agradecimientos especiales a Esteban a Manchado Velzquez que tiene much a simo que ver con la calidad de la redaccin y que ha sido uno de los revisores ms constantes. Yeray Darias adems o a a de revisar, escribi el cap o tulo que le ped sobre DDD, aunque nalmente queda para un libro futuro. Mi buen amigo Eladio Lpez de Luis ha dejado o algn comentario en cada revisin que he ido enviando. Alberto Rodr u o guez Lozano ha sido una de las personas ms inuyentes en las correcciones del a cap tulo 9, aportando comentarios tcnicos de calidad. No puedo olvidar al e resto de revisores que han seguido el libro en distintas etapas aportando tambin comentarios muy brillantes: Nstor Bethencourt, Juan Jorge Prez e e e Lpez, Pablo Rodr o guez Snchez, Jos Ramn D Jose M. Rodr a e o az, guez de la Rosa, V ctor Roldn y Nstor Salceda. Tambin agradezco a todas las a e e personas que han le alguna de estas revisiones previas y me han enviado do emails personales de agradecimiento. Hadi Hariri me inuenci mucho para que partiese el libro en dos y dejase o para ste, solo aquellos temas de relacin directa con TDD. e o Ahora, ms all de los que han tenido relacin directa con el libro, quiero a a o agradecer a Rodrigo Trujillo, ex director de la Ocina de Software Libre de la Universidad de La Laguna (ULL), que me diera la oportunidad de dirigir un proyecto y carta blanca para aplicar TDD desde el primer d porque a, durante el ao que trabaj ah aprend toneladas de cosas. Rodrigo es una de n e las personas que ms se mueve en la ULL; no deja de intentar abrir puertas a a los estudiantes que estn terminado ni de preocuparse por la calidad de su a universidad. Gracias tambin a Jos Lu Roda, Pedro Gonzlez Yanes y Jess Alberto e e s a u Gonzlez por abrirme las puertas de la facultad de informtica y tambin por a a e permitirme que impartiese mi primer curso completo de TDD. De la facultad tengo tambin que agradecer a Marcos Colebrook que me diese el empujn e o que necesitaba para terminar la carrera, que la ten casi abandonada. Sin a Marcos y Goyo, el cambio de plan hubiera hecho que abandonase la idea de terminar la carrera. A Esteban Abeledo y al resto del Colegio de Informaticos de Canarias les agradezco mucho hayan homologado nuestros cursos de TDD. Los alumnos de mis cursos de TDD tienen mucho que ver con el resultado nal del libro ya que he volcado en l muchas de sus dudas y comentarios e t picos. Gracias a los dos grupos que he tenido en Tenerife y al de Sevilla, todos en 2009. Mencin especial a las personas que ayudaron a que saliesen o adelante: Alvaro Lobato y los ya citados Gregorio, Pedro y Roda. 14

Agradecimientos Gracias a todos los que estn promoviendo XP en Espaa. A saber: Ala n fredo Casado, Xavier Gost, Leo Antol Agust Yage, Eric Mignot, Jorge , n u Jimmez, Ivn Prraga, Jorge Uriarte, Jess Prez, David Esmerodes, Luismi e a a u e Cavall, David Calavera, Ricardo Roldn y tantos otros profesionales de la e a comunida Agile Spain, entre los cuales estn los coautores y revisores de este a libro. Y tambin a los que estn promoviendo XP en Latinoamrica: Fabin e a e a Figueredo, Carlos Peix, Angel Lpez, Carlos Lone y tantos otros. Y al pibe o que vos viste nos rrre-ayud a traer el Agile Open a Espaa, Xavier Quesada o n ;-) Alfredo Casado adems ha escrito la sinopsis del libro. a Quiero saludar a todas las personas con las que he trabajado en mi paso por las muchas empresas en que he estado. De todos he aprendido algo. Gracias a los que me han apoyado y gracias tambin a los que me han querido e tapar, porque de todos he aprendido cosas. Ha sido tremendamente enriquecedor cambiar de empresa y de proyectos. Thank you all guys at Buy4Now in Dublin during my year over there in 2007. Tambin he aprendido mucho e de todos aquellos desarrolladores con los que he trabajado en proyectos open source, en mi tiempo libre. A quienes han conado en m para impartir cursos de materias diversas, porque han sido claves para que desarrolle mi capacidad docente; Agust n Benito, Academia ESETEC, Armando Fumero, Innova7 y Rodrigo Trujillo. Agradecimientos tambin a Pedro Gracia Fajardo, una de las mentes e ms brillantes que he conocido. Un visionario. Pedro fue quin me habl por a e o primera vez de XP en 2004. En aquel entonces nos cre amos que ramos giles e a aunque en realidad lo estbamos haciendo fatal. La experiencia sirvi para a o que yo continuase investigando sobre el tema. Gracias a la comunidad TDD internacional que se presenta en un grupo de discusin de Yahoo. Aunque ellos no leern estas l o a neas por ser en espaol, n quiero dejar claro que sin su ayuda no hubiese sabido resolver tantos problemas tcnicos. Gracias a Kent Beck y Lasse Koskela por sus libros, que han e sido para m fuente de inspiracin. o Aprovecho para felicitar a Roberto Canales por su libro, Informtica a Profesional[13]. Es una pena que me haya puesto a leerlo cuando ya ten a escrito este libro, a pocos d de terminar el proyecto. Si lo hubiese le en as do octubre, cuando Leo me lo regal, me hubiese ahorrado bastantes prrafos o a de la parte terica. Es un pedazo de libro que recomiendo a todo el mundo. o Gracias Roberto por brindarnos un libro tan brillante. Un libro, un amigo. Gracias a Angel Medinilla, Juan Palacio, Xavi Albaladejo y Rodrigo Corral, por entrenar a tantos equipos giles en nuestro pa Angel adems me a s. a regala muy buenos consejos en mi nueva etapa en iExpertos. Por ultimo no quiero dejar de decirle a mi familia que sin ellos esto 15

Agradecimientos no ser posible. A Dcil de nuevo por todo lo much a a simo que me aporta diariamente. A mis padres por traerme al mundo y ensearme tantas cosas. n A mis hermanas por querer tanto a su hermano mayor. A mis primos. A mi t Tina por acogerme en su casa durante mis aos de universidad y a n darme la oportunidad de estudiar una carrera, ya que mis padres no pod an costearmelo. Ella me ayuda siempre que lo necesito. A mis abuelos por estar siempre ah como mis segundos padres. ,

16

Autores del libro

Carlos Bl Jurado e

Nac en Crdoba en 1981, hijo de cordobeses o pero cuando ten 4 aos emigramos a Lanzaa n rote y, salvo algunos aos intercalados en los n que viv en Crdoba, la mayor parte de mi vida o la he pasado en Canarias. Primero en Lanzarote y despus en Tenerife. e Mi primer apellido signica trigo en francs. Lo e trajo un francs al pueblo cordobs de La Vice e toria en tiempos de Carlos III. Cuando ten 6 aos mi padre trajo a casa un a n 8086 y aquello me fascin tanto que supe que o me quer dedicar a trabajar con ordenadores a desde entonces. He tenido tambin Amiga 500, e Amiga 1200, y luego unos cuantos PC, desde el AMD K6 hasta el Intel Core Duo de hoy.

Soy ingeniero tcnico en informtica de sistemas. Para m el t e a tulo no es ninguna garant de profesionalidad, ms bien hago un balance negativo a a de mi paso por la Universidad, pero quer ponerlo aqu para que los que a padecen de titulitis vean que el que escribe es titulado. La primera vez que gan dinero con un desarrollo de software fue en e 2001. Poco despus de que instalase Debian GNU/Linux en mi PC. En 2003 e entr de lleno en el mercado laboral. Al principio trabaj administrando sise e temas adems de programar. a En 2005 se me ocurri la genial idea de montar una empresa de desarrollo o de software a medida con mis colegas. Sin tener suciente experiencia como desarrollador y ninguna experiencia como empresario ni comercial, fue toda 17

Autores del libro una aventura sobrevivir durante los dos aos que permanec en la empresa. n Fueron dos aos equivalentes a cuatro o cinco trabajando en otro sitio. Ver n el negocio del software desde todos los puntos de vista, me brind la oporo tunidad de darme cuenta de muchas cosas y valorar cuestionas que antes no valoraba. Aprend que no pod tratar al cliente como si fuera un tester. a No pod limitarme a probar las aplicaciones 10 minutos a mano antes de a entregarlas y pensar que estaban hechas. Aprend que la venta era ms deci a siva que el propio software. Tambin aprend cosas feas como que Hacienda e no somos todos, que los concursos pblicos se amaan, que el notario da u n ms miedo que el dentista, que el pez grande se come al pequeo y muchas a n otras. Al nal me d cuenta de que la odisea me sobrepasaba y no era capaz de llevar la empresa a una posicin de estabilidad donde por f dejase de o n amanecerme sentado frente a la pantalla. Cansado de los morosos y de que la administracin pblica nos tuviera sin cobrar meses y meses, mientras estaba o u con el agua al cuello, en 2007 me mand a mudar a Irlanda para trabajar e como desarrollador. Aprend lo importante que era tener un equipo de QA1 y me volqu con los tests automatizados. Llegu a vivir el boom de los sueldos e e en Dublin, cobrando 5000 euros mensuales y sin hacer ni una sola hora extra. En 2008 regres a Tenerife. e En la actualidad he vuelto a emprender. Desarrollo software profesionalmente de manera vocacional. Mi esp ritu emprendedor me lleva a poner en marcha nuevas ideas en la nube. Adems me dedico a formar a desarrolladores a impartiendo cursos sobre TDD, cdigo limpio, metodolog y herramientas o a de programacin. En lugar de intentar trabajar en mi empresa, trabajo para o la empresa2 , cuya web es www.iExpertos.com Habitualmente escribo en www.carlosble.com y en el blog de iExpertos. He escrito la mayor parte del libro con la excepcin de los fragmentos o que nos han regalado los dems autores. a

1 2

Quality Assurance El matiz viene del libro, El Mito del Emprendedor, de Michael E. Gerber

18

Autores del libro

Jose Manuel Beas

En 2008 decidi aprovechar sus circunstancias o personales para tomarse un respiro, mirar atrs, a a los lados y, sobre todo, hacia adelante. Y as , aprovech ese tiempo para mejorar su formao cin en temas como Scrum asistiendo al curo so de Angel Medinilla. Pero tambin quiso poe ner su pequeo granito de arena en el desarron llo y la difusin de Concordion, una herramieno ta de cdigo abierto para realizar pruebas de o aceptacin, y rellenar su caja de herramientas o con cosas como Groovy y Grails. Pero sobre todo vi que merec la pena poner parte de su o a energ en revitalizar una vieja iniciativa llamaa da Agile Spain y buscar a todos aquellos que, como l, estuvieran buscando maneras mejores de e hacer software. Y vaya que si los encontr. Aco tualmente es el Presidente de la asociacin Agio le Spain, que representa a la comunidad agilista en Espaa y organizadora de los Agile Open n Spain y las Conferencias Agile Spain. Tambin e participa en la elaboracin de los podcasts de o Podgramando.es, una iniciativa de agilismo.es powered by iExpertos.com. Puedes localizarlo fcilmente a travs del portal agilismo.es, en a e la lista de correo de Agile Spain o en su blog personal http://jmbeas.iexpertos.com.

19

Autores del libro Juan Plaza Gutirrez e Escribir, he escrito poco, pero aprender, much simo. He intentado hacer algo con sentido con mi oxidado Python en el cap tulo 11 aunque ms que escribir, ha sido discutir y rea discutir con Carlos sobre cual era la mejor forma de hacer esto y aquello (siempre ha ganado l). e Tambin he sacado del bal de los recuerdos mi e u LaTeX y he revisado mucho. Cuando no estoy discutiendo con la gente de Agile Spain, trabajo como Agile Coach en F-Secure donde intento ayudar a equipos de Finlandia, Malasia, Rusia y Francia en su transicin a las metodolog o as a giles (tanto en gestin como en prcticas de o a software entre las que se incluye, por supuesto, TDD). Pero cmo he llegado hasta aqu o ? Mi devocin los ordenadores me llev a estuo o diar la carrera de ingenier en informtica en a a la UPM de Madrid. Trabaj en Espaa por un e n tiempo antes de decidir mudarme a Finlandia en el 2004 para trabajar en Nokia. En las largas y oscuras tardes del invierno Finlands e estudi un master en industrial management e antes de cambiar a F-Secure. Cuando el tiempo lo permite, escribo en http://agilizar.es Soy un apasionado desarrollador de software interesado en prcticas giles. Llevo cerca de a a 4 aos trabajando para la r n gida Administracin pblica con un fantastico equipo. Conoc a o u Carlos Bl en un provechoso curso de TDD que e imparti para un grupo de compaeros. Entre o n cervezas (una fase importante para asimilar lo aprendido), compartimos ideas y experiencias del mundo del software, y me habl adems o a del proyecto en el que se encontraba embarcado, en el cual me brind la oportunidad de o participar con un pequeo apndice sobre inten e gracin continua. Una prctica, que intentamos, o a forme parte del d a d en nuestros proyectos. a a http://es.linkedin.com/in/franreyesperdomo

Fran Reyes Perdomo

20

Autores del libro

Gregorio Mena

Mi corta vida profesional ha sido suciente para dar sentido a la frase de Horacio Ningn hombre ha llegado a la u excelencia en arte o profesin alguna, o sin haber pasado por el lento y doloroso proceso de estudio y preparacin. Auno que en mi caso el camino no es doloroso, sino apasionante. Siguiendo esta losof intento formarme y fomentar la a, formacin, por lo que he organizado un o curso de TDD impartido con gran acierto por Carlos Ble y voy a participar en futuras ediciones. Trabajo desde iExpertos para que entre todos hagamos posible el primer curso de Scrum en Canarias, pues tambin colaboro con la platae forma ScrumManager. Ha sido esta forma de ver nuestra profesin la que me o llev a colaborar con Carlos en este lio bro. Pensaba aportar algo, pero lo cierto es que lo que haya podido aportar no tiene comparacin con lo que he tenido o la suerte de recibir. Por ello debo dar a Carlos las gracias por ofrecerme esta oportunidad y por el esfuerzo que ha hecho para que este libro sea una realidad para todos los que vemos en la formacin o continua el camino al xito. e Habitualmente escribo en http://eclijava.blogspot.com.

21

Autores del libro

22

Convenciones y Estructura

Este libro contiene numerosos bloques de cdigo fuente en varios leno guajes de programacin. Para hacerlos ms legibles se incluyen dentro de o a rectngulos que los separan del resto del texto. Cuando se citan elementos a de dichos bloques o sentencias del lenguaje, se usa este tipo de letra. A lo largo del texto aparecen referencias a sitios web en el pie de pgina. a Todas ellas aparecen recopiladas en la pgina web del libro. a Las referencias bibliogrcas tienen un formato como este:[3]. Las ultimas a pginas del libro contienen la bibliograf detallada correspondiente a todas a a estas referencias. Otros libros de divulgacin repiten determinadas ideas a lo largo de varios o cap tulos con el n de reforzar los conceptos. En la era de la infoxicacin3 o tal repeticin sobra. He intentado minimizar las repeticiones de conceptos o para que el libro se pueda revisar rpidamente, por lo que es recomendable a una segunda lectura del mismo para quienes desean profundizar en la materia. Ser como ver una pel a cula dos veces: en el segundo pase uno aprecia muchos detalles que en el primero no vio y su impresin sobre el contenido puede o variar. En realidad, es que soy tan mal escritor que, si no lee el libro dos veces no lo va a entender. Hgase a la idea de que tiene 600 pginas en lugar a a de 300. Sobre los paquetes de cdigo fuente que acompaan a este libro (listo o n para descarga en la web), el escrito en C# es un proyecto de Microsoft Visual Studio 2008 (Express Edition) y el escrito en Python es una carpeta de cheros.

http://es.wiktionary.org/wiki/infoxicaci %C3 %B3n

23

Convenciones y Estructura

24

La Libertad del Conocimiento

El conocimiento ha sido transmitido de individuo a individuo desde el comienzo mismo de la vida en el planeta. Gracias a la libre circulacin del o conocimiento hemos llegado a donde estamos hoy, no slo en lo referente al o avance cient co y tecnolgico, sino en todos los campos del saber. o Afortunadamente, el conocimiento no es propiedad de nadie sino que es como la energ simplemente uye, se transforma y nos transforma. De a; hecho no pertenece a las personas, no tiene dueo, es de todos los seres. n Observando a los animales podemos ver cmo los padres ensean a su prole o n las tcnicas que necesitarn para desenvolverse en la vida. e a El conocimiento contenido en este libro no pertenece al autor. No pertenece a nadie en concreto. La intencin es hacerlo llegar a toda persona que o desee aprovecharlo. En Internet existe mucha documentacin sobre la libertad del conocimieno to, empezando por la entrada de la Wikipedia4 . Este argumento es uno de los principales pilares del software libre5 , al que tanto tengo que agradecer. Las principales herramientas que utilizaremos en este libro estn basadas en softa ware libre: frameworks xUnit, sistemas de control de versiones y frameworks para desarrollo de software. Personalmente me he convertido en usuario de la tecnolog Microsoft .Net gracias al framework Mono6 , desarrollado por a Novell con licencia libre. De no haber sido por Mono probablemente no hubiera conocido C#.
A El libro ha sido maquetado usando LTEX, concretamente con teTex y MikTeX(software libre) y ha requerido de multitud de paquetes libres desarro-

http://es.wikipedia.org/wiki/Conocimiento libre http://es.wikipedia.org/wiki/Cdigo libre o 6 http://www.mono-project.com


5

25

La Libertad del Conocimiento llados por la comunidad. Para la edicin del texto se ha usado Texmaker, o Dokuwiki, Vim y Emacs. El versionado de los cheros de texto se ha llevado a cabo con Subversion. Los diagramas de clases los ha generado SharpDevelop, con el cual tambin he editado cdigo. Estoy muy agradecido a todos e o los que han escrito todas estas piezas. En la web del libro se encuentra el esqueleto con el que se ha maquetado para que, quien quiera, lo use para sus propias publicaciones. Pese a mi simpat por el software de fuente abierta, este libro va ms all de a a a la dicotom software libre/software propietario y se centra en tcnicas aplia e cables a cualquier lenguaje de programacin en cualquier entorno. Uno de los o peores enemigos del software libre es el fanatismo radical de algunos de sus evangelizadores, que raya en la mala educacin y empaa el buen hacer de o n los verdaderos profesionales. Es mi deseo aclarar que mi posicin personal no o es ni a favor ni en contra del software propietario, simplemente me mantengo al margen de esa contienda.

26

La Web del Libro

Los enlaces a sitios web de Internet permanecen menos tiempo activos en la red que en las pginas de un libro impreso; la lista de referencias se a mantendr actualizada en un sitio web dedicado al libro: a http://www.dirigidoportests.com Si el lector desea participar informando de enlaces rotos, podr hacerlo diria gindose a la web del libro o bien mediante correo electrnico al autor: e o carlos[Arroba]iExpertos[Punto]com El cdigo fuente que acompaa al libro se podr descargar en la misma o n a web. Si bien es cierto que escribir el libro ha sido un placer, tambin es cierto e que ha sido duro en ocasiones. Ha supuesto casi ao y medio de trabajo n y, dado que el libro es gratis y ni siquiera su venta en formato papel se traducir en algo de benecio, en la web es posible hacer donaciones. Si el a libro le gusta y le ayuda, le invito a que haga una donacin para que en el o futuro puedan haber ms libros libres como este. a

27

La Web del Libro

28

Cul es el Objetivo del Libro? a

El objetivo fundamental del libro es traer a nuestro idioma, el espaol, n conocimiento tcnico que lleva aos circulando por pa de habla inglesa. e n ses No cabe duda de que los que inventaron la computacin y el software nos o siguen llevando ventaja en cuanto a conocimiento se reere. En mi opinin, o es una cuestin cultural en su mayor parte pero, sea como fuere, no podeo mos perder la oportunidad de subirnos al carro de las nuevas tcnicas de e desarrollo y difundir el conocimiento proporcionado por nuestros compaeros n angloparlantes. Slo as competiremos en igualdad de condiciones, porque a o d de hoy cada vez ms clientes apuestan por el outsourcing. Conocimiento a a es poder. A d de hoy, por suerte o por desgracia, no nos queda ms remedio a a que dominar el ingls, al menos el ingls tcnico escrito, y es conveniente e e e leer mucho en ese idioma. Se aprende much simo porque no slo lo usan o los nativos de habla inglesa sino que se ha convertido en el idioma universal en cuanto a tecnolog Sin embargo, reconozco que yo mismo hace unos a. aos era muy reacio a leer textos en ingls. He tenido que hacer un gran n e esfuerzo y leer mucho con el diccionario en la mano hasta llegar al punto en que no me cuesta trabajo leer en ingls (adems de vivir una temporada e a fuera de Espaa). Conozco a pocos compaeros que hagan este esfuerzo. Es n n comprensible y normal que la mayor se limite a leer lo que est documentado a a en espaol. Al n y al cabo es de los idiomas ms hablados del planeta. Por n a eso concluyo que hay que traer la informacin a nuestro idioma para llegar o a ms gente, aunque el buen profesional deber tener presente las mltiples a a u ventajas que le aportar el dominio del ingls escrito. Cuando no dominamos a e el ingls nos perdemos muchos matices que son signicativos7 . e Apenas hay libros sobre agilismo en espaol. Los unicos libros novedosos n que se editan en nuestra lengua relacionados con el mundo del software,
7

http://jmbeas.iexpertos.com/hablar-ingles-es-facil-si-sabes-como/

29

Cul es el objetivo del libro? a tratan sobre tecnolog muy espec as cas que hoy valen y maana quizs no. n a Est muy bien que haya libros sobre Java, sobre .Net o sobre Ruby, pero no a tenemos que limitarnos a ello. El unico libro sobre agilismo que hay a d de a hoy es el de Scrum, de Juan Palacio[15]. Por otro lado, Angel Medinilla ha traducido el libro de Henrik Kniberg[8], Scrum y XP desde las Trincheras y Leo Antol ha traducido The Scrum Primer. Estos regalos son de agradecer. Ahora que existen editoriales en la red tales como Lulu.com, ya no hay excusa para no publicar contenidos tcnicos. Personalmente me daba reparo e afrontar este proyecto sin saber si alguna editorial querr publicarlo pero a ya no es necesario que las editoriales consideren el producto comercialmente vlido para lanzarlo a todos los pblicos. Otro objetivo del libro es animar a a u los muchos talentos hispanoparlantes que gustan de compartir con los dems, a a que nos deleiten con su conocimiento y sus dotes docentes. Quin se anima e con un libro sobre Programacin Extrema?. o No me cabe duda de que las ideas planteadas en este libro pueden resultarles controvertidas y desaantes a algunas personas. El lector no tiene por qu coincidir conmigo en todo lo que se expone; tan slo le invito a que e o explore con una mente abierta las tcnicas aqu recogidas, que las ponga en e prctica y despus decida si le resultan o no valiosas. a e

30

Parte I

Base Terica o

31

Captulo

El Agilismo
Para denir qu es el agilismo, probablemente basten un par de l e neas. Ya veremos ms adelante, en este mismo cap a tulo, que el concepto es realmente simple y queda plasmado en cuatro postulados muy sencillos. Pero creo que llegar a comprenderlo requiere un poco ms de esfuerzo y, seguramente, la a mejor manera sea haciendo un repaso a la historia del desarrollo de software, para al nal ver como el agilismo no es ms que una respuesta lgica a los a o problemas que la evolucin social y tecnolgica han ido planteando. o o Ya desde el punto de partida es necesario hacer una reexin. Al otear la o historia nos damos cuenta de que el origen del desarrollo de software est a a unas pocas dcadas de nosotros. Para llegar al momento en el que el primer e computador que almacenaba programas digitalmente corri exitosamente su o primer programa, slo tenemos que remontarnos al verano de 19481 . Esto nos o hace reexionar sobre el hecho de que nos encontramos ante una disciplina que es apenas una recin nacida frente a otras centenarias con una base de e conocimiento slida y estable. Por nuestra propia naturaleza nos oponemos o al cambio, pero debemos entender que casi no ha transcurrido tiempo como para que exijamos estabilidad. Siguiendo la ley de Moore2 , los componentes hardware acaban duplicando su capacidad cada ao. Con lo que, en muy poco tiempo, aparecen mquin a nas muy potentes capaces de procesar miles de millones de operaciones en segundos. A la vez, los computadores van reduciendo su tamao consideran blemente, se reducen los costes de produccin del hardware y avanzan las o comunicaciones entre los sistemas. Todo esto tiene una consecuencia evidente: los computadores ya no slo se encuentran en mbitos muy restringidos, o a como el militar o el cient co.
1 2

http://en.wikipedia.org/wiki/Tom Kilburn http://en.wikipedia.org/wiki/Moore %27s Law

33

Cap tulo 1 Al extenderse el mbito de aplicacin del hardware (ordenadores persoa o nales, juegos, relojes, ...), se ofrecen soluciones a sistemas cada vez ms a complejos y se plantean nuevas necesidades a una velocidad vertiginosa que implican a los desarrolladores de Software. Sin informacin y conocimieno to suciente, unos pocos aventureros empiezan a desarrollar las primeras aplicaciones que dan respuesta a las nuevas necesidades pero es un reto muy complejo que no llega a ser resuelto con la inmediatez y la precisin necesao rias. Los proyectos no llegan a buen puerto, o lo hacen muy tarde. En la dcada de los cincuenta nos encontramos con otro hito impore tante. En el mbito militar, surge la necesidad de profesionalizar la gestin a o de proyectos para poder abordar el desarrollo de complejos sistemas que requer coordinar el trabajo conjunto de equipos y disciplinas diferentes en la an construccin de sistemas unicos. Posteriormente, la industria del automvil o o sigui estos pasos. Esta nueva disciplina se basa en la planicacin, ejecucin o o o y seguimiento a travs de procesos sistemticos y repetibles. e a Hasta este punto, hemos hablado slo de desarrollo de software y no de o ingenier de software, ya que es en 1968 cuando se acua este trmino en a n e la NATO Software Engineering Conference3 .En esta conferencia tambin se e acua el trmino crisis del software para denir los problemas que estaban n e surgiendo en el desarrollo y que hemos comentado anteriormente. Los esfuerzos realizados producen tres reas de conocimiento que se rea velaron como estratgicas para hacer frente a la crisis del software4 : e Ingenier del software: este trmino fue acuado para denir la nea e n cesidad de una disciplina cient ca que, como ocurre en otras reas, a permita aplicar un enfoque sistemtico, disciplinado y cuanticable al a desarrollo, operacin y mantenimiento del software. o Gestin Predictiva de proyectos: es una disciplina formal de gestin, o o basada en la planicacin, ejecucin y seguimiento a travs de procesos o o e sistemticos y repetibles. a Produccin basada en procesos: se crean modelos de procesos basao dos en el principio de Pareto5 , empleado con buenos resultados en la produccin industrial. Dicho principio nos indica que la calidad del o resultado depende bsicamente de la calidad de los procesos. a En este punto, con el breve recorrido hecho, podemos sacar conclusiones reveladoras que luego nos llevarn a la mejor comprensin del agilismo. Por a o un lado, la gestin predictiva de proyectos establece como criterios de xito o e
3

http://en.wikipedia.org/wiki/Software engineering http://www.navegapolis.net/les/Flexibilidad con Scrum.pdf 5 http://es.wikipedia.org/wiki/Principio de Pareto


4

34

Cap tulo 1

1.1. Modelo en cascada

obtener el producto denido en el tiempo previsto y con el coste estimado. Para ello, se asume que el proyecto se desarrolla en un entorno estable y predecible. Por otro, se empiezan a emular modelos industriales e ingenieriles que surgieron en otros mbitos y con otros desencadenantes. a Debemos tener en cuenta que, al principio, el tiempo de vida de un producto acabado era muy largo; durante este tiempo, generaba benecios a las empresas, para las que era ms rentable este producto que las posibles a novedades pero, a partir de los ochenta, esta situacin empieza a cambiar. o La vida de los productos es cada vez ms corta y una vez en el mercado, son a novedad apenas unos meses, quedando fuera de l enseguida. Esto obliga a e cambiar la losof de las empresas, que se deben adaptar a este cambio consa tante y basar su sistema de produccin en la capacidad de ofrecer novedades o de forma permanente. Lo cierto es que ni los productos de software se pueden denir por completo a priori, ni son totalmente predecibles, ni son inmutables. Adems, los a procesos aplicados a la produccin industrial no tienen el mismo efecto que o en desarrollo de software, ya que en un caso se aplican sobre mquinas y en a otro, sobre personas. Estas particularidades tan caracter sticas del software no tuvieron cabida en la elaboracin del modelo ms ampliamente seguio a do hasta el momento: El modelo en cascada. En el siguiente punto de este cap tulo veremos una breve descripcin de dicho modelo, para entender su o funcionamiento y poder concluir por qu en determinados entornos era pree ciso un cambio. Como ya comentaba al principio, el objetivo es ver que el agilismo es la respuesta a una necesidad.

1.1.

Modelo en cascada

Este es el ms bsico de todos los modelos6 y ha servido como bloque de a a construccin para los dems paradigmas de ciclo de vida. Est basado en el o a a ciclo convencional de una ingenier y su visin es muy simple: el desarrollo de a o software se debe realizar siguiendo una secuencia de fases. Cada etapa tiene un conjunto de metas bien denidas y las actividades dentro de cada una contribuyen a la satisfaccin de metas de esa fase o quizs a una subsecuencia o a de metas de la misma. El arquetipo del ciclo de vida abarca las siguientes actividades: Ingenier y Anlisis del Sistema: Debido a que el software es siempre a a parte de un sistema mayor, el trabajo comienza estableciendo los requisitos de todos los elementos del sistema y luego asignando algn u subconjunto de estos requisitos al software.
6

Bennington[4], Pag. 26-30

35

1.1. Modelo en cascada

Cap tulo 1

Anlisis de los requisitos del software: el proceso de recopilacin de a o los requisitos se centra e intensica especialmente en el software. El ingeniero de software debe comprender el mbito de la informacin del a o software as como la funcin, el rendimiento y las interfaces requeridas. o Diseo: el diseo del software se enfoca en cuatro atributos distintos n n del programa; la estructura de los datos, la arquitectura del software, el detalle procedimental y la caracterizacin de la interfaz. El proceso o de diseo traduce los requisitos en una representacin del software con n o la calidad requerida antes de que comience la codicacin. o Codicacin: el diseo debe traducirse en una forma legible para la o n maquina. Si el diseo se realiza de una manera detallada, la codicacin n o puede realizarse mecnicamente. a Prueba: una vez que se ha generado el cdigo comienza la prueba del o programa. La prueba se centra en la lgica interna del software y en o las funciones externas, realizando pruebas que aseguren que la entrada denida produce los resultados que realmente se requieren. Mantenimiento: el software sufrir cambios despus de que se entrega a e al cliente. Los cambios ocurrirn debidos a que se haya encontrado a errores, a que el software deba adaptarse a cambios del entorno externo (sistema operativo o dispositivos perifricos) o a que el cliente requiera e ampliaciones funcionales o del rendimiento. En el modelo vemos una ventaja evidente y radica en su sencillez, ya que sigue los pasos intuitivos necesarios a la hora de desarrollar el software. Pero el modelo se aplica en un contexto, as que debemos atender tambin a l y e e saber que: Los proyectos reales raramente siguen el ujo secuencial que propone el modelo. Siempre hay iteraciones y se crean problemas en la aplicacin o del paradigma. Normalmente, al principio, es dif para el cliente establecer todos los cil requisitos expl citamente. El ciclo de vida clsico lo requiere y tiene a dicultades en acomodar posibles incertidumbres que pueden existir al comienzo de muchos productos. El cliente debe tener paciencia. Hasta llegar a las etapas nales del proyecto no estar disponible una versin operativa del programa. Un a o error importante que no pueda ser detectado hasta que el programa est funcionando, puede ser desastroso. e 36

Cap tulo 1

1.2. Hablemos de cifras

1.2.

Hablemos de cifras

Quizs nos encontremos en un buen punto para dejar de lado los datos a tericos y centrarnos en cifras reales que nos indiquen la magnitud del proo blema que pretendemos describir. Para ello nos basaremos en los estudios realizados por un conjunto de profesionales de Massachussets que se uni en o 1985 bajo el nombre de Standish Group7 . El objetivo de estos profesionales era obtener informacin de los proyectos fallidos en tecnolog de la informacin o as o (IT) y as poder encontrar y combatir las causas de los fracasos. El buen hacer de este grupo lo ha convertido en un referente, a nivel mundial, sobre los factores que inciden en el xito o fracaso de los proyectos de IT. Factores e que se centran, fundamentalmente, en los proyectos de software y se aplican tanto a los desarrollos como a la implementacin de paquetes (SAP, Oracle, o Microsoft, etc.) A lo largo del tiempo, el Standish Group revel 50.000 proyectos fallidos o y en 1994 se obtuvieron los siguientes resultados: Porcentaje de proyectos que son cancelados: 31 % Porcentaje de proyectos problemticos: 53 % a Porcentaje de proyectos exitosos: 16 % (pero estos slo cumplieron, en o promedio, con el 61 % de la funcionalidad prometida) Atendiendo a estos resultados poco esperanzadores, durante los ultimos diez aos, la industria invirti varios miles de millones de dlares en el desan o o rrollo y perfeccionamiento de metodolog y tecnolog (PMI, CMMI, ITIL, as as etc.). Sin embargo, en 2004 los resultados segu sin ser alentadores: an Porcentaje de proyectos exitosos: crece hasta el 29 %. Porcentaje de proyectos fracasados: 71 %. Segn el informe de Standish, las diez causas principales de los fracasos, por u orden de importancia, son: Escasa participacin de los usuarios o Requerimientos y especicaciones incompletas Cambios frecuentes en los requerimientos y especicaciones Falta de soporte ejecutivo Incompetencia tecnolgica o
7

http://www.standishgroup.com

37

1.3. El maniesto gil a

Cap tulo 1

Falta de recursos Expectativas no realistas Objetivos poco claros Cronogramas irreales Nuevas tecnolog as Cabe destacar de estos resultados que siete de los factores nombrados, son factores humanos. Las cifras evidencian la existencia de un problema, al que, como veremos a continuacin, el agilismo intenta dar respuesta. o En el libro de Roberto Canales[13] existe ms informacin sobre los mtoa o e dos en cascada, las metodolog giles y la gestin de proyectos. as a o

1.3.

El maniesto gil a

Hasta ahora hemos visto que quizs para algunos proyectos se est reaa e lizando un esfuerzo vano e incluso peligroso: intentar aplicar prcticas de a estimacin, planicacin e ingenier de requisitos. No es conveniente pensar o o a que estas prcticas son malas en s mismas o que los fracasos se deben a una a mala aplicacin de estas, sino que deber o amos recapacitar sobre si estamos aplicando las prcticas adecuadas. a En 2001, 17 representantes de nuevas metodolog y cr as ticos de los modelos de mejora basados en procesos se reunieron, convocados por Kent Beck, para discutir sobre el desarrollo de software. Fue un grito de basta ya! a las prcticas tradicionales. Estos profesionales, con una dilatada experiencia coa mo aval, llevaban ya alrededor de una dcada utilizando tcnicas que les e e fueron posicionando como l deres de la industria del desarrollo software. Conoc perfectamente las desventajas del clsico modelo en cascada donde an a primero se analiza, luego se disea, despus se implementa y, por ultimo (en n e algunos casos), se escriben algunos tests automticos y se martiriza a un a grupo de personas para que ejecuten manualmente el software, una y otra vez hasta la saciedad. El maniesto gil8 se compone de cuatro principios. a Es pequeo pero bien cargado de signicado: n
8 La traduccin del o spain.com/maniesto agil

maniesto

es

de

Agile

Spain

http://www.agile-

38

Cap tulo 1
'

1.3. El maniesto gil a $

Estamos descubriendo mejores maneras de desarrollar software tanto por nuestra propia experiencia como ayudando a terceros. A travs de esta experiencia hemos aprendido a valorar: e Individuos e interacciones sobre procesos y herramientas Software que funciona sobre documentacin exhaustiva o Colaboracin con el cliente sobre negociacin de cono o tratos Responder ante el cambio sobre seguimiento de un plan Esto es, aunque los elementos a la derecha tienen valor, nosotros valoramos por encima de ellos los que estn a la izquierda. a
Kent Beck, Mike Beedle, Arie van Bennekum, Alistair Cockburn, Ward Cunningham, Martin Fowler, James Grenning, Jim Highsmith, Andrew Hunt, Ron Jeries, Jon Kern, Brian Marick, Robert C. Martin, Steve Mellor, Ken Schwaber, Je Sutherland,

Tras este maniesto se encuentran 12 principios de vital importancia para entender su losof 9 : a Nuestra mxima prioridad es satisfacer al cliente a travs de entregas a e tempranas y continuas de software valioso. Los requisitos cambiantes son bienvenidos, incluso en las etapas nales del desarrollo. Los procesos giles aprovechan al cambio para ofrecer a una ventaja competitiva al cliente. Entregamos software que funciona frecuentemente, entre un par de semanas y un par de meses. De hecho es comn entregar cada tres o u cuatro semanas. Las personas del negocio y los desarrolladores deben trabajar juntos diariamente a lo largo de todo el proyecto. Construimos proyectos entorno a individuos motivados. Dndoles el a lugar y el apoyo que necesitan y conando en ellos para hacer el trabajo. El mtodo ms eciente y efectivo de comunicar la informacin hacia e a o y entre un equipo de desarrollo es la conversacin cara a cara. o
9 Traduccin o libre de los http://www.agilemanifesto.org/principles.html

&

Dave Thomas.

principios

publicados

en

39

1.4. En qu consiste el agilismo?: Un enfoque prctico e a

Cap tulo 1

La principal medida de avance es el software que funciona. Los procesos giles promueven el desarrollo sostenible. Los patrocia nadores, desarrolladores y usuarios deben poder mantener un ritmo constante. La atencin continua a la excelencia tcnica y el buen diseo mejora o e n la agilidad. La simplicidad, es esencial. Las mejores arquitecturas, requisitos y diseos emergen de la auton organizacin de los equipos. o A intervalos regulares, el equipo reexiona sobre cmo ser ms ecaces, o a a continuacin mejoran y ajustan su comportamiento en consecuencia. o Este libro no pretende abarcar el vasto conjunto de tcnicas y metodoe log del agilismo pero, considerando la poca literatura en castellano que as existe actualmente sobre este tema, merece la pena publicar el maniesto.

1.4.

En qu consiste el agilismo?: Un enfoque e prctico a

El agilismo es una respuesta a los fracasos y las frustraciones del modelo en cascada. A d de hoy, las metodolog giles de desarrollo de software a as a estn en boca de todos y adquieren cada vez ms presencia en el mundo a a hispano, si bien llevan siendo usadas ms de una dcada en otros pa a e ses. El abanico de metodolog giles es amplio, existiendo mtodos para organizar as a e equipos y tcnicas para escribir y mantener el software. Personalmente, me e inclino hacia la Programacin Extrema (eXtreme Programming, XP) como o forma de atacar la implementacin del producto y hacia Scrum como forma o de gestionar el proyecto, pero el estudio de ambas en su totalidad queda fuera del alcance de este libro. Por ilustrarlo a modo de alternativa al modelo en cascada: podemos gestionar el proyecto con Scrum y codicar con tcnicas de XP; concretamente e TDD10 y Programacin por Parejas11 , sin olvidar la propiedad colectiva del o cdigo y la Integracin Continua12 . o o
Test Driven Development o Desarrollo Dirigido por Test Pair Programming o Programacin por Parejas, es otra de las tcnicas que como e ponen XP y que no vamos a estudiar en detalle en este libro. Vanse en la bibliograf e a los textos relacionados con XP para mayor informacin o 12 Vase el apndice sobre Integracin Continua al nal del libro e e o
11 10

40

Cap tulo 1

1.4. En qu consiste el agilismo?: Un enfoque prctico e a

Agilismo no es perfeccionismo, es ms, el agilista reconoce que el softa ware es propenso a errores por la naturaleza de quienes lo fabrican y lo que hace es tomar medidas para minimizar sus efectos nocivos desde el principio. No busca desarrolladores perfectos sino que reconoce que los humanos nos equivocamos con frecuencia y propone tcnicas que nos aportan conanza a e pesar ello. La automatizacin de procesos es uno de sus pilares. La nalidad o de los distintos mtodos que componen el agilismo es reducir los problemas e clsicos de los programas de ordenador, a la par que dar ms valor a las a a personas que componen el equipo de desarrollo del proyecto, satisfaciendo al cliente, al desarrollador y al analista de negocio. El viejo modelo en cascada se transforma en una noria que, a cada vuelta (iteracin), se alimenta con nuevos requerimientos o aproximaciones ms o a renadas de los ya abordados en iteraciones anteriores, puliendo adems los a detalles tcnicos (no resolviendo defectos sino puliendo). Al igual que en el e modelo tradicional, existen fases de anlisis, desarrollo y pruebas pero, en a lugar de ser consecutivas, estn solapadas. Esta combinacin de etapas se a o ejecuta repetidas veces en lo que se denominan iteraciones. Las iteraciones suelen durar de dos a seis semanas y en cada una de ellas se habla con el cliente para analizar requerimientos, se escriben pruebas automatizadas, se escriben l neas de cdigo nuevas y se mejora cdigo existente. Al cliente se le o o ensean los resultados despus de cada iteracin para comprobar su aceptan e o cin e incidir sobre los detalles que se estimen oportunos o incluso reajustar o la planicacin. No es casual que hayamos situado las pruebas automticas o a antes de la escritura de nuevo cdigo ya que, como veremos en este libro, o dentro del agilismo se contempla una tcnica en la que las pruebas son una e herramienta de diseo del cdigo (TDD) y, por tanto, se escriben antes que n o el mismo. Llegado el caso las pruebas se consideran ejemplos, requerimientos que pueden ser conrmados (o validados) por una mquina (validacin a o automatizada). Todo el equipo trabaja unido, formando una pia13 y el cliente es parte n de ella, ya no se le considera un oponente. La estrategia de juego ya no es el control sino la colaboracin y la conanza. Del control se encargarn o a los procesos automticos que nos avisarn de posibles problemas o puntos a a a mejorar. La jerarqu clsica (director tcnico, analista de negocio, arquitecto, a a e programador senior, junior ...) pierde sentido y los roles se disponen sobre un eje horizontal en lugar de vertical, donde cada cual cumple su cometido pero sin estar por encima ni por debajo de los dems. En lugar de trabajar por a horas, trabajamos por objetivos y usamos el tiempo como un recurso ms y a no como un n en s mismo (lo cual no quiere decir que no existan fechas de
de ah el nombre de Scrum, que se traduce por Mel, palabra del argot del Rugby e usada para designar la unin de los jugadores en bloque o
13

41

1.4. En qu consiste el agilismo?: Un enfoque prctico e a

Cap tulo 1

entrega para cada iteracin). o La esencia del agilismo es la habilidad para adaptarse a los cambios. Ejecutando las diversas tcnicas que engloba, con la debida disciplina, se e obtienen resultados satisfactorios sin lugar para el caos. En cualquier mtodo gil, los equipos deben ser pequeos, t e a n picamente menores de siete personas. Cuando los proyectos son muy grandes y hace falta ms personal, se crean varios equipos. Nos encontramos ante el famoso a divide y vencers. a El anlisis no es exhaustivo ni se dilata indenidamente antes de empezar a la codicacin, sino que se acota en el tiempo y se encuadra dentro de cada o iteracin y es el propio progreso de la implementacin el que nos ayuda a o o terminar de descubrir los pormenores. En el anlisis buscamos cules son a a las historias de usuario y, las ambigedades que puedan surgir, se deshacen u con ejemplos concisos en forma de tests automticos. Hablaremos sobre las a historias de usuario en el cap tulo de ATDD. Dichas historias contienen los requisitos de negocio y se ordenan por prioridad segn las necesidades del u cliente, a n de desarrollar antes unas u otras. Cada requisito debe implementarse en un mximo de una semana para a que, al nal de la iteracin, el cliente pueda ver funcionalidad con valor de o negocio. El analista de negocio adoptar el rol de dueo del producto cuando el a n cliente no pueda participar tan frecuentemente como nos gustar y cama biar los cientos de pginas de documentacin en prosa por tests de acepa a o tacin14 lo sucientemente claros como para que el cliente los apruebe y o la mquina los valide. Tambin se encargar de priorizar el orden de implea e a mentacin de los requisitos acorde a lo que se hable en las reuniones con el o cliente. Los desarrolladores estarn en contacto diario con los analistas para a resolver cualquier duda del mbito de negocio lo antes posible. La experiencia a ha demostrado que una buena proporcin podr ser 1:4, esto es, al menos o a un analista de negocio por cada cuatro desarrolladores15 . Los cambios en los requisitos se suman a la planicacin de las iteraciones o siguientes y se priorizan junto con las dems tareas pendientes. a Los planes se hacen frecuentemente y se reajustan si hace falta. Siempre son planes de corta duracin, menores de seis meses, aunque la empresa o pueda tener una planicacin a muy alto nivel que cubra ms tiempo. o a El cdigo pertenece a todo el equipo (propiedad colectiva) y cualquier o desarrollador est en condiciones de modicar cdigo escrito por otro. Evia o
En el cap tulo sobre ATDD se describe este proceso Esta cifra puede ser relativa a las personas por grupo de trabajo, en los cuales los analistas estarn asignados con tiempo ms reducido, es decir, estarn en ms a a a a grupos. Por ejemplo con 16 desarrolladores y 2 analistas pueden hacerse 4 grupos de 4 desarrolladores y un analista pero cada analista en 2 grupos
15 14

42

Cap tulo 1

1.4. En qu consiste el agilismo?: Un enfoque prctico e a

tamos las situaciones del tipo... esto slo lo sabe tocar Manolo que lleva o meses trabajando en ello. Todo el equipo se rene peridicamente, incluidos usuarios y analistas de u o negocio, a ser posible diariamente y si no, al menos una vez a la semana. Por norma general, se admite que slo los desarrolladores se renan diariamente o u y que la reunin con el cliente/analista sea slo una vez a la semana, ya se o o sabe que no vivimos en un mundo ideal. De hecho, nos contentaremos con que el cliente acuda a las reuniones de comienzo de iteracin. o Las reuniones tienen hora de comienzo y de nal y son breves. Cuando suena la alarma de n de la reunin, es como si sonase la campana de o incendio en la central de bomberos: cada uno de vuelta a su puesto de trabajo inmediatamente. La superacin de obstculos imprevistos tiene prioridad sobre las cono a venciones o reglas generales de trabajo preestablecidas. Es decir, si hay que saltarse el protocolo de la empresa para resolver un problema que se nos ha atravesado, se hace. Por protocolo nos referimos a la forma en que habitualmente cooperan unas personas con otras, o tal vez la manera en que se lanzan nuevas versiones,... Las grandes decisiones de arquitectura las toma todo el equipo, no son impuestas por el arquitecto. Sigue siendo recomendable utilizar patrones de diseo y otras buenas prcticas pero siempre dando mxima importancia y n a a prioridad a los requisitos de negocio. Las arquitecturas giles son evolutivas, a no se disean al completo antes de escribir el cdigo de negocio. Este lin o bro deende particularmente las arquitecturas que emergen de los requisitos; TDD habla de que la arquitectura se forja a base de iterar y refactorizar, en lugar de disearla completamente de antemano. n La aplicacin se ensambla y se despliega en entornos de preproduccin a o o diario, de forma automatizada. Las bater de tests se ejecutan varias veces as al d La cobertura16 de los tests debe ser en torno al 60 % o mayor. En a. realidad, se trata de tener en cada iteracin una cobertura an mayor que en o u la anterior, no hay que ser demasiado precisos con el porcentaje. Los desarrolladores env sus cambios al repositorio de cdigo fuente al an o menos una vez al d (commit). a Cada vez que se termina de desarrollar una nueva funcin, esta pasa al o equipo de calidad para que la valide aunque el resto todav no estn listas. a e Partiendo de estas premisas, cada metodolog o tcnica gil detalla con a e a exactitud cmo se gestiona el proyecto y cmo es el proceso de desarrollo o o del cdigo. A veces se pueden combinar varias metodolog aunque algunos o as autores recomiendan seguir al pie de la letra la metodolog en cuestin sin a o
La cobertura de cdigo mediante tests se reere al porcentaje de cdigo que tiene o o tests asociados, considerando todos los cauces que puede tomar el ujo de ejecucin o
16

43

1.5. La situacin actual o

Cap tulo 1

mezclar ni salirse del camino en ningn momento. u No podemos negar que las metodolog son a menudo disciplinas y que as implantarlas no es sencillo, todo tiene su coste y tenemos que poner en la balanza las dicultades y los benecios para determinar qu decisin tomamos e o frente a cada problema. En el libro de Canales[13] se habla precisamente de la implantacin y puesta en marcha de metodolog en la empresa. o as Esperemos que en el futuro cercano contemos con literatura en castellano sobre cada una de las metodolog giles ms populares. as a a

1.5.

La situacin actual o

Son muchos los que se han dado cuenta de la necesidad de un cambio y, guiados por aquellos que ya lo han emprendido, han modicado el proceso de desarrollo para reaccionar ante esta crisis. La mayor parte de los que lo han hecho viven en el mundo anglosajn, en lugares como Norte Amrica o Reino o e Unido o bien siguen su corriente, como las grandes potencias en expansin, o India y China, que copian lo mejor del sistema anglosajn. Sin olvidar el o pa de la tecnolog Japn, que adems de copiar marca tendencias. Por s a, o a qu estamos tardando tanto en apuntarnos a este movimiento? e En Espaa, el objetivo de las universidades es la formacin integral del n o alumno. No se pretende capacitarles para afrontar problemas concretos en circunstancias concretas, sino hacer que sean profesionales capacitados para afrontar con xito su cometido sea cual sea la tendencia que les toque vivir. En e denitiva, el objetivo principal es la creacin, desarrollo, transmisin, difusin o o o y cr tica de la ciencia, la tcnica, el arte y la cultura, promoviendo una visin e o integral del conocimiento. En el caso concreto de la informtica, esto hace a que no se imponga como requisito que los profesores sean profesionales que se dediquen o se hayan dedicado profesionalmente a construir software. Esto no es bueno ni malo, cada cual cumple su funcin en las distintas o etapas por las que pasamos, lo que es negativo es que aceptemos, sin la menor duda ni cr tica, que lo que nos han enseado es la unica manera de sacar n adelante los proyectos. Que ya no hay que aprender nada ms. Es ciertamente a dramtico que, a pesar de esta realidad, miremos con tanto escepticismo las a nuevas tcnicas de gestin de proyectos software. Los grandes libros sobre e o software escritos en ingls en las ultimas dos dcadas, no estn escritos por e e a profesores de universidad sino por l deres de la industria con treinta aos n de batalla sobre sus espaldas. No pretendo abrir un debate sobre cul es el a objetivo de la universidad ni de la formacin profesional, sino abrir un debate o interior en cada uno de los que ya han salido de su largo periodo de formacin o y piensan que aquello que han aprendido es el unico camino posible, todo lo que tienen que aplicar, cuando en realidad lo que han adquirido es tan slo o 44

Cap tulo 1

1.5. La situacin actual o

una base y, en algunos casos, una autoconanza peligrosamente arrogante. La labor de nuestros profesores es fundamental y debemos estar agradecidos porque nos han enseado las reglas de los lenguajes formales y nos n han hablado de Alan Turing o del algoritmo de Edsger Dijkstra. No cabe duda de que, en esa etapa, hemos convertido el cerebro en un msculo bien u entrenado. La tecnolog cambia a velocidad de vrtigo, no podemos esperar a e que en la universidad nos enseen continuamente lo ultimo que va saliendo n porque en poco tiempo puede quedar obsoleto. Es ms dif que el modelo a cil de la Mquina de Turing quede obsoleto. Recuerdo cuando me quejaba pora que en la ingenier tcnica no hab visto nada de ciertas herramientas de a e a moda y ahora resulta que estn extinguindose, que realmente no las necea e sito. Desgraciadamente, hay materias que llevan aos en uso y a las que se n les augura larga vida pero que todav no han llegado a los temarios de los a institutos ni de las universidades. Las cosas de palacio van despacio. Estoy convencido de que llegarn pero no podemos esperar a que nos lo cuenten a ah para aplicarlos, porque el cliente nos est pidiendo el producto ya. Ne a cesita software de calidad ahora. Por tanto, el mensaje de fondo no es que todo lo aprendido durante nuestros aos de estudiantes sea errneo sino que n o el camino est empezando. a Todo lo dicho es aplicable a nuestros mentores en la empresa privada. Hoy d son muchas las empresas de tecnolog que estn compuestas por a a a gente muy joven y con poca experiencia que no tiene ms remedio que llea var la batuta como buenamente puede. La cuestin es plantearse que quizs o a la manera en que se resuelven los problemas no es la ms apropiada. Por a otro lado, es importante saber reconocer cuando estamos sacando el mximo a partido a los mtodos. Lo que pasa es que sin conocimiento, no podremos e discernirlo. En cuanto a las empresas con personal experimentado, no estn a libres de caer en la autoreferencia y limitarse a reproducir lo que han hecho durante aos. Esta rutina en s misma no es negativa siempre que el cliente n est satisfecho y le estemos ofreciendo el mejor servicio y, al mismo tieme po, el personal se sienta realizado. Entonces la cuestin es... Lo estamos o haciendo?. La parte artesanal de los programas de ordenador se podr deber a que a desconocemos la totalidad de las variables de las que depende, porque si las conocisemos de antemano, no ser una ingenier tan distinta del resto. e a a Desde un punto de vista muy ingenieril, podemos considerar que la artesan a es simplemente una forma de producir demasiado compleja como para sintetizarla y reproducirla mecnicamente. Este arte no se desarrolla estudiando a teor sino practicando, al igual que a andar se aprende andando. a En el mundo tecnolgico los meses parecen d y los aos, meses. Las o as n oportunidades aparecen y se desvanecen fugazmente y nos vemos obligados 45

1.6. Agil parece, pltano es a

Cap tulo 1

a tomar decisiones con presteza. Las decisiones tecnolgicas han convertido o en multimillonarias a personas en cuestin de meses y han hundido impeo rios exactamente con la misma rapidez. Ahora nos est comenzando a llegar a la onda expansiva de un movimiento que pone en entredicho tcnicas que e ten amos por buenas pero que con el paso de los aos se estn revelando n a insostenibles. Si bien hace poco gustbamos de disear complejas arquiteca n turas antes de escribir una sola l nea de cdigo que atacase directamente o al problema del cliente, ahora, con la escasez de recursos econmicos y la o mayor exigencia de los usuarios, la palabra agilidad va adquiriendo valores de ecacia, elegancia, simplicidad y sostenibilidad. Podemos beneciarnos de esta nueva corriente?. Saber adaptarse al cambio es esencial para la evolucin. Nos adaptaremos a los cambios del entorno a tiempo?. Todos esos o pa de los que hablbamos son competidores en realidad y lo sern cada ses a a vez ms dada la rpida expansin de Internet. No estar a a o amos mejor si fuesen simplemente colegas?. El software es una herramienta de presente y de futuro, creada para hacer ms agradable la vida de los usuarios. Y, aunque tienda a olvidarse, tambin a e puede ser muy graticante para los desarrolladores/analistas. Tendremos que valernos de conanza y dedicacin junto con gusto por el trabajo para alo canzar esta meta pero... Cmo podemos fomentar estas condiciones? Como o ven, zarpamos con preguntas hacia el fascinante mundo del desarrollo gil de a software. Ser una traves que nos ir descubriendo las claves de cmo hacer a a a o mejor software al tiempo que nos sentimos ms satisfechos con nuestro traa bajo. Es posible escribir software de mayor calidad con menos complicaciones y aportar ms a los negocios de las personas que lo utilizan. a Bienvenidos a bordo.

1.6.

Agil parece, pltano es a

Se est usando mucho la palabra gil17 y, por desgracia, no siempre a a est bien empleada. Algunos aprovechan el trmino gil para referirse a cowa e a boy programming (programacin a lo vaquero), es decir, hacer lo que les o viene en gana, como quieren y cuando quieren. Incluso hay empresas que creen estar siguiendo mtodos giles pero que en realidad no lo hacen (y no e a saben que no lo hacen). Existen mitos sobre el agilismo que dicen que no se documenta y que no se planica o analiza. Tambin se dice que no se e necesitan arquitectos pero, no es cierto, lo que sucede es que las decisiones de arquitectura se toman en equipo. El mal uso de la palabra gil causa malas y falsas ideas sobre lo que a
17

agile en ingls, pronunciada como yail e a

46

Cap tulo 1

1.7. Los roles dentro del equipo

verdaderamente es. Llegado este punto, hay que mirar con lupa a quien dice que est siguiendo un desarrollo gil, tal como pasa con quien dice que vende a a productos ecolgicos. Hay quien cree que es gil porque habla mucho con o a el cliente. Quizs por eso aparecieron las certicaciones en determinadas a metodolog giles aunque, como muchas otras certicaciones, son slo paas a o peles que no garantizan la profesionalidad de la persona certicada(conf en o que las certicaciones de la agricultura ecolgica s sean autnticas). No nos o e debemos ar de alguien que ha asistido dos d a un curso de Scrum y ya as dice ser un maestro, a no ser que tenga aos de experiencia que le avalen. n Adoptar una metodolog supone aprendizaje y disciplina, como todo lo a que est bien hecho y, quienes realmente quieren subirse a este carro, nea cesitarn la ayuda de personas expertas en la materia. En Internet existen a multitud de grupos y foros donde se ofrece ayuda desinteresadamente y tambin existen profesionales que ofrecen formacin y entrenamiento en estas e o reas y se desplazan a cualquier parte del mundo para trabajar con grupos. a En ingls hay bastante literatura al respecto y es ms que recomendable leer e a varios libros, sobre todo aquellos cuyos autores rmaron el maniesto gil. a Sobre recursos en castellano, actualmente hay mucho movimiento y grandes profesionales en Agile Spain (comunidad de mbito espaol) y en el Foro a n Agiles18 (la comunidad latinoamericana, muy extendida en Argentina), que entre otras cosas, organiza el evento internacional anual Agiles, as como multitud de openspaces bajo la marca Agile Open.

1.7.

Los roles dentro del equipo

Saber distinguir las obligaciones y limitaciones de cada uno de los roles del equipo ayuda a que el trabajo lo realicen las personas mejor capacitadas para ello, lo que se traduce en mayor calidad. Roles distintos no necesariamente signica personas distintas, sobre todo en equipos muy reducidos. Una persona puede adoptar ms de un rol, puede ir adoptando distintos roles con a el paso del tiempo, o rotar de rol a lo largo del d Hagamos un repaso a los a. papeles ms comunes en un proyecto software. a Dueo del producto n Cliente Analista de negocio Desarrolladores
18

http://tech.groups.yahoo.com/group/foro-agiles

47

1.7. Los roles dentro del equipo

Cap tulo 1

Arquitectos Administradores de Sistemas Dueo del producto: Su misin es pedir lo que necesita (no el cmo, sino n o o el qu) y aceptar o pedir correcciones sobre lo que se le entrega. e Cliente: Es el dueo del producto y el usuario nal. n Analista de negocio: Tambin es el dueo del producto porque trabaja e n codo a codo con el cliente y traduce los requisitos en tests de aceptacin o para que los desarrolladores los entiendan, es decir, les explica qu hay que e hacer y resuelve sus dudas. Desarrolladores: Toman la informacin del analista de negocio y deciden o cmo lo van a resolver adems de implementar la solucin. Aparte de escribir o a o cdigo, los desarrolladores deben tener conocimientos avanzados sobre usao bilidad y diseo de interfaces de usuario, aunque es conveniente contar con n una persona experimentada para asistir en casos particulares. Lo mismo para la accesibilidad. Administradores de sistemas: Se encargan de velar por los servidores y servicios que necesitan los desarrolladores. En el mundo anglosajn se habla mucho del arquitecto del software. El o arquitecto es la persona capaz de tomar decisiones de diseo pero adems n a se le supone la capacidad de poder hablar directamente con el cliente y entender los requisitos de negocio. En lugar de un rol, es una persona que adopta varios roles. En el agilismo todos los desarrolladores son arquitectos en el sentido de que se les permite tomar decisiones de arquitectura conforme se va escribiendo o refactorizando cdigo. Hay que resaltar que se hacen o revisiones de cdigo entre compaeros. Adems, ante decisiones complejas o n a se pide opinin a desarrolladores ms experimentados. Recordemos que existe o a propiedad colectiva del cdigo y uidez de conocimiento dentro del equipo. o Parece que no son tan complicados los roles... sin embargo, los confundimos a menudo. En nuestra industria del software hemos llegado al extremo de el que el cliente nos dice a nosotros, los ingenieros, cmo tenemos que o hacer las cosas. Nos dice que quiere una pantalla con tal botn y tales mens, o u que las tablas de la base de datos tienen tales columnas, que la base de datos tiene que ser Oracle... y nosotros lo aceptamos!. Sabemos que la escasez de profesionalidad ha tenido mucho que ver con esto y, el hecho de no tener claro cules son los roles de cada uno, hace que no seamos capaces de a ponernos en nuestro sitio. Quien dice el cliente, dice el dueo del producto n o, llegado el caso, el analista. De hecho, a menudo nos encontramos con analistas de negocio que, cuando hacen el anlisis, entregan al equipo de a desarrollo interfaces de usuario (pantallas dibujadas con Photoshop o con cualquier otro diseador) adems de las tablas que creen que lleva la base de n a 48

Cap tulo 1

1.8. Por qu nos cuesta comenzar a ser giles? e a

datos y sus consultas. No hab amos quedado en que el dueo del producto n pide el qu y no dice el cmo?. Si la persona que tiene el rol de analista e o tambin tiene el rol de desarrollador, entonces es comprensible que disee e n una interfaz de usuario pero entonces no deber pintarla con un programa a de diseo grco y endirsela a otro, sino trabajar en ella. Las pantallas n a na no se disean al comienzo sino al nal, cuando los requisitos de negocio ya n se cumplen. Los requisitos son frases cortas en lenguaje natural que ejecuta una mquina automticamente, ya que tienen forma de test, con lo que se a a sabe cundo se han implementado. Si las pantallas se disean primero, se a n contamina la lgica de negocio con la interpretacin que el diseador pueda o o n hacer de los requisitos y corremos el riesgo de escribir un cdigo sujeto a la o UI en lugar de a los requisitos, lo cual lo hace dif de modicar ante camcil bios futuros en el negocio. El dueo del producto tampoco debe disear las n n tablas de la base de datos a no ser que tambin adopte el rol de desarrollador e pero, incluso as las tablas son de los ultimos19 elementos que aparecen en , el proceso de implementacin del requisito, tal como ocurre con la interfaz o grca. Es decir, vamos desde el test de aceptacin a tests de desarrollo que a o acabarn en la capa de datos que pide persistencia. Pensamos en requisia tos, implementamos objetos, luego bajamos a tablas en una base de datos relacional y nalmente le ponemos una carcasa a la aplicacin que se llama o interfaz grca de usuario. No al revs. a e Si cada cual no se limita a ejercer su rol o roles, estaremos restringiendo a aquellos que saben hacer su trabajo, limitndoles de modo que no les dejamos a hacer lo mejor que saben.

1.8.

Por qu nos cuesta comenzar a ser giles? e a

Si el agilismo tiene tantas ventajas, Por qu no lo est practicando ya e a todo el mundo? La resistencia al cambio es uno de los motivos fundamentales. Todav forma parte de nuestra cultura pensar que las cosas de toda la a vida son las mejores. Ya se sabe... Si es de toda la vida, es como debe ser. Si los ingenieros y los cient cos penssemos as entonces tendr a , amos mquinas de escribir en lugar de computadoras (en el mejor de los casos). a Existen fuertes razones histricas para ser tan reticentes al cambio pero los o que trabajamos con tecnolog podemos dar un paso al frente en favor del a desarrollo. Ojo! Podemos dejar nuestro lado conservador para otros aspectos no tecnolgicos, que seguro nos aportar muchos benecios. No se trata de o a ser progresista ni estamos hablando de pol tica, limitmonos a cuestiones de e ciencia y tecnolog a.
19 Cuando la lgica de negocio es tan simple como guardar y recuperar un dato, es o aceptable empezar por los datos.

49

1.8. Por qu nos cuesta comenzar a ser giles? e a

Cap tulo 1

Estamos preparados para darle una oportunidad a la innovacin o nos o quedamos con lo de toda la vida, aunque slo tenga una vida de medio o siglo? (en el caso de programadores junior, slo un par de aos). Hemos o n aceptado una determinada forma de trabajar (en el mundo del software) y nos parece inmutable an cuando esta industria todav est en paales. Hemos u a a n llegado al punto en que la informtica ya no es una cuestin de matemticos a o a sino de especialistas en cada una de las muchas reas de la informtica. Ni a a siquiera se han jubilado an los primeros expertos en software de la historia. u Cuando era nio no se me pasaba por la cabeza que todo el mundo llevase n un telfono mvil encima y se comunicase desde cualquier lugar (hace slo e o o 15 aos). Hace poco no nos imaginbamos que comprar n a amos por Internet ni que las personas encontrar pareja a travs de la red. Los que han tenido an e conanza en el cambio y han sabido crecer orgnicamente trabajan en lo a que les gusta y no tienen problemas para llegar a n de mes. Las nuevas tecnolog son el tren de alta velocidad que une el presente con el futuro en as un abrir y cerrar de ojos. Ahora bien, aprender una nueva tcnica supone esfuerzo. Es natural que e nos d pereza dar los primeros pasos hacia el cambio y por eso usamos mil e excusas: Que es antinatural... Que est todo al revs... a e Que es un caos... Que no tenemos tiempo ahora para aprender eso... Que no tenemos los conocimientos previos para empezar... Que no sabemos por dnde empezar... o Maana empiezo... n Es que,... es que... Nos comportamos como lo har un fumador al que le dicen que deje de a fumar. La corriente popular en trminos de software no es capaz de evolue cionar lo sucientemente rpido como para que sus teor sean las mejores. a as Hay que plantearse si seguirla es buena idea o conviene cambiar de corriente. Esto es,... Preere la pastilla azul o la roja?20 . No negaremos que hay que
Clebre escena de la pel e cula Matrix en que Morfeo ofrece a Neo la posibilidad de despertar del sueo. Neo escoge la roja. En este caso despertar del sueo signica n n cambia a mejor, a diferencia de lo que sucede en esta pel cula de ccin. La Pastilla o Roja tambin es el t e tulo de un libro sobre Software Libre escrito por Juan Toms a Garc a(http://www.lapastillaroja.net/resumen ejecutivo.html)
20

50

Cap tulo 1

1.8. Por qu nos cuesta comenzar a ser giles? e a

hacer una inversin en tiempo y esfuerzo para aprender y poner en prctica o a una nueva forma de funcionar, la cuestin es que tal inversin se amortiza o o rpidamente. Si esperamos a maana para que se den las condiciones pera n fectas y empezar a ser giles, quizs nunca llegue el d Acaso piensa que a a a. alguna vez tendr tiempo y dinero de sobra para todo lo que quiera? El plan a es ms bien parecido al de echar monedas a la hucha para irse de vacaciones; a pequeas inversiones poco a poco. No se interprete que podemos jugar con n el dinero del cliente, aprender no signica jugar con su dinero, vale?. Si aceptamos que el software siempre se puede mejorar, el siguiente paso es admitir que es positivo mantener un cierto aire inconformista en nuestra actitud profesional. La autocr tica nos lleva a escribir cdigo de mayor calidad o y a reconocer nuestros errores. El juicio sano sobre nuestro trabajo nos gu a en la bsqueda de mejoras estrategias y nos ayuda a superar la pereza que u nos produce la idea del cambio. Todos los d aprendemos algo nuevo. El as d que deje de ser as habr que reexionar seriamente sobre si estamos a a ejerciendo bien el puesto de ingenieros de software. Este cap tulo ha sido compuesto a base de pinceladas procedentes diversos temas. No pretende ser completo, ya que se podr escribir un libro entero a sobre metodolog sino solo establecer un contexto de partida para el resto as, de cap tulos.

51

1.8. Por qu nos cuesta comenzar a ser giles? e a

Cap tulo 1

52

Captulo

Qu es el Desarrollo Dirigido por e Tests? (TDD)


El Desarrollo Dirigido por Tests (Test Driven Development), al cual me referir como TDD, es una tcnica de diseo e implementacin de software e e n o incluida dentro de la metodolog XP. Coincido con Peter Provost1 en que el a nombre es un tanto desafortunado; algo como Diseo Dirigido por Ejemplos n hubiese sido quizs mas apropiado. TDD es una tcnica para disear software a e n que se centra en tres pilares fundamentales: La implementacin de las funciones justas que el cliente necesita y no o ms2 . a La minimizacin del nmero de defectos que llegan al software en fase o u de produccin. o La produccin de software modular, altamente reutilizable y preparado o para el cambio. Cuando empezamos a leer sobre TDD creemos que se trata de una buena tcnica para que nuestro cdigo tenga una cobertura de tests muy alta, algo e o que siempre es deseable, pero es realmente una herramienta de diseo que n convierte al programador en un ocial de primera. O, si no les gustan las metforas, convierte al programador en desarrollador3 . TDD es la respuesta a a las grandes preguntas de:
1 2

http://www.youtube.com/watch?v=JMEO6T6gkAA Evitamos desarrollar funcionalidad que nunca ser usada a 3 http://www.ericsink.com/No Programmers.html

53

Cap tulo 2 Cmo lo hago?, Por dnde empiezo?, Cmo s qu es lo que o o o e e hay que implementar y lo que no?, Cmo escribir un cdigo que o o se pueda modicar sin romper funcionalidad existente? No se trata de escribir pruebas a granel como locos, sino de disear adecuan damente segn los requisitos. u Pasamos, de pensar en implementar tareas, a pensar en ejemplos certeros que eliminen la ambigedad creada por la prosa en lenguaje natural (nuestro u idioma). Hasta ahora estbamos acostumbrados a que las tareas, o los casos a de uso, eran las unidades de trabajo ms pequeas sobre las que ponerse a a n desarrollar cdigo. Con TDD intentamos traducir el caso de uso o tarea en X o ejemplos, hasta que el nmero de ejemplos sea suciente como para describir u la tarea sin lugar a malinterpretaciones de ningn tipo. u En otras metodolog de software, primero nos preocupamos de denir as cmo va a ser nuestra arquitectura. Pensamos en las clases de infraestruco tura que van a homogeneizar la forma de trabajar en todos y cada uno de los casos, pensamos si vamos a usar un patrn Facade4 y otro Singleton5 o y una comunicacin mediante eventos, o DTOs, y una clase central que va o a hacer esto y aquello... Y si luego resulta que no necesitamos todo eso? Cunto vamos a tardar en darnos cuenta de ello? Cunto dinero vamos a a a malgastar? En TDD dejamos que la propia implementacin de pequeos o n ejemplos, en constantes iteraciones, hagan emerger la arquitectura que necesitamos usar. Ni ms ni menos. No es que nos despreocupemos por completo a de las caracter sticas tcnicas de la aplicacin a priori, es decir, lgicamente e o o tendremos que saber si el desarrollo ser para un telfono mvil, para una a e o web o para un pc de escritorio; ms que nada porque tenemos que elegir a unas herramientas de desarrollo conformes a las exigencias del guin. Sin o embargo, nos limitamos a escoger el framework correspondiente y a usar su arquitectura como base. Por ejemplo, si escog esemos Django6 o ASP.NET MVC7 , ya tendr amos denida buena parte de la base antes de empezar a escribir una sola l nea de cdigo. No es que trabajemos sin arquitectura, lgio o camente, si en los requisitos est la interoperabilidad en las comunicaciones, a tendremos que usar servicios web o servicios REST, lo cual ya propicia un determinado soporte. Lo que eliminamos son las arquitecturas encima de esas arquitecturas, las que intentan que todo se haga siempre igual y tal como se le ocurri al genio de la empresa. A ser posible, esas que nos obligan a moo dicar siete cheros para cambiar una cadena de texto. TDD produce una arquitectura que emerge de la no-ambigedad de los tests automatizados, u
4

o n Facade: http://es.wikipedia.org/wiki/Facade (patrn de diseo) Singleton: http://es.wikipedia.org/wiki/Singleton 6 http://www.djangoproject.com 7 http://www.asp.net/mvc/


5

54

Cap tulo 2 lo cual no exime de las revisiones de cdigo entre compaeros ni de hacer o n preguntas a los desarrolladores ms veteranos del equipo. a Las primeras pginas del libro de Kent Beck [3] (uno de los padres de a la metodolog XP) dan unos argumentos muy claros y directos sobre por a qu merece la pena darle unos cuantos tragos a TDD, o mejor, por qu es e e benecioso convertirla en nuestra herramienta de diseo principal. Estas son n algunas de las razones que da Kent junto con otras destacadas guras de la industria: La calidad del software aumenta (y veremos por qu). e Conseguimos cdigo altamente reutilizable. o El trabajo en equipo se hace ms fcil, une a las personas. a a Nos permite conar en nuestros compaeros aunque tengan menos n experiencia. Multiplica la comunicacin entre los miembros del equipo. o Las personas encargadas de la garant de calidad adquieren un rol ms a a inteligente e interesante. Escribir el ejemplo (test) antes que el cdigo nos obliga a escribir el o m nimo de funcionalidad necesaria, evitando sobredisear. n Cuando revisamos un proyecto desarrollado mediante TDD, nos damos cuenta de que los tests son la mejor documentacin tcnica que podeo e mos consultar a la hora de entender qu misin cumple cada pieza del e o puzzle. Personalmente, aadir lo siguiente: n a Incrementa la productividad. Nos hace descubrir y afrontar ms casos de uso en tiempo de diseo. a n La jornada se hace mucho ms amena. a Uno se marcha a casa con la reconfortante sensacin de que el trabajo o est bien hecho. a Ahora bien, como cualquier tcnica, no es una varita mgica y no dar el e a a mismo resultado a un experto arquitecto de software que a un programador junior que est empezando. Sin embargo, es util para ambos y para todo el a rango de integrantes del equipo que hay entre uno y otro. Para el arquitecto es 55

2.1. El algoritmo TDD

Cap tulo 2

su mano derecha, una gu que le hace claricar el dominio de negocio a cada a test y que le permite conar en su equipo aunque tenga menos experiencia. Frecuentemente, nos encontramos con gente muy desconada que mira con lupa el cdigo de su equipo antes de que nadie pueda hacer commit al o sistema de control de versiones. Esto se convierte en un cuello de botella porque hay varias personas esperando por el jefe (el arquitecto) para que d el visto bueno y a este se le acumula el trabajo. Ni que decir tiene que el e ambiente de trabajo en estos casos no es nada bueno ni productivo. Cuando el jefe sabe que su equipo hace TDD correctamente puede conar en ellos y en lo que diga el sistema de integracin cont 8 y las estad o nua sticas del repositorio de cdigo. Para el programador junior que no sabe por dnde va o o a coger al toro, o si es el toro quien le va a coger a l (o a ella), se convierte en e el Pepito Grillo que le cuenta qu paso tiene que dar ahora. Y as un paso e , tras otro, le gu en la implementacin de la tarea que le ha sido asignada. a o Cuando el equipo practica de esta manera, la comunicacin uye, la o gente se vuelve ms activa y la maquinaria funciona como un engranaje a bien lubricado. Todos los que disfrutamos trabajando en el software llevamos dentro al personaje del buen arquitecto, entre muchos otros de nuestros personajes. La prctica que aqu se describe nos lo trae para que nos ayude a y nos haga la vida ms fcil. a a

2.1.

El algoritmo TDD

La esencia de TDD es sencilla pero ponerla en prctica correctamente es a cuestin de entrenamiento, como tantas otras cosas. El algoritmo TDD slo o o tiene tres pasos: Escribir la especicacin del requisito (el ejemplo, el test). o Implementar el cdigo segn dicho ejemplo. o u Refactorizar para eliminar duplicidad y hacer mejoras. Vemosla en detalle. a

2.1.1.

Escribir la especicacin primero o

Una vez que tenemos claro cul es el requisito, lo expresamos en forma a de cdigo. Si estamos a nivel de aceptacin o de historia, lo haremos con un o o framework tipo Fit, Fitnesse, Concordion o Cucumber. Esto es, ATDD. Si no, lo haremos con algn framework xUnit. Cmo escribimos un test para u o
8

http://es.wikipedia.org/wiki/Continuous integration

56

Cap tulo 2

2.1. El algoritmo TDD

un cdigo que todav no existe? Respondamos con otra pregunta Acaso o a no es posible escribir una especicacin antes de implementarla? Por citar o un ejemplo conocido, las JSR (Java Specication Request) se escriben para que luego terceras partes las implementen... ah, entonces es posible!. El framework Mono se ha implementado basndose en las especicaciones del a ECMA-334 y ECMA-335 y funciona. Por eso, un test no es inicialmente un test sino un ejemplo o especicacin. La palabra especicacin podr tener o o a la connotacin de que es inamovible, algo preestablecido y jo, pero no es o as Un test se puede modicar. Para poder escribirlo, tenemos que pensar . primero en cmo queremos que sea la API del SUT9 , es decir, tenemos que o trazar antes de implementar. Pero slo una parte pequea, un comportao n miento del SUT bien denido y slo uno. Tenemos que hacer el esfuerzo o de imaginar cmo seria el cdigo del SUT si ya estuviera implementado y o o cmo comprobar o amos que, efectivamente, hace lo que le pedimos que haga. La diferencia con los que dictan una JSR es que no diseamos todas las n especicaciones antes de implementar cada una, sino que vamos una a una siguiendo los tres pasos del algoritmo TDD. El hecho de tener que usar una funcionalidad antes de haberla escrito le da un giro de 180 grados al cdigo o resultante. No vamos a empezar por fastidiarnos a nosotros mismos sino que nos cuidaremos de disear lo que nos sea ms cmodo, ms claro, siempre n a o a que cumpla con el requisito objetivo. En los prximos cap o tulos veremos cmo o mediante ejemplos.

2.1.2.

Implementar el cdigo que hace funcionar el ejemplo o

Teniendo el ejemplo escrito, codicamos lo m nimo necesario para que se cumpla, para que el test pase. T picamente, el m nimo cdigo es el de menor o nmero de caracteres porque m u nimo quiere decir el que menos tiempo nos llev escribirlo. No importa que el cdigo parezca feo o chapucero, eso lo o o vamos a enmendar en el siguiente paso y en las siguientes iteraciones. En este paso, la mxima es no implementar nada ms que lo estrictamente obligatorio a a para cumplir la especicacin actual. Y no se trata de hacerlo sin pensar, o sino concentrados para ser ecientes. Parece fcil pero, al principio, no lo es; a veremos que siempre escribimos ms cdigo del que hace falta. Si estamos a o bien concentrados, nos vendrn a la mente dudas sobre el comportamiento a del SUT ante distintas entradas, es decir, los distintos ujos condicionales que pueden entrar en juego; el resto de especicaciones de este bloque de funcionalidad. Estaremos tentados de escribir el cdigo que los gestiona sobre o la marcha y, en ese momento, slo la atencin nos ayudar a contener el o o a
Subject Under Test. Es el objeto que nos ocupa, el que estamos dise ando a travs n e de ejemplos.
9

57

2.1. El algoritmo TDD

Cap tulo 2

impulso y a anotar las preguntas que nos han surgido en un lugar al margen para convertirlas en especicaciones que retomaremos despus, en iteraciones e consecutivas.

2.1.3.

Refactorizar

Refactorizar no signica reescribir el cdigo; reescribir es ms general o a que refactorizar. Segun Mart Fowler, refactorizar10 es modicar el diseo n n sin alterar su comportamiento. A ser posible, sin alterar su API pblica. En u este tercer paso del algoritmo TDD, rastreamos el cdigo (tambin el del o e test) en busca de l neas duplicadas y las eliminamos refactorizando. Adems, a revisamos que el cdigo cumpla con ciertos principios de diseo (me inclino o n por S.O.L.I.D) y refactorizamos para que as sea. Siempre que llego al paso de refactorizar, y elimino la duplicidad, me planteo si el mtodo en cuestin e o y su clase cumplen el Principio de una Unica Responsabilidad11 y dems a principios. El propio Fowler escribi uno de los libros ms grandes de la literatura o a tcnica moderna[7] en el que se describen las refactorizaciones ms comunes. e a Cada una de ellas es como una receta de cocina. Dadas unas precondiciones, se aplican unos determinados cambios que mejoran el diseo del softwan re mientras que su comportamiento sigue siendo el mismo. Mejora es una palabra ciertamente subjetiva, por lo que empleamos la mtrica del cdigo e o duplicado como parmetro de calidad. Si no existe cdigo duplicado, entona o ces hemos conseguido uno de ms calidad que el que presentaba duplicidad. a Mas all de la duplicidad, durante la refactorizacin podemos permitirnos a o darle una vuelta de tuerca al cdigo para hacerlo ms claro y fcil de mano a a tener. Eso ya depende del conocimiento y la experiencia de cada uno. Los IDE como Eclipse, Netbeans o VisualStudio, son capaces de llevar a cabo las refactorizaciones ms comunes. Basta con sealar un bloque de cdigo a n o y elegir la refactorizacin Extraer-Mtodo, Extraer-Clase, Pull-up, Pull-down o e o cualquiera de las muchas disponibles. El IDE modica el cdigo por nosoo tros, asegurndonos que no se cometen errores en la transicin. Al margen de a o estas refactorizaciones, existen otras ms complejas que tienen que ver con a la maestr del desarrollador y que a veces recuerdan al mago sacando un a conejo de la chistera. Algunas de ellas tienen nombre y estn catalogadas a a modo de patrn y otras son annimas pero igualmente eliminan la duplicidad. o o Cualquier cambio en los adentros del cdigo, que mantenga su API pblio u ca, es una refactorizacin. La clave de una buena refactorizacin es hacerlo o o en pasitos muy pequeos. Se hace un cambio, se ejecutan todos los tests n
10 11

http://www.refactoring.com/ Ver Cap tulo7 en la pgina 111 a

58

Cap tulo 2

2.1. El algoritmo TDD

y, si todo sigue funcionando, se hace otro pequeo cambio. Cuando refacn torizamos, pensamos en global, contemplamos la perspectiva general, pero actuamos en local. Es el momento de detectar malos olores y eliminarlos. El verbo refactorizar no existe como tal en la Real Academia Espaola pero, tras n discutirlo en la red, nos resulta la mejor traduccin del trmino refactoring. o e La tarea de buscar y eliminar cdigo duplicado despus de haber completado o e los dos pasos anteriores, es la que ms tiende a olvidarse. Es comn entrar en a u la dinmica de escribir el test, luego el SUT, y as sucesivamente olvidando a la refactorizacin. Si de las tres etapas que tiene el algoritmo TDD dejamos o atrs una, lgicamente no estamos practicando TDD sino otra cosa. a o Otra forma de enumerar las tres fases del ciclo es: Rojo Verde Refactorizar Es una descripcin metafrica ya que los frameworks de tests suelen colorear o o en rojo aquellas especicaciones que no se cumplen y en verde las que lo hacen. As cuando escribimos el test, el primer color es rojo porque todav , a no existe cdigo que implemente el requisito. Una vez implementado, se pasa o a verde. Cuando hemos dado los tres pasos de la especicacin que nos ocupa, o tomamos la siguiente y volvemos a repetirlos. Parece demasiado simple, la reaccin de los asistentes a mis cursos es mayoritariamente incrdula y es o e que el efecto TDD slo se percibe cuando se practica. Me gusta decir que o tiene una similitud con un buen vino; la primera vez que se prueba el vino en la vida, no gusta a nadie, pero a fuerza de repetir se convierte en un placer para los sentidos. Connotaciones alcohlicas a un lado, espero que se capte o el mensaje. Y TDD sirve para proyectos grandes? Un proyecto grande no es sino la agrupacin de pequeos subproyectos y es ahora cuando toca aplicar aquello o n de divide y vencers. El tamao del proyecto no guarda relacin con la a n o aplicabilidad de TDD. La clave est en saber dividir, en saber priorizar. De a ah la ayuda de Scrum para gestionar adecuadamente el backlog del producto. Por eso tanta gente combina XP y Scrum. Todav no he encontrado ningn a u proyecto en el que se desaconseje aplicar TDD. En la segunda parte del libro se expondr el algoritmo TDD mediante a ejemplos prcticos, donde iremos de menos a ms, iterando progresivamente. a a No se preocupe si no lo ve del todo claro ahora.

59

2.2. Consideraciones y recomendaciones

Cap tulo 2

2.2.
2.2.1.

Consideraciones y recomendaciones
Ventajas del desarrollador experto frente al junior

Existe la leyenda de que TDD unicamente es vlido para personal alta a mente cualicado y con much sima experiencia. Dista de la realidad; TDD es bueno para todos los individuos y en todos los proyectos. Eso s hay algunos , matices. La diferencia entre el desarrollador experimentado que se sienta a hacer TDD y el junior, es cmo enfocan los tests, es decir, qu tests escriben; o e ms all del cdigo que escriben. El experto en diseo orientado a objetos a a o n buscar un test que fuerce al SUT a tener una estructura o una API que a sabe que le dar buenos resultados en trminos de legibilidad y reusabilidad. a e Un experto es capaz de anticipar futuros casos de uso y futuros problemas y ser ms cuidadoso diseando la API test tras test, aplicando las buenas a a n prcticas que conoce. El junior probablemente se siente a escribir lo que mejor a le parece, sin saber que la solucin que elige quizs le traiga quebraderos de o a cabeza ms adelante. La ventaja es que, cuando se d cuenta de que su diseo a e n tiene puntos a mejorar y empiece a refactorizar, contar con un important a simo respaldo detrs en forma de bater de tests. Por poco experimentado a a que sea, se cuidar de no disear una API que le resulte casi imposible de a n usar. Debe tenerse en cuenta que se supone que el principiante no est soa lo, sino que en un contexto XP, hay desarrolladores de ms experiencia que a supervisarn y habr momentos en los que se programe en parejas. La gura a a de los l deres es importante en XP al igual que en otras metodolog con la as, gran diferencia de que el l gil est para responder preguntas y ayudar a der a a los dems y no para darles ltigo. El l debe intentar que las personas que a a der trabajan con l estn contentas de trabajar ah y quieran seguir hacindolo. e e e

2.2.2.

TDD con una tecnolog desconocida a

La primera vez que usamos una determinada tecnolog o incluso una a nueva librer es complicado que podamos escribir la especicacin antes a, o que el SUT, porque no sabemos las limitaciones y fortalezas que ofrece la nueva herramienta. En estos casos, XP habla de spikes (disculpen que no lo traduzca, no sabr como) .Un spike es un pequeo programa que se escria n be para indagar en la herramienta, explorando su funcionalidad. Es hacerse alguna funcin o alguna aplicacin pequea que nos aporte el conocimiento o o n que no tenemos. Si el spike es pequeo, y resulta que nos damos cuenta n que su propio cdigo es vlido tal cual, entonces escribiremos el test justo a o a continuacin, en lugar de dejarlo sin test. Sin un conocimiento bsico de la o a API y las restricciones del sistema, no recomendar lanzarse a escribir espea cicaciones. Hay que respetar el tiempo de aprendizaje con la herramienta y 60

Cap tulo 2

2.2. Consideraciones y recomendaciones

avanzar una vez que tengamos conanza con ella. Intentar practicar TDD en un entorno desconocido es, a mi parecer, un antipatrn poco documentado. o Tampoco es que descartemos forzosamente TDD, sino que primero tendremos que aprender a pilotar la mquina. Una vez sepamos si es de cambio a manual o automtico, dnde se encienden las luces y dnde se activa el lima o o pia parabrisas, podremos echar a rodar. Es slo cuestin de aplicar el sentido o o comn, primero aprendemos a usar la herramienta y luego la usamos. Teneu mos que evitar algo que pasa muy frecuentemente, minusvalorar el riesgo de no dominar las herramientas (y frameworks y lenguajes...)

2.2.3.

TDD en medio de un proyecto

En la segunda parte del libro, la de los ejemplos prcticos, iniciamos el a desarrollo de una aplicacin desde cero. Igual que hacemos en los cursos que o imparto. La pregunta de los asistentes aparece antes o despus: no se puede e aplicar TDD en un proyecto que ya est parcialmente implementado? Claro a que se puede, aunque con ms consideraciones en juego. Para los nuevos a requisitos de la aplicacin, es decir, aquello que todav falta por implemeno a tar, podremos aplicar eso de escribir el test primero y luego el cdigo (y o despus refactorizar!). Es probable que el nuevo SUT colabore con partes e legadas que no permiten la inyeccin de dependencias y que no cumplen una o 12 ; cdigo legado que nos diculta su reutilizacin. El unica responsabilidad o o libro ms recomendado por todos en los ultimos tiempos sobre este asuna to es, Working Eectively with Legacy Code de Michael C. Feathers[6]. Tratar con cdigo legado no es moco de pavo. En general, por cdigo leo o gado entendemos que se trata de aquel que no tiene tests de ningn tipo. u Mi recomendacin, antes de ponerse a reescribir partes de cdigo legado, es o o crear tests de sistema (y cuando el cdigo lo permita, tests unitarios) que o minimicen los posibles efectos colaterales de la reescritura. Si es una web, por ejemplo, agarrar Selenium o similar y grabar todos los posibles usos de la interfaz grca para poderlos reproducir despus de las modicaciones y a e comprobar que todo el sistema se sigue comportando de la misma manera. Es un esfuerzo de usar y tirar porque estos tests son tremendamente frgiles, a pero es mucho ms seguro que lanzarse a reescribir alegremente. La siguiente a recomendacin es que la nueva API y la vieja convivan durante un tiempo, o en lugar de reescribir eliminando la versin legada. Adems de tener dos API o a podemos sobrecargar mtodos para intentar que el cdigo legado y su nueva e o versin convivan, si es que la API antigua nos sigue sirviendo. Viene siendo o cuestin de aplicar el sentido comn y recordar la ley de Murphy; Si puede o u salir mal, saldr mal. Otra alternativa para hacer TDD con cdigo nuevo a o
12

Ver Cap tulo 7 en la pgina 111 a

61

2.2. Consideraciones y recomendaciones

Cap tulo 2

que colabora con cdigo legado es abusar de los objetos mock13 . Digamos o que los tests van a ser ms frgiles de lo que deber pero es mejor usar a a an paraca das que saltar sin nada. Y por supuesto, si el nuevo cdigo es ms o a independiente, podemos seguir haciendo TDD sin ningn problema. Se lo u recomiendo encarecidamente.

13

Los veremos en el Cap tulo 6 en la pgina 95 a

62

Captulo

Desarrollo Dirigido por Tests de Aceptacin (ATDD) o


A pesar de la brevedad de este cap tulo, puede considerarse probablemente el ms importante de todo el libro. Si no somos capaces de entendernos con a el cliente, ni la mejor tcnica de desarrollo de todos los tiempos producir un e a buen resultado. La mayor diferencia entre las metodolog clsicas y la Programacin as a o Extrema es la forma en que se expresan los requisitos de negocio. En lugar de documentos de word, son ejemplos ejecutables. El Desarrollo Dirigido por Test de Aceptacin (ATDD), tcnica conocida tambin como Story Testo e e Driven Development (STDD), es igualmente TDD pero a un nivel diferente. Los tests de aceptacin o de cliente son el criterio escrito de que el softwao re cumple los requisitos de negocio que el cliente demanda. Son ejemplos escritos por los dueos de producto. n Es el punto de partida del desarrollo en cada iteracin, la conexin pero o fecta entre Scrum y XP; all donde una se queda y sigue la otra. a ATDD/STDD es una forma de afrontar la implementacin de una manera o totalmente distinta a las metodolog tradicionales. El trabajo del analista de as negocio se transforma para reemplazar pginas y pginas de requisitos escritos a a en lenguaje natural (nuestro idioma), por ejemplos ejecutables surgidos del consenso entre los distintos miembros del equipo, incluido por supuesto el cliente. No hablo de reemplazar toda la documentacin, sino los requisitos, o los cuales considero un subconjunto de la documentacin. o El algoritmo es el mismo de tres pasos pero son de mayor zancada que en el TDD practicado exclusivamente por desarrolladores. En ATDD la lista de ejemplos (tests) de cada historia, se escribe en una reunin que incluye a o

63

3.1. Las historias de usuario

Cap tulo 3

dueos de producto, desarrolladores y responsables de calidad. Todo el equipo n debe entender qu es lo que hay que hacer y por qu, para concretar el modo e e en que se certica que el sotfware lo hace. Como no hay unica manera de decidir los criterios de aceptacin, los distintos roles del equipo se apoyan o entre s para darles forma.

3.1.

Las historias de usuario

Una historia de usuario posee similitudes con un caso de uso, salvando ciertas distancias. Por hacer una correspondencia entre historias de usuario y casos de uso, podr amos decir que el t tulo de la historia se corresponde con el del caso de uso tradicional. Sin embargo, la historia no pretende denir el requisito. Escribir una denicin formal incurre en el peligro de la imprecisin o o y la malinterpretacin, mientras que contarlo con ejemplos ilustrativos, transo mite la idea sin complicaciones. En ATDD cada historia de usuario contiene una lista de ejemplos que cuentan lo que el cliente quiere, con total claridad y ninguna ambigedad. El enunciado de una historia es tan slo una frase en u o lenguaje humano, de alrededor de cinco palabras, que resume qu es lo que e hay que hacer. Ejemplos de historias podr ser: an
Formulario de inscripcin o Login en el sistema Reservar una habitacin o A~adir un libro al carrito de la compra n Pago con tarjeta de crdito e Anotar un da festivo en el canlendario Informe de los artculos ms vendidos a Darse de baja en el foro Buscar casas de alquiler en Tenerife

Breves, concretas y algo estimables. Son el resultado de escuchar al cliente y ayudarle a resumir el requisito en una sola frase. Muy importante: Estn a escritas con el vocabulario del negocio del cliente, no con vocabulario tcnico. e Por s misma una historia aislada es dif de estimar incluso con este formato. cil Lo que las hace estimables y nos hace ser capaces de estimarlas cada vez mejor, es el proceso evolutivo que llamamos gil. Esto es: a base de iterar, a estimar en cada iteracin y hacer restrospectiva al nal de la misma, vamos o renando la habilidad de escribir historias y estimarlas.

64

Cap tulo 3
'

3.1. Las historias de usuario $

Canales[13] da una gu estupenda para las estimaciones entre las pgia a nas 305 y 319 de su libro. Es ms que recomendable leerlo. Sin embargo, a desde la pgina 320 hasta la 343, discrepo con su forma de afrontar el a anlisis. Antes de conocer ATDD, tambin trabajaba como nos dice en a e esas pginas pero la experiencia me ha enseado que no es la mejor maa n nera. Saltar de casos de uso a crear un diagrama de clases modelando entidades, es en mi opinin, peligroso cuanto menos. Los diagramas nos o pueden ayudar a observar el problema desde una perspectiva global, de manera que nos aproximamos al dominio del cliente de una manera ms a intuitiva. Pueden ayudarnos a comprender el dominio hasta que llegamos a ser capaces de formular ejemplos concretos. En cambio, representar elementos que formarn parte del cdigo fuente mediante diagramas, es a o una fuente de problemas. Traducir diagramas en cdigo fuente, es decir o el modelado, es en cierto modo opuesto a lo que se expone en este libro. Para m la unica utilidad que tiene el UMLa es la de representar me, diante un diagrama de clases, cdigo fuente existente. Es decir, utilizo o herramientas que autogeneran diagramas de clases, a partir de cdigo, o para poder echar un vistazo a las entidades de manera global pero nunca hago un diagrama de clases antes de programar. Mis entidades emergen a base de construir el cdigo conforme a ejemplos. En todos los ejemo plos que aparecen en las citadas pginas, realmente lo que leemos son a descripciones, no son ejemplos potencialmente ejecutables. Denir entidades/modelos y hablar de pantallas antes de que haya una lista de ejemplos ejecutables y cdigo ejecutable que los requiera, es un camino o problemtico. Como artesano del software, no creo en los generadores de a aplicaciones.
a

Cada historia provoca una serie de preguntas acerca de los mltiples u contextos en que se puede dar. Son las que naturalmente hacen los desarrolladores a los analistas de negocio o al cliente. Qu hace el sistema si el libro que se quiere aadir al carrito ya e n est dentro de l? a e Qu sucede si se ha agotado el libro en el almacn? e e Se le indica al usuario que el libro ha sido aadido al carrito de la n compra?

&

Lenguaje de Modelado Universal

65

3.1. Las historias de usuario

Cap tulo 3

Las respuestas a estas preguntas son armaciones, ejemplos, los cuales transformamos en tests de aceptacin. Por tanto, cada historia de usuario tiene o asociados uno o varios tests de aceptacin (ejemplos): o
Cuando el libro X se a~ade al carrito, el sistema devuelve n un mensaje que dice: El libro X ha sido a~adido al carrito n Al mostrar el contenido del carrito aparece el libro X El libro X ya no aparece entre los libros a a~adir al carrito n

Cuantas menos palabras para decir lo mismo, mejor:


A~adir libro X en stock produce: El libro X ha sido a~adido al carrito n n Libro X est contenido en el carrito a Libro X ya no est en catlogo de libros a a

Las preguntas surgidas de una historia de usuario pueden incluso dar lugar a otras historias que pasan a engrosar el backlog o lista de requisitos: Si el libro no est en stock, se enviar un email al usuario cuando llegue. a a Los tests de aceptacin son as armaciones en lenguaje humano que o ; tanto el cliente, como los desarrolladores, como la mquina, entienden. La a mquina? cmo puede entender eso la mquina? Mgicamente no. El equipo a o a a de desarrollo tiene que hacer el esfuerzo de conectar esas frases con los puntos de entrada y salida del cdigo. Para esto existen diversos frameworks libres o y gratuitos que reducen el trabajo. Los ms conocidos son FIT, Fitnesse, a Concordion, Cucumber y Robot. Bsicamente lo que proponen es escribir a las frases con un formato determinado como por ejemplo HTML, usando etiquetas de una manera espec ca para delimitar qu partes de la frase son e variables de entrada para el cdigo y cuales son datos para validacin del o o resultado de la ejecucin. Como salida, Concordion por ejemplo, produce un o HTML modicado que marca en rojo las armaciones que no se cumplieron, adems de mostrar las estad a sticas generales sobre cuntos tests pasaron y a cuntos no. Veamos un ejemplo de la sintaxis de Concordion: a

66

Cap tulo 3
'

3.1. Las historias de usuario $

Lgicamente, no le pedimos al cliente que se aprenda la sintaxis de Cono cordion y escriba el cdigo HTML. Le pedimos que nos ayude a denir la o frase o que nos la valide y luego, entre analistas de negocio, desarrolladores y testers (equipo de calidad), se escribir el HTML. Lo interesante para el a cliente es que el renderizado del HTML contiene el ejemplo que l entiende e y es una bonita tarjeta que Concordion colorear con ayuda de la hoja de a estilos, subrayando en verde o en rojo segn funcione el software. Concordion u sabe dnde buscar la funcin greetingsFor y reconoce que el argumento o o con que la invocar es Manolo. Comparar el resultado de la ejecucin con la a a o frase Hola Manolo! y marcar el test como verde o rojo en funcin de ello. a o Un test de cliente o de aceptacin con estos frameworks, a nivel de cdigo, o o es un enlace entre el ejemplo y el cdigo fuente que lo implementa. El propio o framework se encarga de hacer la pregunta de si las armaciones son ciertas o no. Por tanto, su aspecto dista mucho de un test unitario o de integracin o con un framework xUnit. Para cada test de aceptacin de una historia de usuario, habr un cono a junto de tests unitarios y de integracin de grano ms no que se encargar, o a a primero, de ayudar a disear el software y, segundo, de armar que funciona n como sus creadores quer que funcionase. Por eso ATDD o STDD es el an comienzo del ciclo iterativo a nivel desarrollo, porque partiendo de un test de aceptacin vamos profundizando en la implementacin con sucesivos test o o unitarios hasta darle forma al cdigo que nalmente cumple con el criterio o de aceptacin denido. No empezamos el diseo en torno a una supuesta o n interfaz grca de usuario ni con el diseo de unas tablas de base de datos, a n sino marcando unos criterios de aceptacin que nos ayuden a ir desde el lado o del negocio hasta el lado ms tcnico pero siempre concentrados en lo que a e el cliente demanda, ni ms ni menos. Las ventajas son numerosas. En pria mer lugar, no trabajaremos en funciones que nalmente no se van a usar1 .
1

&

<html xmlns:concordion="http://www.concordion.org/2007/concordion"> <body> <p> El saludo para el usuario <span concordion:set="\#firstName">Manolo</span> ser: a <span concordion:assertEquals="greetingFor(\#firstName)"> Hola Manolo!</span> </p> </body> </html>

El famoso YAGNI (You aint gonna need it)

67

3.2. Qu y no Cmo e o

Cap tulo 3

En segundo lugar, forjaremos un cdigo que est listo para cambiar si fuera o a necesario porque su diseo no est limitado por un diseo de base de datos n a n ni por una interfaz de usuario. Es ms fcil hacer modicaciones cuando se a a ha diseado as de arriba a abajo, en vez de abajo a arriba. Si el arquitecto n , disea mal la estructura de un edicio, ser muy complejo hacerle cambios n a en el futuro pero si pudiera ir montando la cocina y el saln sin necesidad o de estructuras previas, para ir ensendolas al cliente, seguro que al nal na colocar la mejor de las estructuras para darle soporte a lo dems. En la a a construccin de viviendas eso no se puede hacer pero en el software, s Y, o . adems, es lo natural, aunque estemos acostumbrados a lo contrario. Porque a una aplicacin informtica no es una casa!. Dada esta metfora, se podr o a a a interpretar que deber amos partir de una interfaz grca de usuario para la a implementacin pero no es cierto. Ver el dibujo de una interfaz grca de o a usuario no es como ver una cocina. Primero, porque la interfaz grca puede a o no ser intuitiva, utilizable y, a consecuencia de esto, en segundo lugar, no es el medio adecuado para expresar qu es lo que el cliente necesita sino que e la interfaz de usuario es parte del cmo se usa. o

3.2.

Qu y no Cmo e o

Una de las claves de ATDD es justamente que nos permite centrarnos en el qu y no en el cmo. Aprovechamos los frameworks tipo Concordion para e o desarrollar nuestra habilidad de preguntar al cliente qu quiere y no cmo lo e o quiere. Evitamos a toda costa ejemplos que se meten en el cmo hacer, ms o a all del qu hacer: a e
Al rellenar el cuadro de texto de buscar y pulsar el botn contiguo, o los resultados aparecen en la tabla de la derecha Al introducir la fecha y hacer click en el botn de a~adir, o n se crea un nuevo registro vaco Los libros se almacenan en la tabla Libro con campos: id, titulo y autor Seleccionar la opcin de borrar del combo, marcar con un tick o las lneas a borrar y verificar que se eliminan de la tabla al pulsar el botn aplicar. o Aplicacin Flash con destornilladores y tornillos girando o en 3D para vender artculos de mi ferretera por Internet

Cuando partimos de especicaciones como estas corremos el riesgo de pasar por alto el verdadero propsito de la aplicacin, la informacin con autntico o o o e 68

Cap tulo 3

3.2. Qu y no Cmo e o

valor para el negocio del cliente. Salvo casos muy justicados, el Dueo del n Producto no debe decir cmo se implementa su solucin, igual que no le o o decimos al fontanero cmo tiene que colocar una tuber La mayor de las o a. a veces, el usuario no sabe exactamente lo que quiere pero, cuando le sugerimos ejemplos sin ambigedad ni deniciones, generalmente sabe decirnos si es o u no es eso lo que busca. Uno de los motivos por los que el cliente se empea en n pedir la solucin de una determinada manera es porque se ha encontrado con o profesionales poco experimentados que no le han sabido sugerir las formas adecuadas o que no llegaron a aportarle valor para su negocio. Con ATDD nos convertimos un poco en psiclogos en lugar de pretender ser adivinos. o A base de colaboracin encontramos y clasicamos la informacin que ms o o a benecio genera para el usuario. Encuentro particularmente dif practicar ATDD cuando los dueos de cil n producto estn mal acostumbrados al sistema clsico en el que el anlisis a a a de los requisitos acaba produciendo un diagrama de componentes o mdulos o y luego un diagrama de clases. En las primeras reuniones de anlisis, se a empean en que dibujemos ese diagrama de mdulos en los que el sistema n o se va a dividir a pesar de que les explique que eso no aporta ms valor a su a negocio. Les digo que la abstraccin de los requisitos en forma de mdulos o o o grupos no sirve ms que para contaminar el software con falsos requisitos de a negocio y para limitarnos a la hora de implementar, aunque a veces les resulta dif de ver en un principio. Los unicos mdulos que hay que identicar son cil o los que tienen valor de negocio, es decir, aquellos conjuntos lgicos que o tengan relacin con una estrategia de negocio. Por ejemplo, de cara a ofrecer o determinados servicios: servicio de venta, de alquiler, de consultor a... La forma en que comprenden el proceso iterativo, es sentndose frente a a ellos en un lugar cmodo y adoptando el rol de psiclogo de las pel o o culas norteamericanas, abordando los ejemplos. Una vez llevo la voz cantante, empiezo a formular ejemplos para que me digan si son vlidos o no. Al principio a no son capaces de distinguir entre una descripcin y un ejemplo preciso por o lo que se apresuran a darme descripciones que consideran sucientes como para implementar el software pero que para m ajeno a su negocio, no lo son: ,
Buscando por Santa Cruz de Tenerife, aparece una lista de pisos en alquiler.

Entonces reconduzco la conversacin hacindoles ver que su descripcin o e o se corresponde en realidad con varios ejemplos.

69

3.3. Est hecho o no? a

Cap tulo 3

Buscando que el precio sea inferior a 600e, e introduciendo el texto "Santa Cruz de Tenerife", el sistema muestra una lista de pisos que no superan los 600emensuales de alquiler y que se encuentran en la ciudad de Santa Cruz de Tenerife Buscando que el precio est entre 500ey 700ey e que tenga 2 habitaciones e introduciendo el texto "Santa Cruz de Tenerife", el sistema muestra una lista de pisos que cumplen las tres condiciones Buscando que tenga 3 habitaciones y 2 cuartos de ba~o, e n introduciendo el texto "Santa Cruz de Tenerife", el sistema muestra una lista de pisos que cumplen las tres condiciones Buscando con el texto "Tenerife", el sistema muestra la lista de pisos de toda la provincia de Santa Cruz de Tenerife En la lista, cada piso se muestra mediante una fotografa y el nmero de habitaciones que tiene u

Para responder si los ejemplos son verdaderos o falsos, ellos mismos descubren dudas sobre lo que necesitan para su negocio. Dejan de ir teniendo pensamientos mgicos para ser conscientes de la precisin con que tenemos a o que denir el funcionamiento del sistema. A partir de ese momento, entienden que la distancia entre los expertos en desarrollo y los expertos en negocio va menguando y dejan de preocuparse por diagramas abstractos. Entonces dicen... Tenemos que pensar todas estas cosas? Y tengo que contarles que, aunque los ordenadores hayan avanzado mucho, no dejan de ser mquia nas muy tontas. Les cuento que si esas decisiones sobre el negocio no me las validan ellos, tendr que decidir yo, que no soy experto en su negocio. e As comienzan a involucrarse ms en el desarrollo y todos comenzamos a a hablar el mismo idioma. Al nal, todo esto no consiste en otra cosa que en escribir ejemplos e implementarlos.

3.3.

Est hecho o no? a

Otra ventaja de dirigir el desarrollo por las historias y, a su vez, por los ejemplos, es que vamos a poder comprobar muy rpido si el programa a est cumpliendo los objetivos o no. Conocemos en qu punto estamos y cmo a e o vamos progresando. El Dueo de Producto puede revisar los tests de acepn tacin y ver cuntos se estn cumpliendo, as que nuestro trabajo gana una o a a 70

Cap tulo 3

3.4. El contexto es esencial

conanza tremenda. Es una gran manera de fomentar una estrecha colaboracin entre todos los roles el equipo. Pinselo bien: la propia mquina es o e a capaz de decirnos si el programa cumple las especicaciones el cliente o no! De cara a hacer modicaciones en nuevas versiones del programa y lanzarlo a produccin, el tiempo que tardamos en efectuar las pruebas de regresin o o disminuye de manera drstica, lo cual se traduce en un ahorro considerable. a
'

3.4.

&

Los tests de regresin deben su nombre al momento en que se o ejecutan, no a su formato ni a otras caracter sticas. Antes de lanzar una nueva versin de un producto en produccin, ejecutamos o o todas las pruebas posibles, tanto manuales como automticas paa ra corroborar que tanto las nuevas funciones como las existentes funcionan. Regresin viene de regresar, puesto que regresamos a o funcionalidad desarrollada en la versin anterior para validar que o no se ha roto nada. Cuando no se dispone de una completa bater de tests, la regresin completa de una aplicacin puede llevar a o o varios d en los que el equipo de calidad ejecuta cada parte de as la aplicacin con todas y cada una de sus posibles variantes. Hao blando en plata; una tortura y un gasto econmico importante. o

El contexto es esencial

Fuera del contexto gil, ATDD tiene pocas probabilidades de xito ya que a e si los analistas no trabajan estrechamente con los desarrolladores y testers, no se podr originar un ujo de comunicacin sucientemente rico como para a o que las preguntas y respuestas aporten valor al negocio. Si en lugar de ejemplos, se siguen escribiendo descripciones, estaremos aumentando la cantidad de trabajo considerablemente con lo cual el aumento de coste puede no retornar la inversin. Si los dueos de producto (cliente y analistas) no tienen o n tiempo para denir los tests de aceptacin, no tiene sentido encargrselos o a a los desarrolladores, ser malgastar el dinero. Tener tiempo es un asunto a muy relativo y muy delicado. No entraremos en ese tema tan escabroso al que yo llamar ms bien estar dispuestos a invertir el tiempo, ms que tener a a a o no tener tiempo. Alguien tiene tiempo?. La herramienta con la que se escriben los tests de aceptacin tiene que minimizar la cantidad de cdigo o o que requiere esa conexin entre las frases y el cdigo del sistema, si no, el o o mantenimiento se encarecer demasiado. ATDD/STDD es un engranaje que a cuesta poner en marcha pero que da sus frutos, como se puede leer en este art culo de la revista Better Software de 20042 .
2

http://industriallogic.com/papers/storytest.pdf

71

3.4. El contexto es esencial

Cap tulo 3

Desgraciadamente no podemos extendernos ms con respecto a ATDa D/STDD, si bien se podr escribir un libro sobre ello. Mike Cohn escria bi uno muy popular titulado User Stories Applied[5] que le recomiendo eno carecidamente leer. Mencin especial tambin al cap o e tulo sobre ATDD de Lasse Koskela en Test Driven[9] y los sucesivos, que incluyen ejemplos sobre el framework FIT. Gojko Adzic[1] tiene un libro basado en FitNesse y por supuesto cabe destacar su famoso libro sobre Acceptance Testing[2]. Elisabeth Hendrickson, en colaboracin con otros expertos de la talla de Brian Marick, o public un paper que puede leerse online3 e incluye ejemplos en el framework o Robot. En la parte prctica de este libro tendremos ocasin de ver algunos ejema o plos ms aunque, por motivos de espacio, no es exhaustiva. a

http://testobsessed.com/wordpress/wp-content/uploads/2008/12/atddexample.pdf

72

Captulo

Tipos de test y su importancia


La nomenclatura sobre tests puede ser francamente catica. Ha sido fueno te de discusin en los ultimos aos y sigue sin existir universalmente de mao n nera categrica. Cada equipo tiende a adoptar sus propias convenciones, ya o que existen distintos aspectos a considerar para denominar tests. Por aspecto quiero decir que, segn cmo se mire, el test se puede clasicar de una mau o nera o de otra. As se habla, por ejemplo, del aspecto visibilidad (si se sabe lo que hay dentro del SUT o no), del aspecto potestad (a quin pertenece e el test), etc. Dale H. Emery hace una extensa recopilacin de los posibles o aspectos o puntos de vista y los tipos que alberga cada uno de ellos1 . No es la unica que existe, ni sus deniciones son extensas, pero nos da una idea de la complejidad del problema. Michael Feathers tambin enumera varias clae 2 . Por su parte, la Wikipedia aporta sicaciones de tests en un post reciente otro punto de vista complementario a los anteriores3 . Denitivamente, cada comunidad usa unos trminos diferentes. Una coe munidad podr ser la de los que practicamos TDD, y otra podr ser la de a a los que escriben tests para aplicaciones ya implementadas que todav no a los tienen, por ejemplo. Un mismo test puede ser de varios tipos, incluso mirndolo desde la perspectiva de un solo equipo de desarrollo ya que su claa sicacin depender del aspecto a considerar. No existen reglas universales o a para escribir todos y cada uno de los tipos de tests ni sus posibles combinaciones. Es una cuestin que llega a ser artesanal. Sin embargo, ms all de o a a los trminos, es conveniente que tengamos una idea de cmo es cada tipo de e o test, segn las convenciones que hayamos elegido, para ser coherentes a la u hora de escribirlos. Cada vez que vamos a programar un test, tenemos que
1 2

http://cwd.dhemery.com/2004/04/dimensions/ http://blog.objectmentor.com/articles/2009/04/13/x-tests-are-not-x-tests 3 http://en.wikipedia.org/wiki/Software testing

73

4.1. Terminolog en la comunidad TDD a

Cap tulo 4

estar seguros de por qu lo escribimos y de qu estamos probando. Es extree e madamente importante tener claro qu queremos armar con cada test y por e qu lo hacemos de esa manera. Podr ser que el hecho de no saber determie a nar qu tipo de test vamos a programar, indique que no estamos seguros de e por qu lo escribimos. Dicho de otro modo, si no conseguimos darle nombre e a lo que hacemos, quizs no sepamos por qu lo hacemos. Probablemente no a e tengamos claro el diseo que queremos, o puede que el test no est probando n e lo que debe, o que no estemos delimitando responsabilidades adecuadamente, o quizs estemos escribiendo ms tests de los que son necesarios. Es una a a heur stica a tener en cuenta. En el mbito de TDD no hablamos de tests desde el aspecto visibilidad a (t picamente tests de caja blanca y caja negra). Usamos otros trminos, pero e sabemos que un test de caja negra podr coincidir con un test unitario basado a en el estado. Y un test de caja blanca podr coincidir con un test unitario a basado en la interaccin. No es una regla exacta porque, dependiendo de o cmo se haya escrito el test, puede que no sea unitario sino de integracin. o o Lo importante es ser capaces de entender la naturaleza de los tests. En la siguiente seccin veremos los trminos ms comunes dentro de la o e a comunidad TDD y sus signicados.

4.1.

Terminolog en la comunidad TDD a

Desde el aspecto potestad, es decir, mirando los tests segn a quin u e le pertenecen, distinguimos entre tests escritos por desarrolladores y tests escritos por el Dueo del Producto. Recordemos que el Dueo del Producto n n es el analista de negocio o bien el propio cliente. Lo ideal es que el analista de negocio ayude al cliente a escribir los tests para asegurarse de que las armaciones estn totalmente libres de ambigedad. Los tests que pertenecen a u al Dueo del Producto se llaman tests de cliente o de aceptacin. Charlie n o Poole4 preere llamarles tests de cliente ya que por aceptacin se podr o a entender que se escriben al nal cuando, realmente, no tiene que ser as . De hecho, en TDD partimos de tests de aceptacin (ATDD) para conectar o requerimientos con implementacin, o sea, que los escribimos antes que nada. o Cuando se escribe el cdigo que permite ejecutar este test, y se ejecuta o positivamente, se entiende que el cliente acepta el resultado. Por esto se habla de aceptacin. Y tambin por esto es un trmino provocador, al haber o e e clientes que niegan que un test de aceptacin positivo signique que aceptan o esa parte del producto. Nosotros hablaremos de aceptacin porque se usa ms o a
Uno de los padres de NUnit, con ms de 30 aos de experiencia: a n http://www.charliepoole.org/cp.php
4

74

Cap tulo 4

4.1. Terminolog en la comunidad TDD a

en la literatura que test de cliente, aunque convendr recordar lo peligrosa a que puede llegar a ser esta denominacin. o En el siguiente diagrama se muestra la clasicacin de los tests t o pica de un entorno ATDD/TDD. A la izquierda, se agrupan los tests que pertenecen a desarrolladores y, a la derecha, los que pertenecen al Dueo del Producto. n A su vez, algunos tipos de tests contienen a otros.

Desarrollo

Dueo de Producto

Tests Unitarios

Tests de Aceptacin/Cliente

Tests Funcionales

Tests de Integracin

Tests de Sistema

4.1.1.

Tests de Aceptacin o

Cmo es un test de aceptacin? Es un test que permite comprobar que o o el software cumple con un requisito de negocio. Como se vio en el cap tulo de ATDD, un test de aceptacin es un ejemplo escrito con el lenguaje del o cliente pero que puede ser ejecutado por la mquina. Recordemos algunos a ejemplos:

75

4.1. Terminolog en la comunidad TDD a

Cap tulo 4

El producto X con precio 50etiene un precio final de 55edespus de aplicar el impuesto Z e Si el paciente naci el 1 de junio de 1981, o su edad es de 28 a~os en agosto de 2009 n

Los tests de aceptacin no usan la interfaz de usuario del programa? o Podr ser que s pero en la mayor de los casos la respuesta debe ser no. a , a Los tests de carga y de rendimiento son de aceptacin cuando el cliente los o considera requisitos de negocio. Si el cliente no los requiere, sern tests de a desarrollo.

4.1.2.

Tests Funcionales

Todos los tests son en realidad funcionales, puesto que todos ejercitan alguna funcin del SUT5 , aunque en el nivel ms elemental sea un mtodo de o a e una clase. No obstante, cuando se habla del aspecto funcional, se distingue entre test funcional y test no funcional. Un test funcional es un subconjunto de los tests de aceptacin. Es decir, comprueban alguna funcionalidad con o valor de negocio. Hasta ahora, todos los tests de aceptacin que hemos viso to son tests funcionales. Los tests de aceptacin tienen un mbito mayor o a porque hay requerimientos de negocio que hablan de tiempos de respuesta, capacidad de carga de la aplicacin, etc; cuestiones que van ms all de la o a a funcionalidad. Un test funcional es un test de aceptacin pero, uno de acepo tacin, no tiene por qu ser funcional. o e

4.1.3.

Tests de Sistema

Es el mayor de los tests de integracin, ya que integra varias partes del o sistema. Se trata de un test que puede ir, incluso, de extremo a extremo de la aplicacin o del sistema. Se habla de sistema porque es un trmino ms o e a general que aplicacin, pero no se reere a administracin de sistemas, no o o es que estemos probando el servidor web o el servidor SMTP aunque, tales servicios, podr ser una parte de nuestro sistema. As pues, un test del an sistema se ejercita tal cual lo har el usuario humano, usando los mismos a puntos de entrada (aqu s es la interfaz grca) y llegando a modicar la a base de datos o lo que haya en el otro extremo. Cmo se puede automao tizar el uso de la interfaz de usuario y validar que funciona? Hay software
5

Subject Under Test; el cdigo que estamos probando o

76

Cap tulo 4

4.1. Terminolog en la comunidad TDD a

que permite hacerlo. Por ejemplo, si la interfaz de usuario es web, el plugin Selenium6 para el navegador Mozilla Firefox7 nos permite registrar nuestra actividad en una pgina web como si estuviramos grabando un v a e deo para luego reproducir la secuencia automticamente y detectar cambios en la a respuesta del sitio web. Pongamos que grabo la forma en que relleno un formulario con una direccin de correo electrnico incorrecta para que el sitio o o web me env un mensaje de error. Cada vez que quiera volver a comprobar e que el sitio web responde igual ante esa entrada, slo tengo que ejecutar el o test generado por Selenium. Hay herramientas que permiten hacer lo mismo mediante programacin: nos dan una API para seleccionar controles grcos, o a y accionarlos desde cdigo fuente, comprobando el estado de la ejecucin o o con sentencias condicionales o asertivas. El propio Selenium lo permite. Una de las herramientas ms populares es Watir8 para Ruby y sus versiones para a otros lenguajes de programacin (Watin para .Net). Para aplicaciones eso critas con el framework Django (Python), se utiliza el cliente web9 . Para aplicaciones de escritorio, hay frameworks espec cos como UIAutomation10 o NUnitForms11 que tambin permiten manipular la interfaz grca desde e a cdigo. o Existen muchas formas de probar un sistema. Supongamos que hemos implementado un servidor web ligero y queremos validar que, cada vez que alguien accede a una pgina, se registra su direccin ip en un chero de a o registro (log). Podr amos hacer un script con algn comando que se conecte u a una URL del servidor, al estilo de Wget12 desde la misma mquina y despus a e buscar la ip 127.0.0.1 en el chero de log con Grep13 . Sirva el ejemplo para recalcar que no hay una sola herramienta ni forma de escribir tests de sistema, ms bien depende de cada sistema. a Los tests de sistema son muy frgiles en el sentido de que cualquier cama bio en cualquiera de las partes que componen el sistema, puede romperlos. No es recomendable escribir un gran nmero de ellos por su fragilidad. Si la u cobertura de otros tipos de tests de granularidad ms na, como por ejemplo a los unitarios, es amplia, la probabilidad de que los errores slo se detecten o con tests de sistema es muy baja. O sea, que si hemos ido haciendo TDD, no es productivo escribir tests de sistema para todas las posibles formas de uso del sistema, ya que esta redundancia se traduce en un aumento del costo
http://seleniumhq.org/projects/ide/ http://www.mozilla-europe.org/es/refox/ 8 http://wtr.rubyforge.org/ 9 http://www.djangoproject.com/documentation/models/test client/ 10 http://msdn.microsoft.com/en-us/library/ms747327.aspx 11 http://nunitforms.sourceforge.net/ 12 http://es.wikipedia.org/wiki/GNU Wget 13 http://es.wikipedia.org/wiki/Grep
7 6

77

4.1. Terminolog en la comunidad TDD a

Cap tulo 4

de mantenimiento de los tests. Por el contrario, si no tenemos escrito absolutamente ningn tipo de test, blindar la aplicacin con tests de sistema u o ser el paso ms recomendable antes de hacer modicaciones en el cdigo a a o fuente. Luego, cuando ya hubiesen tests unitarios para los nuevos cambios introducidos, se podr ir desechando tests de sistema. an Por qu se les llama tests de aceptacin y tests funcionales a los tests e o de sistema? En mi opinin, es un error. Un test funcional es una frase escrita o en lenguaje natural que utiliza el sistema para ejecutarse. En el caso de probar que una direccin de email es incorrecta, el test utilizar la parte del o a sistema que valida emails y devuelve mensajes de respuesta. Sin embargo, el requisito de negocio no debe entrar en cmo es el diseo de la interfaz de o n usuario. Por tanto, el test funcional no entrar a ejecutar el sistema desde a el extremo de entrada que usa el usuario (la GUI), sino desde el que necesita para validar el requisito funcional. Si la mayor de los criterios de aceptacin a o se validan mediante tests funcionales, tan slo nos harn falta unos pocos o a tests de sistema para comprobar que la GUI est bien conectada a la lgica a o de negocio. Esto har que nuestros tests sean menos frgiles y estaremos a a alcanzando el mismo nivel de cobertura de posibles errores. En la documentacin de algunos frameworks, llaman test unitarios a tests o que en verdad son de integracin y, llaman tests funcionales, a tests que son o de sistema. Llamar test funcional a un test de sistema no es un problema siempre que adoptemos esa convencin en todo el equipo y todo el mundo o sepa para qu es cada test. e En casos puntuales, un requisito de negocio podr involucrar la GUI, tal a como pasa con el cliente de Gmail del iPhone por ejemplo. Est claro que el a negocio en ese proyecto est directamente relacionado con la propia GUI. En a ese caso, el test de sistema ser tambin un test funcional. a e

4.1.4.

Tests Unitarios

Son los tests ms importantes para el practicante TDD, los ineludibles. a Cada test unitario o test unidad (unit test en ingls) es un paso que andamos e en el camino de la implementacin del software. Todo test unitario debe ser: o Atmico o Independiente Inocuo Rpido a

78

Cap tulo 4

4.1. Terminolog en la comunidad TDD a

Si no cumple estas premisas entonces no es un test unitario, aunque se ejecute con una herramienta tipo xUnit. Atmico signica que el test prueba la m o nima cantidad de funcionalidad posible. Esto es, probar un solo comportamiento de un mtodo de una a e clase. El mismo mtodo puede presentar distintas respuestas ante distintas e entradas o distinto contexto. El test unitario se ocupar exclusivamente de a uno de esos comportamientos, es decir, de un unico camino de ejecucin. A o veces, la llamada al mtodo provoca que internamente se invoque a otros e mtodos; cuando esto ocurre, decimos que el test tiene menor granularidad, e o que es menos no. Lo ideal es que los tests unitarios ataquen a mtodos lo e ms planos posibles, es decir, que prueben lo que es indivisible. La razn es a o que un test atmico nos evita tener que usar el depurador para encontrar un o defecto en el SUT, puesto que su causa ser muy evidente. Como veremos a en la parte prctica, hay veces que vale la pena ser menos estrictos con la a atomicidad del test, para evitar abusar de los dobles de prueba. Independiente signica que un test no puede depender de otros para producir un resultado satisfactorio. No puede ser parte de una secuencia de tests que se deba ejecutar en un determinado orden. Debe funcionar siempre igual independientemente de que se ejecuten otros tests o no. Inocuo signica que no altera el estado del sistema. Al ejecutarlo una vez, produce exactamente el mismo resultado que al ejecutarlo veinte veces. No altera la base de datos, ni env emails ni crea cheros, ni los borra. Es a como si no se hubiera ejecutado. Rpido tiene que ser porque ejecutamos un gran nmero de tests caa u da pocos minutos y se ha demostrado que tener que esperar unos cuantos segundos cada rato, resulta muy improductivo. Un slo test tendr que ejeo a cutarse en una pequea fraccin de segundo. La rapidez es tan importante n o que Kent Beck ha desarrollado recientemente una herramienta que ejecuta los tests desde el IDE Eclipse mientras escribimos cdigo, para evitar dejar o de trabajar en cdigo mientras esperamos por el resultado de la ejecucin. o o Se llama JUnit Max14 . Olof Bjarnason ha escrito otra similar y libre para Python15 Para conseguir cumplir estos requisitos, un test unitario aisla la parte del SUT que necesita ejercitar de tal manera que el resto est inactivo durante a la ejecucin. Hay principalmente dos formas de validar el resultado de la o ejecucin del test: validacin del estado y validacin de la interaccin, o o o o o del comportamiento. En los siguientes cap tulos los veremos en detalle con ejemplos de cdigo. o
14 15

http://www.junitmax.com/junitmax/subscribe.html https://code.launchpad.net/ objarni/+junk/pytddmon

79

4.1. Terminolog en la comunidad TDD a

Cap tulo 4

Los desarrolladores utilizamos los tests unitarios para asegurarnos de que el cdigo funciona como esperamos que funcione, al igual que el cliente usa o los tests de cliente para asegurarse que los requisitos de negocio se alcancen como se espera que lo hagan. F.I.R.S.T Como los acrnimos no dejan de estar de moda, cabe destacar que las cao racter sticas de los tests unitarios tambin se agrupan bajo las siglas F.I.R.S.T e que vienen de: Fast, Independent, Repeatable, Small y Transparent. Repetible encaja con inocuo, pequeo caza con atmico y transparente quiere decir que n o el test debe comunicar perfectamente la intencin del autor. o

4.1.5.

Tests de Integracin o

Por ultimo, los tests de integracin son la pieza del puzzle que nos fal o taba para cubrir el hueco entre los tests unitarios y los de sistema. Los tests de integracin se pueden ver como tests de sistema pequeos. T o n picamente, tambin se escriben usando herramientas xUnit y tienen un aspecto paree cido a los tests unitarios, slo que estos pueden romper las reglas. Como o su nombre indica, integracin signica que ayuda a unir distintas partes del o sistema. Un test de integracin puede escribir y leer de base de datos para o comprobar que, efectivamente, la lgica de negocio entiende datos reales. Es o el complemento a los tests unitarios, donde hab amos falseado el acceso a datos para limitarnos a trabajar con la lgica de manera aislada. Un test de o integracin podr ser aquel que ejecuta la capa de negocio y despus cono a e sulta la base de datos para armar que todo el proceso, desde negocio hacia abajo, fue bien. Son, por tanto, de granularidad ms gruesa y ms frgiles a a a que los tests unitarios, con lo que el nmero de tests de integracin tiende u o a ser menor que el nmero de tests unitarios. Una vez que se ha probado u que dos mdulos, objetos o capas se integran bien, no es necesario repetir el o test para otra variante de la lgica de negocio; para eso habrn varios tests o a unitarios. Aunque los tests de integracin pueden saltarse las reglas, por moo tivos de productividad es conveniente que traten de ser inocuos y rpidos. a Si tiene que acceder a base de datos, es conveniente que luego la deje como estaba. Por eso, algunos frameworks para Ruby y Python entre otros, tienen la capacidad de crear una base de datos temporal antes de ejecutar la bater de tests, que se destruye al terminar las pruebas. Como se trata de a una herramienta incorporada, tambin hay quien ejecuta los tests unitarios e creando y destruyendo bases de datos temporales pero es una prctica que a debe evitarse porque los segundos extra que se necesitan para eso nos hacen

80

Cap tulo 4

4.1. Terminolog en la comunidad TDD a

perder concentracin. Los tests unitarios deben pertenecer a suites16 difereno tes a los de integracin para poderlos ejecutar por separado. En los prximos o o cap tulos tendremos ocasin de ver tests de integracin en detalle. o o Concluimos el cap tulo sin revisar otros tipos de tests, porque este no es un libro sobre cmo hacer pruebas de software exclusivamente sino sobre cmo o o construir software basndonos en ejemplos que ilustran los requerimientos del a negocio sin ambigedad. Los tests unitarios, de integracin y de aceptacin u o o son los ms importantes dentro del desarrollo dirigido por tests. a

16

Una suite es una agrupacin de tests o

81

4.1. Terminolog en la comunidad TDD a

Cap tulo 4

82

Captulo

Tests unitarios y frameworks xUnit


En cap tulos previos hemos citado xUnit repetidamente pero xUnit como tal no es ninguna herramienta en s misma. La letra x es tan slo un prejo o a modo de comod para referirnos de manera genrica a los numerosos fran e meworks basados en el original SUnit. SUnit fue creado por Kent Beck para la plataforma SmallTalk y se ha portado a una gran variedad de lenguajes y plataformas como Java (JUnit), .Net (NUnit), Python (PyUnit), Ruby (Rubyunit), Perl (PerlUnit), C++ (CppUnit), etc. Si aprendemos a trabajar con NUnit y PyUnit como veremos en este libro, sabremos hacerlo con cualquier otro framework tipo xUnit porque la losof es siempre la misma. Adems a a en Java, desde la versin 4 de JUnit, se soportan las anotaciones por lo que o NUnit y JUnit se parecen todav ms. a a Una clase que contiene tests se llama test case (conjunto de tests) y para denirla en cdigo heredamos de la clase base TestCase del framework o correspondiente (con JUnit 3 y Pyunit) o bien la marcamos con un atributo especial (JUnit 4 y NUnit). Los mtodos de dicha clase pueden ser tests o no. e Si lo son, sern tests unitarios o de integracin, aunque podr ser incluso de a o an sistema. El framework no distingue el tipo de test que es, los ejecuta a todos por igual. Quienes debemos hacer la distincin somos nosotros mismos, una o vez que tenemos claro qu queremos probar y por qu lo hacemos con un e e framework xUnit. En este cap tulo todos los tests son unitarios. En prximos cap o tulos veremos cmo escribir tambin tests de integrao e cin. Para etiquetar los mtodos como tests, en Java y .Net usamos anotacioo e nes y atributos respectivamente. En Python se hace precediendo al nombre del mtodo con el prejo test (ej: def test letter A isnot a number), e igual que pasaba con la versin 3 de JUnit. Los mtodos que no estn marcao e a 83

5.1. Las tres partes del test: AAA

Cap tulo 5

dos como tests, se utilizan para unicar cdigo requerido por ellos. Es normal o que haya cdigo comn para preparar los tests o para limpiar los restos su ejeo u cucin, por lo que xUnit provee una manera sencilla de organizar y compartir o este cdigo: los mtodos especiales SetUp y TearDown. SetUp suele destio e narse a crear variables, a denir el escenario adecuado para despus llamar e al SUT y TearDown a eliminar posibles objetos que no elimine el recolector de basura.

5.1.

Las tres partes del test: AAA

Un test tiene tres partes, que se identican con las siglas AAA en ingls: e Arrange (Preparar), Act (Actuar), Assert (Armar). Una parte de la preparacin puede estar contenida en el mtodo SetUp, si o e es comn a todos los tests de la clase. Si la estapa de preparacin es comn u o u a varios tests de la clase pero no a todos, entonces podemos denir otro mtodo o funcin en la misma clase, que ane tal cdigo. No le pondremos e o u o la etiqueta de test sino que lo invocaremos desde cada punto en que lo necesitemos. El acto consiste en hacer la llamada al cdigo que queremos probar (SUT) o y la armacin o armaciones se hacen sobre el resultado de la ejecucin, bien o o mediante validacin del estado o bien mediante validacin de la interaccin. o o o Se arma que nuestras espectativas sobre el resultado se cumplen. Si no se cumplen el framework marcar en rojo cada falsa expectativa. a Veamos varios ejemplos en lenguaje C# con el framework NUnit. Tendremos que crear un nuevo proyecto de tipo librer con el IDE (VisualStudio, a SharpDevelop, MonoDevelop...) e incorporar la DLL1 nunit.framework a las referencias para disponer de su funcionalidad. Lo ideal es tener el SUT en una DLL y sus tests en otra. As en el proyecto que contiene los tests, tambin e se incluye la referencia a la DLL o ejecutable (.exe) del SUT, adems de a a NUnit. Estos ejemplos realizan la validacin a travs del estado. El estado o e del que hablamos es el de algn tipo de variable, no hablamos del estado del u sistema. Recordemos que un test unitario no puede modicar el estado del sistema y que todos los tests de este cap tulo son unitarios.

1 2 3 4 5 6 7 8

using System ; using NUnit . Framework ; namespace EjemplosNUnit { [ TestFixture ] public class NameNormalizerTests {
1

Librer de enlace dinmico. Equivalente en .Net a los .jar de Java a a

84

Cap tulo 5

5.1. Las tres partes del test: AAA

9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

[ Test ] public void F i r s t L e t t e r U p p e r C a s e () { // Arrange string name = " pablo rodriguez " ; NameNormalizer normalizer = new NameNormalizer (); // Act string result = normalizer . F i r s t L e t t e r U p p e r C a s e ( name ); // Assert Assert . AreEqual ( " Pablo Rodriguez " , result ); } } }

Hemos indicado a NUnit que la clase es un conjunto de tests (test case), utilizando para ello el atributo TestFixture. El que la palabra xture aparezca aqu puede ser desconcertante, ser ms claro si el atributo se llamase , a a TestCase.
'

El trmino Fixture se utiliza en realidad para hablar de los datos e de contexto de los tests. Los datos de contexto o xtures son aquellos que se necesitan para construir el escenario que require el test. En el ejemplo de arriba la variable name es una variable de contexto o xture. Los datos de contexto no son exclusivamente variables sino que tambin pueden ser datos obtenidos de algn e u sistema de almacenamiento persistente. Es comn que los tests u de integracin dependan de datos que tienen que existir en la base o de datos. Estos datos que son un requisito previo, son igualmente datos de contexto o xtures. De hecho se habla ms de xtures a cuando son datos que cuando son variables. Algunos frameworks de tests como el de Django (que se basa en PyUnit) permiten denir conjuntos de datos de contexto mediante JSONa que son automticamente insertados en la base de datos antes de cada a test y borrados al terminar. As aunque los desarrolladores de , NUnit decidieran llamar TestFixture al atributo que etiqueta un conjunto de tests, no debemos confundirnos con los datos de contexto. Charlie Poole comenta que es una buena idea agrupar tests dentro de un mismo conjunto cuando sus datos de contexto son comunes, por eso optaron por llamarle TestFixture en lugar de TestCase.
a

&

Un diccionario con sintaxis javascript

El nombre que le hemos puesto a la clase describe el conjunto de los tests 85

5.1. Las tres partes del test: AAA

Cap tulo 5

que va a contener. Debemos utilizar conjuntos de tests distintos para probar grupos de funcionalidad distinta o lo que es lo mismo: no se deben incluir todos los tests de toda la aplicacin en un solo conjunto de tests (una sola o clase). En nuestro ejemplo la clase contiene un unico test que est marcado a con el atributo Test. Los tests siempre son de tipo void y sin parmetros a de entrada. El nombre del test es largo porque es autoexplicativo. Es la mejor forma de documentarlo. Poner un comentario de cabecera al test, es un antipatrn porque vamos a terminar con un gran nmero de tests y el o u esfuerzo de mantener todos sus comentarios es muy elevado. De hecho es un error que el comentario no coincida con lo que hace el cdigo y eso pasa o cuando modicamos el cdigo despus de haber escrito el comentario. No o e importa que el nombre del mtodo tenga cincuenta letras, no le hace dao a e n nadie. Si no sabemos cmo resumir lo que hace el test en menos de setenta o letras, entonces lo ms probable es que tampoco sepamos qu test vamos a a e escribir, qu misin cumple. Es una forma de detectar un mal diseo, bien e o n del test o bien del SUT. A veces cuando un cdigo requiere documentacin o o es porque no est lo sucientemente claro. a En el cuerpo del test aparecen sus tres partes delimitadas con comentarios. En la prctica nunca delimitamos con comentarios, aqu est escrito a a meramente con nes docentes. La nalidad del test del ejemplo es comprobar que el mtodo FirstLetterUpperCase de la clase NameNormalizer es e capaz de poner en mayscula la primera letra de cada palabra en una frase. u Es un test de validacin de estado porque hacemos la armacin de que o o funciona basndonos en el estado de una variable. Assert en ingls viene a a e signicar armar. La ultima l nea dice: Arma que la variable result es igual a Pablo Rodriguez. Cuando NUnit ejecute el mtodo, dar positivo si la e a armacin es cierta o negativo si no lo es. Al positivo le llamamos luz verde o porque es el color que emplea la interfaz grca de NUnit o s a mplemente decimos que el test pasa. Al resultado negativo le llamamos luz roja o bien decimos que el test no pasa. Imaginemos que el cdigo del SUT ya est implementado y el test da o a luz verde. Pasemos al siguiente ejemplo recalcando que todav no estamos a practicando TDD, sino s mplemente explicando el funcionamiento de un framework xUnit.

1 2 3 4 5 6 7 8

[ Test ] public void SurnameFirst () { string name = " gonzalo aller " ; NameNormalizer normalizer = new NameNormalizer (); string result = normalizer . SurnameFirst ( name );

86

Cap tulo 5

5.1. Las tres partes del test: AAA

9 10

Assert . AreEqual ( " aller , gonzalo " , result ); }

Es otro test de validacin de estado que creamos dentro del mismo conjunto o de tests porque el SUT es un mtodo de la misma clase que antes. Lo que e el test comprueba es que el mtodo SurnameFirst es capaz de recibir un e nombre completo y devolver el apellido por delante, separado por una coma. Si nos jamos bien vemos que la declaracin de la variable normalizer es o idntica en ambos tests. A n de eliminar cdigo duplicado la movemos hacia e o el SetUp. El conjunto queda de la siguiente manera:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

namespace EjemplosNUnit { [ TestFixture ] public class NameNormalizerTests { NameNormalizer _normalizer ; [ SetUp ] public void SetUp () { _normalizer = new NameNormalizer (); } [ Test ] public void F i r s t L e t t e r U p p e r C a s e () { string name = " pablo rodriguez " ; string result = _normalizer . F i r s t L e t t e r U p p e r C a s e ( name ); Assert . AreEqual ( " Pablo Rodriguez " , result ); } [ Test ] public void SurnameFirst () { string name = " gonzalo aller " ; string result = _normalizer . SurnameFirst ( name ); Assert . AreEqual ( " aller , gonzalo " , result ); } } }

Antes de cada uno de los dos tests el framework invocar al mtodo SetUp a e recordndonos que cada prueba es independiente de las dems. a a Hemos denido normalizer como un miembro privado del conjunto de tests. El guin bajo (underscore) que da comienzo al nombre de la variable, o es una regla de estilo que nos ayuda a identicarla rpidamente como variable a 87

5.1. Las tres partes del test: AAA

Cap tulo 5

de la clase en cualquier parte del cdigo2 . El mtodo SetUp crea una nueva o e instancia de dicha variable asegurndonos que entre la ejecucin de un test a o y la de otro, se destruye y se vuelve a crear, evitndo efectos colaterales. Por a u tanto lo que un test haga con la variable normalizer no afecta a ningn otro. Podr amos haber extraido tambin la variable name de los tests pero e como no se usa nada ms que para alimentar al SUT y no interviene en la a fase de armacin, lo mejor es liquidarla: o

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

namespace EjemplosNUnit { [ TestFixture ] public class NameNormalizerTests { NameNormalizer _normalizer ; [ SetUp ] public void SetUp () { _normalizer = new NameNormalizer (); } [ Test ] public void F i r s t L e t t e r U p p e r C a s e () { string result = _normalizer . F i r s t L e t t e r U p p e r C a s e ( " pablo rodriguez " ); Assert . AreEqual ( " Pablo Rodriguez " , result ); } [ Test ] public void SurnameFirst () { string result = _normalizer . SurnameFirst ( " gonzalo aller " ); Assert . AreEqual ( " aller , gonzalo " , result ); } } }

Nos est quedando un conjunto de tests tan bonito como los muebles que haa cen en el programa de Bricoman de la televisin. No hemos denido mtodo a o e tearDown porque no hay nada que limpiar expl citamente. El recolector de basura es capaz de liberar la memoria que hemos reservado; no hemos dejado ninguna referencia muerta por el camino. La validacin de estado generalmente no tiene mayor complicacin, salvo o o
2 Uncle Bob en Clean Code[11] y Xavier Gost en el Agile Open 2009 me han convencido denitivamente para que deje de utilizar esta regla en mi cdigo pero encuentro o que en el papel ayudar. No aconsejo utilizarla si disponemos de un IDE (pero el libro a no lo es)

88

Cap tulo 5

5.1. Las tres partes del test: AAA

1 2 3 4 5 6 7 8 9 10 11

que la ejecucin del SUT implique cambios en el sistema y tengamos que o evitarlos para respetar las clusulas que denen un test unitario. Veremos a ejemplos en los prximos cap o tulos. Continuemos con la validacin de excepciones. Se considera validacin o o de comportamiento, pues no se valida estado ni tampoco interaccin entre o colaboradores. Supongamos que a cualquiera de las funciones anteriores, pasamos como parmetro una cadena vac Para tal entrada queremos que el a a. SUT lanze una excepcin de tipo EmptyNameException, denida por nuestra o propia aplicacin. Cmo escribimos esta prueba con NUnit? o o
[ Test ] public void ThrowOnEmptyName () { try { _normalizer . SurnameFirst ( " " ); Assert . Fail ( " Exception should be thrown " ); } catch ( EmptyNameException ){} }

1 2 3 4 5 6 7

Cuando un test se ejecuta sin que una excepcin lo aborte, ste pasa, aunque o e no haya ninguna armacin. Es decir, cuando no hay armaciones y ninguo na excepcin interrumpe el test, se considera que funciona. En el ejemplo, o esperamos que al invocar a SurnameFirst, el SUT lanze una excepcin de o tipo concreto. Por eso colocamos un bloque catch, para que el test no se interrumpa. Dentro de ese bloque no hay nada, as que la ejecucin del test o termina. Entonces se considera que el SUT se comporta como deseamos. Si por el contrario la ejecucin del SUT termina y no se ha lanzado la excepo cin esperada, la llamada a Assert.Fail abortar el test expl o a citamente sealando luz roja. Se puede escribir el mismo test ayudndonos de atribun a tos especiales que tanto NUnit como JUnit (en este caso son anotaciones) incluyen.
[ Test ] [ ExpectedException ( " EmptyNameException " , ExpectedMessage = " The name can not be empty " )] public void ThrowOnEmptyName () { _normalizer . SurnameFirst ( " " ); }

El funcionamiento y signicado de los dos ultimos tests es exactamente el mismo. Cuando se quiere probar que se lanza una excepcin, se debe exo presar exactamente cul es el tipo de la excepcin esperada y no capturar a o la excepcin genrica (System.Exception en .Net). Si usamos la excepcin o e o genrica, estaremos escondiendo posibles fallos del SUT, excepciones inespee radas. Adems, en el caso de PyUnit la llamada a fail no slo detiene el a o 89

5.1. Las tres partes del test: AAA

Cap tulo 5

test marcndolo en rojo sino que adems lanza una excepcin, con lo cual a a o el bloque catch la captura y obtenemos un resultado totalmente confuso. Creer amos que el SUT est lanzando la excepcin cuando en realidad no lo a o hace pero no lo advertir amos. Llega el turno de la validacin de interaccin. La validacin de interaccin o o o o es el recurso que usamos cuando no es posible hacer validacin de estado. Es o un tipo de validacin de comportamiento. Es recomendable recurrir a esta o tcnica lo menos posible, porque los tests que validan interaccin necesitan e o conocer cmo funciona por dentro el SUT y por tanto son ms frgiles. o a a La mayor de las veces, se puede validar estado aunque no sea evidente a a simple vista. Quizs tengamos que consultarlo a travs de alguna propiedad a e del SUT en vez de limitarnos a un valor devuelto por una funcin. Si el o mtodo a prueba es de tipo void, no se puede armar sobre el resultado e pero es posible que algn otro miembro de la clase reeje un cambio de u estado. El caso de validacin de interaccin ms comn es el de una colaboracin o o a u o que implica alteraciones en el sistema. Elementos que modican el estado del sistema son por ejemplo las clases que acceden a la base de datos o que env mensajes a travs de un servicio web (u otra comunicacin que salga an e o fuera del dominio de nuestra aplicacin) o que crean datos en un sistema de o cheros. Cuando el SUT debe colaborar con una clase que guarda en base de datos, tenemos que validar que la interaccin entre ambas partes se produce o y al mismo tiempo evitar que realmente se acceda a la base de datos. No queremos probar toda la cadena desde el SUT hacia abajo hasta el sistema de almacenamiento. El test unitario pretende probar exclusivamente el SUT. Tratamos de aislarlo todo lo posible. Luego ya habr un test de integracin a o que se encargue de vericar el acceso a base de datos. Para llevar a cabo este tipo de validaciones es fundamental la inyeccin de dependencias3 . Si los o miembros de la colaboracin no se han denido con la posibilidad de inyectar o uno en el otro, dif cilmente podremos conseguir respetar las reglas de los tests unitarios. Con lenguajes interpretados como Python, es posible pero el cdigo del test termina siendo complejo de entender y mantener, es slo una o o opcin temporal. o Un ejemplo vale ms que las palabras, as que imaginemos un sistema que a gestiona el expediente acadmico de los alumnos de un centro de formacin. e o Hay una funcin que dado el identicador de un alumno (su nmero de o u expediente) y la nota de un examen, actualiza su perl en base de datos. Supongamos que existen los objetos relacionales Student y Score y una clase DataManager capaz de recuperarlos y guardarlos en base de datos. El SUT se llama ScoreManager y su colaborador ser DataManager, que a
3

Ver Cap tulo 7 en la pgina 111 a

90

Cap tulo 5

5.1. Las tres partes del test: AAA

implementa la interfaz IDataManager. Las fases de preparacin y accin las o o sabemos escribir ya:

1 2 3 4 5 6

[ Test ] public void AddStudentScore () { ScoreManager smanager = new ScoreManager (); smanager . AddScore ( " 23145 " , 8.5); }

Pero el mtodo AddScore no devuelve nada y adems estar actualizando la e a a base de datos. Cmo validamos? Lo primero es hacer que el colaborador de o ScoreManager se pueda inyectar:

1 2 3 4 5 6 7 8

[ Test ] public void AddStudentScore () { IDataManager dataManager = new DataManager (); ScoreManager smanager = new ScoreManager ( dataManager ); smanager . AddScore ( " 23145 " , 8.5); }

La validacin de la interaccin se hace con frameworks de objetos mock, como o o muestra el siguiente cap tulo pero para comprender parte de lo que hacen internamente los mocks y resolver este test sin ninguna otra herramienta externa, vamos a implementar la solucin manualmente. Si el SUT va a o invocar a su colaborador para leer de la base de datos, operar y guardar, podemos inyectar una instancia que se haga pasar por buena pero que en verdad no acceda a tal base de datos.

1 2 3 4 5 6 7 8 9 10 11 12 13

public class MockDataManager : IDataManager { public IRelationalObject GetByKey ( string key ) { return new Student (); } public void Save ( IRelationalObject robject ) {} public void Create ( IRelationalObject robject ) {} }

La interfaz IDataManager tiene tres mtodos; uno para obtener un objeto e relacional dada su clave primaria, otro para guardarlo cuando se ha modicado y el ultimo para crearlo en base de datos por primera vez. Actualizamos el test:

1 2 3

[ Test ] public void AddStudentScore () {

91

5.1. Las tres partes del test: AAA

Cap tulo 5

4 5 6 7 8

MockDataManager dataManager = new MockDataManager (); ScoreManager smanager = new ScoreManager ( dataManager ); smanager . AddScore ( " 23145 " , 8.5); }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

Vale, ya no acceder al sistema de almacenamiento porque la clase no ima plementa nada pero cmo validamos que el SUT intenta hacerlo? Al n o y al cabo la misin de nuestro SUT no es meramente operar sino tambin o e coordinar el registro de datos. Tendremos que aadir algunas variables de n estado internas para controlarlo:
public class MockDataManager : IDataManager { private bool _getKeyCalled = false ; private bool _saveCalled = false ; public IRelationalObject GetByKey ( string key ) { _getKeyCalled = true ; return new Student (); } public void Save ( IRelationalObject robject ) { _saveCalled = true ; } public void VerifyCalls () { if (! _saveCalled ) throw Exception ( " Save method was not called " ); if (! _getKeyCalled ) throw Exception ( " GetByKey method was not called " ); } public void Create ( IRelationalObject robject ) {} }

1 2 3 4 5 6 7 8 9 10

Ya podemos hacer armaciones (en este caso vericaciones) sobre el resultado de la ejecucin: o
[ Test ] public void AddStudentScore () { MockDataManager dataManager = new MockDataManager (); ScoreManager smanager = new ScoreManager ( dataManager ); smanager . AddScore ( " 23145 " , 8.5); dataManager . VerifyCalls (); }

No hemos tocado la base de datos y estamos validando que el SUT hace esas dos llamadas a su colaborador. Sin embargo, la solucin propuesta es costosa o 92

Cap tulo 5

5.1. Las tres partes del test: AAA

de implementar y no contiene toda la informacin que necesitamos (cmo o o sabemos que el dato que se salv era el correcto?). En el siguiente cap o tulo veremos una solucin alternativa, los mocks generador por frameworks, que o nos permitir denir armaciones certeras basadas en expectativas con todo a lujo de detalles. Como lectura adicional recomiendo el libro de J.B Rainsberg[16]. Adems a el blog y los videos de las conferencias de este autor son una joya.

93

5.1. Las tres partes del test: AAA

Cap tulo 5

94

Captulo

Mocks y otros dobles de prueba


Antes de decidirnos a usar objetos mock (en adelante mocks) hay que contar hasta diez y pensarlo dos veces. Lo primero, es saber en todo momento qu es lo que vamos a probar y por qu. En las listas de correo a menudo e e la gente pregunta cmo deben usar mocks para un problema determinado y o buena parte de las respuestas concluyen que no necesitan mocks, sino partir su test en varios y/o reescribir una parte del SUT. Los mocks presentan dos inconvenientes fundamentales: El cdigo del test puede llegar a ser dif de leer. o cil El test corre el riesgo de volverse frgil si conoce demasiado bien el a interior del SUT. Frgil signica que un cambio en el SUT, por pequeo que sea, romper el a n a test forzndonos a reescribirlo. La gran ventaja de los mocks es que reducen a drsticamente el nmero de l a u neas de cdigo de los tests de validacin de o o interaccin y evitan que el SUT contenga hacks (apaos) para validar. En o n los tests de validacin de estado, tambin se usan mocks o stubs cuando o e hay que acceder a datos procedentes de un colaborador. Por lo tanto, los mocks (y otros dobles) son imprescindibles para un desarrollo dirigido por tests completo pero, igualmente importante, es saber cundo van a poner en a jaque al test, o sea, cundo debemos evitarlos. a Un mock es un tipo concreto de doble de test. La expresin doble se o usa en el mismo sentido de los actores dobles en las pel culas de accin, ya o que se hace pasar por un colaborador del SUT cuando en realidad no es la entidad que dice ser. Gerard Meszaros describe los distintos tipos de dobles de test en su libro[12] donde, adems, sienta las bases de la nomenclatura. a Martin Fowler public un art o culo que se ha hecho muy popular basado en 95

Cap tulo 6 esta nomenclatura; Los mocks no son stubs, donde habla de los distintos dobles1 . De ah extraemos el siguiente listado de tipos de doble: Dummy: se pasa como argumento pero nunca se usa realmente. Normalmente, los objetos dummy se usan slo para rellenar listas de parmeo a tros. Fake: tiene una implementacin que realmente funciona pero, por lo o general, toma algn atajo o cortocircuito que le hace inapropiado para u produccin (como una base de datos en memoria por ejemplo). o Stub: proporciona respuestas predenidas a llamadas hechas durante los tests, frecuentemente, sin responder en absoluto a cualquier otra cosa fuera de aquello para lo que ha sido programado. Los stubs pueden tambin grabar informacin sobre las llamadas; tal como una pasarela e o de email que recuerda cuntos mensajes envi. a o Mock: objeto preprogramado con expectativas que conforman la especicacin de cmo se espera que se reciban las llamadas. Son ms o o a complejos que los stubs aunque sus diferencias son sutiles. Las veremos a continuacin. o El stub2 es como un mock con menor potencia, un subconjunto de su funcionalidad. Mientras que en el mock podemos denir expectativas con todo lujo de detalles, el stub tan slo devuelve respuestas preprogramadas o a posibles llamadas. Un mock valida comportamiento en la colaboracin, o mientras que el stub simplemente simula respuestas a consultas. El stub hace el test menos frgil pero, al mismo tiempo, nos aporta menos informacin a o sobre la colaboracin entre objetos. Para poder discernir entre usar un mock o o un stub, volvemos a recalcar que primero hay que saber qu estamos probando e y por qu. Los mocks tienen ventajas e inconvenientes sobre los stubs. Lo e mejor ser mostrarlas con ejemplos. a Los frameworks que generan mocks y stubs son muy ingeniosos. Son capaces de crear una clase en tiempo de ejecucin, que hereda de una clase o X o que implementa una interfaz Y. Tanto X como Y se pueden pasar como parmetro para que el framework genere una instancia de un mock o un a stub que sea de ese tipo pero cuya implementacin simplemente se limita a o reaccionar tal como le indiquemos que debe hacerlo. En este libro usaremos el framework Rhino.Mocks versin 3.6 para .Net, EasyMock 2.5.2 para Java, o
1 Mi buen amigo Eladio Lpez y yo lo tradujimos, aunque a d de hoy la traduccin o a o necesita ser mejorada: http://www.carlosble.com/traducciones/mocksArentStubs.html 2 La traduccin de stub ser sobra o colilla, as que todo el mundo ha optado por o a dejarlo en stub

96

Cap tulo 6

6.1. Cundo usar un objeto real, un stub o un mock a

Mockito 1.8.2 para Java y Pymox 0.5.1 para Python. Todos son software libre y se pueden descargar gratuitamente de la red. Este cap tulo posiblemente sea de los ms dif a ciles de entender de todo el libro. Para una correcta comprensin del mismo es recomendable leerlo con o el ordenador delante para ir ejecutando los ejemplos.

6.1.

Cundo usar un objeto real, un stub o un a mock

Vamos a por el primer ejemplo para sacar a relucir los pros y los contras de las distintas alternativas que tenemos para disear cdigo que trata n o de colaboraciones entre objetos. Supongamos que hemos escrito un control grco que muestra un calendario en pantalla para permitirnos seleccionar a una fecha. Ahora nos piden que dibujemos los d festivos de determinaas dos municipios en un color distinto, para lo cual tenemos que consultar un servicio remoto que lee, de una base de datos, los d festivos. El servicio as necesita conocer qu ao, qu mes y qu municipio nos ocupa, para devolver e n e e un vector con los d festivos del mes. La interfaz del servicio remoto es la as siguiente:

1 2 3 4 5 6

public interface ICalendarService { int [] GetHolidays ( int year , int month , string townCode ); }

El mtodo es de lectura, de consulta. Lo que queremos disear es el trozo de e n cdigo, de nuestra aplicacin cliente, que obtiene los d festivos del servidor o o as remoto. El SUT es el calendario cliente y su colaborador el servicio remoto. Estamos hablando de una aplicacin cliente/servidor. Para disear la colaboo n racin, no vamos a utilizar el servicio real porque el test no ser unitario, no o a se ejecutar de manera veloz ni con independencia del entorno. Est claro que a a necesitamos un doble. Qu tipo de doble de prueba utilizaremos? Generale mente, para los mtodos de consulta se usan stubs pero el factor determinante e para decantarse por un mock o un stub es el nivel de especicidad que se requiere en la colaboracin. Vamos a estudiar las dos posibilidades resaltano do las diferencias de cada una. Utilizaremos Rhino.Mocks para los ejemplos. Lo primero es aadir al proyecto las referencias a las DLL3 Rhino.Mocks, n Castle.Core y Castle.DynamicProxy2. Nuestro calendario cliente tiene tres propiedades que son CurrentYear CurrentMonth, CurrentTown que sirven para congurarlo.
3

incluidas en el cdigo fuente que acompaa al libro o n

97

6.1. Cundo usar un objeto real, un stub o un mock a

Cap tulo 6

El test, usando un mock, ser el siguiente: a

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

using using using using

System ; System . Collections . Generic ; NUnit . Framework ; Rhino . Mocks ;

[ TestFixture ] public class CalendarTests { [ Test ] public void C l i e n t A s k s C a l e n d a r S e r v i c e () { int year = 2009; int month = 2; string townCode = " b002 " ; ICalendarService serviceMock = MockRepository . GenerateStrictMock < ICalendarService >(); serviceMock . Expect ( x = > x . GetHolidays ( year , month , townCode )). Return ( new int [] { 1 , 5 }); Calendar calendar = new Calendar ( serviceMock ); calendar . CurrentTown = townCode ; calendar . CurrentYear = year ; calendar . CurrentMonth = month ; calendar . DrawMonth (); // the SUT serviceMock . V e r i f y A l l E x p e c t a t i o n s (); } }

El cdigo asusta un poco al principio pero, si lo estudiamos, veremos que o no es tan complejo. En l, se distinguen tres bloques separados por l e neas 4 ). El primer bloque consiste en la generacin del mock en blanco (AAA o y la denicin de expectativas. Con la llamada a GenerateStrictMock, o el framework genera una instancia de una clase que implementa la interfaz ICalendarService. Nos ahorramos crear una clase impostora a mano como hicimos en el cap tulo anterior. La siguiente l nea dene la primera expectativa (Expect) y dice que, sobre el propio objeto mock, en algn momento, se u invocar al mtodo GetHolidays con sus tres parmetros. Y adems, dice a e a a que, cuando esa invocacin se haga, el mock devolver un array de dos o a elementos, 1 y 5. O sea, estamos dicindole al mock que le van a invocar de e esa manera y que, cuando ocurra, queremos que se comporte tal cual. El siguiente bloque ya es el de acto (Act), donde se invoca al SUT. La ultima l nea es la que verica que todo fue segn lo esperado (Assert), la u que le dice al mock que compruebe que la expectativa se cumpli. Si no se o cumpli, entonces el test no pasa porque el framework lanza una excepcin. o o Que no se cumpli signica que la llamada nunca se hizo o que se hizo con o
4

Las tres partes de un test: Arrange, Act, Assert

98

Cap tulo 6

6.1. Cundo usar un objeto real, un stub o un mock a

otros parmetros distintos a los que expl a citamente se pusieron en la expectativa o bien que se hizo ms de una vez. Adems, si en el acto se hacen a a llamadas al mock que no estaban contempladas en las expectativas (puesto que solo hay una expectativa, cualquier otra llamada al servicio ser noa contemplada), el framework hace fallar el test. La ausencia de expectativa supone fallo, si se produce alguna interaccin entre SUT y mock, al margen o de la descrita expl citamente. Esta es una restriccin o una validacin imporo o tante, segn cmo se mire. Si el colaborador fuese un stub, la vericacin (y u o o por tanto sus restricciones), no se aplicar como veremos a continuacin. a, o Desde luego, el framework est haciendo una gran cantidad de trabajo por a nosotros, ahorrndonos una buena suma de l a neas de cdigo y evitndonos o a cdigo espec o co de validacin dentro del SUT. o Si por motivos de rendimiento, por ejemplo, queremos obligar a que el SUT se comunique una unica vez con el colaborador, siendo adems de la a forma que dicta el test, entonces un mock est bien como colaborador. Cuala quier cosa que se salga de lo que pone el test, se traducir en luz roja. Digo a rendimiento porque quizs queremos cuidarnos del caso en que el calendaa rio hiciese varias llamadas al servicio por despiste del programador o por cualquier otro motivo. El cdigo del SUT que hace pasar el test ser algo como: o a

1 2 3 4 5 6 7 8

public void DrawMonth () { // ... some b u s i n e s s code here ... int [] holidays = _calendarService . GetHolidays ( _currentYear , _currentMonth , _currentTown ); // ... rest of b u s i n e s s logic here ... }

Cmo lo har o amos con un stub? qu implicaciones tiene?. El stub no e dispone de vericacin de expectativas5 sino que hay que usar el Assert o de NUnit para validar el estado. En el presente ejemplo, podemos validar el estado, deniendo en el calendario cliente alguna propiedad Holidays de tipo array de enteros que almacenase la respuesta del servidor para poder armar sobre l. Al recurrir al stub, nos aseguramos que el SUT es capaz de e funcionar puesto que, cuando invoque a su colaborador, obtendr respuesta. a El stub, al igual que el mock, simular al servicio devolviendo unos valores: a

1 2

[ Test ] public void D r a w H o l i d a y s W i t h S t u b ()

Es decir, la llamada a VerifyAllExpectations. Aunque en realidad dicha funcin o s forma parte de la API para stubs en Rhino.Mocks, no verica nada, siempre da un resultado positivo. Existe la posibilidad de llamar a AssertWasCalled pero el propio Ayende, autor de Rhino.Mocks no est seguro de que sea correcto segn la denicin a u o de stub, con lo que podr optar por eliminarla en futuras versiones. a

99

6.1. Cundo usar un objeto real, un stub o un mock a

Cap tulo 6

3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

{ int year = 2009; int month = 2; string townCode = " b002 " ; ICalendarService serviceStub = MockRepository . GenerateStub < ICalendarService >(); serviceStub . Stub ( x = > x . GetHolidays ( year , month , townCode )). Return ( new int [] { 1 , 5 }); Calendar calendar = new Calendar ( serviceStub ); calendar . CurrentTown = townCode ; calendar . CurrentYear = year ; calendar . CurrentMonth = month ; calendar . DrawMonth (); Assert . AreEqual (1 , calendar . Holidays [0]); Assert . AreEqual (5 , calendar . Holidays [1]); }

La diferencia es que este test mantendr la luz verde incluso aunque no a se llamase a GetHolidays, siempre que la propiedad Holidays de calendar tuviese los valores indicados en las armaciones del nal. Tambin pasar e a aunque la llamada se hiciese cien veces y aunque se hicieran llamadas a otros mtodos del servicio. Al ser menos restrictivo, el test es menos frgil e a que su versin con un mock. Sin embargo, nos queda sensacin de que no o o sabemos si la llamada al colaborador se est haciendo o no. Para salir de a dudas, hay que plantearse cul es el verdadero objetivo del test. Si se trata a de describir la comunicacin entre calendario y servicio con total precisin, o o usar un mock. Si me basta con que el calendario obtenga los d festivos y a as trabaje con ellos, usar un stub. Cuando no estamos interesados en controlar a con total exactitud la forma y el nmero de llamadas que se van a hacer al u colaborador, tambin podemos utilizar un stub. Es decir, para todos aquellos e casos en los que le pedimos al framework... si se produce esta llamada, entonces devuelve X, independientemente de que la llamada se produzca una o mil veces. Digamos que son atajos para simular el entorno y que se den las condiciones oportunas. Al n y al cabo, siempre podemos cubrir el cdigo con un test de integracin posterior que nos asegure que todas las o o partes trabajan bien juntas. A continuacin, vamos a por un ejemplo que nos har dudar sobre el tipo o a de doble a usar o, incluso, si conviene un doble o no. Se trata de un software de facturacin. Tenemos los objetos, Invoice (factura), Line y TaxManager o (gestor de impuestos). El objeto factura necesita colaborar con el gestor de impuestos para calcular el importe a sumar al total, ya que el porcentaje de los mismos puede variar dependiendo de los art culos y dependiendo de la regin. o Una factura se compone de una o ms l a neas y cada l nea contiene el art culo y la cantidad. Nos interesa inyectar el gestor de impuestos en el constructor 100

Cap tulo 6

6.1. Cundo usar un objeto real, un stub o un mock a

de la factura para que podamos tener distintos gestores correspondientes a distintos impuestos. As si estoy en Madrid inyectar el IvaManager y si , e estoy en Canarias6 el IgicManager. Cmo vamos a probar esta colaboracin? utilizaremos un objeto real? o o un stub? un mock tal vez?. Partimos de la base de que el gestor de impuestos ya ha sido implementado. Puesto que no altera el sistema y produce una respuesta rpida, yo utilizar el objeto real: a a

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

[ TestFixture ] public class InvoiceTests { [ Test ] public void CalculateTaxes () { Stock stock = new Stock (); Product product = stock . GetProductWithCode ( " x1abc3t3c " ); Line line = new Line (); int quantity = 10; line . AddProducts ( product , quantity ); Invoice invoice = new Invoice ( new TaxManager ()); invoice . AddLine ( line ); float total = invoice . GetTotal (); Assert . Greater ( quantity * product . Price , total ); } }

Las tres partes del test estn separadas por l a neas en blanco. En la armacin, nos limitamos a decir que el total debe ser mayor que la simple o multiplicacin del precio del producto por la cantidad de productos. Usar el o colaborador real (TaxManager) tiene la ventaja de que el cdigo del test es o sencillo y de que, los posibles defectos que tenga, probablemente sean detectados en este test. Sin embargo, el objetivo del test no es probar TaxManager (el colaborador) sino probar Invoice (el SUT). Visto as resulta que si usa, mos un doble para TaxManager, entonces el SUT queda perfectamente aislado y este test no se rompe aunque se introduzcan defectos en el colaborador. La eleccin no es fcil. Personalmente, preero usar el colaborador real en o a estos casos en que no altera el sistema y se ejecuta rpido. A pesar de que el a test se puede romper por causas ajenas al SUT, ir haciendo TDD me garantiza que, en el instante siguiente a la generacin del defecto, la alerta roja se o va a activar, con lo que detectar y corregir el error ser cuestin de seguna o dos. Los mocks no se inventaron para aislar dependencias sino para disear n colaboraciones, aunque el aislamiento es un aspecto secundario que suele resultar benecioso. Se puede argumentar que, al no haber usado un mock, no tenemos garant de que el clculo del impuesto fuese realizado por el gestor as a
6

En Canarias no se aplica el IVA sino un impuesto llamado IGIC

101

6.1. Cundo usar un objeto real, un stub o un mock a

Cap tulo 6

de impuestos. Podr haberse calculado dentro del mismo objeto factura sin a hacer uso de su colaborador. Pero eso supondr que, forzosamente, hemos a producido cdigo duplicado; replicado del gestor de impuestos. El tercer paso o del algoritmo TDD consiste en eliminar la duplicidad, por lo que, si lo estamos aplicando, no es obligatorio que un mock nos garantice que se hizo la llamada al colaborador. La validacin de estado que hemos hecho, junto con o la ausencia de duplicidad, son sucientes para armar que el cdigo va por o buen camino. As pues, la tcnica no es la misma si estamos haciendo TDD e que si estamos escribiendo pruebas de software a secas. Las consideraciones dieren. Si el gestor de impuestos accediese a la base de datos, escribiese en un chero en disco o enviase un email, entonces seguro que hubiese utilizado un doble. Si no estuviese implementado todav y me viese obligado a disear a n primero esta funcionalidad de la factura, entonces seguro usar un stub para a simular que la funcionalidad del gestor de impuestos est hecha y produce el a resultado que quiero. En palabras de Steve Freeman[14]: utilizamos mocks cuando el servicio cambia el mundo exterior; stubs cuando no lo hace - stubs para consultas y mocks para acciones. Usa un stub para mtodos de consulta y un mock e para suplantar acciones. Es una regla que nos orientar en muchos casos. a Aplicada al ejemplo anterior, funciona; usar amos un stub si el TaxManager no estuviese implementado, ya que le hacemos una consulta. Hay veces en las que un mismo test requiere de mocks y stubs a la vez, ya que es comn que un SUT tenga varios colaboradores, siendo algunos stubs u y otros mocks, dependiendo de la intencin del test. o Antes de pasar a estudiar las peculiaridades de EasyMock veamos un ejemplo ms complejo. Se trata del ejemplo de los estudiantes del cap a tulo anterior. Escribimos el mismo test con la ayuda de Rhino.Mocks:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

using using using using using

System ; System . Collections . Generic ; System . Text ; NUnit . Framework ; Rhino . Mocks ;

[ Test ] public void AddStudentScore () { string studentId = " 23145 " ; float score = 8.5 f ; Student dummyStudent = new Student (); IDataManager dataManagerMock = MockRepository . GenerateStrictMock < IDataManager >(); dataManagerMock . Expect ( x = > x . GetByKey ( studentId )). Return ( dummyStudent ); dataManagerMock . Expect ( x = > x . Save ( dummyStudent ));

102

Cap tulo 6

6.1. Cundo usar un objeto real, un stub o un mock a

21 22 23 24 25

ScoreManager smanager = new ScoreManager ( dataManagerMoc k ); smanager . AddScore ( studentId , score ); dataManagerMock . V e r i f y A l l E x p e c t a t i o n s (); }

En este caso, el colaborador es un mock con dos expectativas. El orden de las expectativas tambin es decisivo en la fase de vericacin: si la que aparece e o segunda se diese antes que la primera, el framework marcar el test como a fallido. A pesar del esfuerzo que hemos hecho por escribir este test, tiene un problema importante y es su fragilidad, ya que conoce cmo funciona el SUT o ms de lo que deber No slo sabe que hace dos llamadas a su colaborador a a. o sino que, adems, conoce el orden y ni si quiera estamos comprobando que a los puntos han subido al marcador del alumno. Ser que el SUT tiene ms a a de una responsabilidad? Un cdigo que se hace muy dif de probar expreo cil sa que necesita ser reescrito. Reexionemos sobre las responsabilidades. El ScoreManager est encargado de coordinar la accin de actualizar el marcaa o dor y guardar los datos. Podemos identicar entonces la responsabilidad de actualizar el marcador y separarla. La delegamos en una clase que se encarga exclusivamente de ello. Vamos a disearla utilizando un test unitario: n

1 2 3 4 5 6 7 8

[ Test ] public void ScoreUpdaterWorks () { ScoreUpdater updater = new ScoreUpdater (); Student student = updater . UpdateScore ( new Student () , 5 f ); Assert . AreEqual ( student . Score , 5 f ); }

El cdigo del SUT: o

1 2 3 4 5 6 7 8 9

public class ScoreUpdater : IScoreUpdater { public Student UpdateScore ( Student student , float score ) { student . Score = student . Score + score ; return student ; } }

Ahora, que ya podemos probarlo todo, reescribamos el test que nos preocupaba:

1 2 3 4 5 6 7

[ Test ] public void AddStudentScore () { string studentId = " 23145 " ; float score = 8.5 f ; Student dummyStudent = new Student ();

103

6.1. Cundo usar un objeto real, un stub o un mock a

Cap tulo 6

8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

IDataManager dataManagerMock = MockRepository . GenerateStrictMock < IDataManager >(); dataManagerMock . Expect ( x = > x . GetByKey ( studentId )). Return ( dummyStudent ); dataManagerMock . Expect ( x = > x . Save ( dummyStudent )); IScoreUpdater scoreUpdaterMock = MockRepository . GenerateStrictMock < IScoreUpdater >(); scoreUpdaterMock . Expect ( y = > y . UpdateScore ( dummyStudent , score )). Return ( dummyS tudent ); ScoreManager smanager = new ScoreManager ( dataManagerMock , scoreUpdaterMock ); smanager . AddScore ( studentId , score ); dataManagerMock . V e r i f y A l l E x p e c t a t i o n s (); scoreUpdaterMock . V e r i f y A l l E x p e c t a t i o n s (); }

Hubo que modicar el constructor de ScoreManager para que aceptase otro colaborador. Ahora estamos seguros que se est probando todo. Pero el test a es idntico a la implementacin del SUT! e o

1 2 3 4 5 6 7 8

public void AddScore ( string studentId , float score ) { IRelationalObject student = _dataManager . GetByKey ( studentId ); Student studentUpdated = _updater . UpdateScore (( Student ) student , score ); _dataManager . Save ( studentUpdated ); }

Desde luego este test parece ms un SUT en s mismo que un ejemplo de a cmo debe funcionar el SUT. Imita demasiado lo que hace el SUT. Cmo lo o o enmendamos? Lo primero que hay que preguntarse es si realmente estamos obligados a obtener el objeto Student a partir de su clave primaria o si tiene ms sentido recibirlo de alguna otra funcin. Si toda la aplicacin tiene un a o o buen diseo orientado a objetos, quizs el mtodo AddScore puede recibir ya n a e un objeto Student en lugar de su clave primaria. En ese caso, nos quitar amos una expectativa del test. Vamos a suponer que podemos modicar el SUT para aceptar este cambio:

1 2 3 4 5 6 7 8 9 10 11 12

[ Test ] public void AddStudentScore () { float score = 8.5 f ; Student dummyStudent = new Student (); IScoreUpdater scoreUpdaterMock = MockRepository . GenerateStrictMock < IScoreUpdater >(); scoreUpdaterMock . Expect ( y = > y . UpdateScore ( dummyStudent , score )). Return ( dummyStudent ); IDataManager dataManagerMock = MockRepository . GenerateStrictMock < IDataManager >();

104

Cap tulo 6

6.1. Cundo usar un objeto real, un stub o un mock a

13 14 15 16 17 18 19 20 21 22

dataManagerMock . Expect ( x = > x . Save ( dummyStudent )); ScoreManager smanager = new ScoreManager ( dataManagerMock , scoreUpdaterMock ); smanager . AddScore ( dummyStudent , score ); dataManagerMock . V e r i f y A l l E x p e c t a t i o n s (); scoreUpdaterMock . V e r i f y A l l E x p e c t a t i o n s (); }


1 2 3 4 5 6

public void AddScore ( Student student , float score ) { Student studentUpdated = _updater . UpdateScore ( student , score ); _dataManager . Save ( studentUpdated ); }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

Ahora el test nos ha quedado con dos expectativas pero pertenecientes a distintos colaboradores. El orden de las llamadas no importa cuando son sobre colaboradores distintos, es decir, que aunque en el test hayamos denido la expectativa UpdateScore antes que Save, no se romper si el SUT los a invocase en orden inverso. Entonces el test no queda tan frgil. a En caso de que no podamos cambiar la API para recibir el objeto Student, slo nos queda partir el test en varios para eliminar las restricciones impuestas o por el framework con respecto al orden en las llamadas a los mocks. La idea es probar un solo aspecto de la colaboracin en cada test mediante mocks, o e ignorar lo dems con stubs. Veamos un ejemplo simplicado. Pensemos en a la orquestacin de unos servicios web. El SUT se encarga de orquestar (cooro dinar) la forma en que se realizan llamadas a distintos servicios. De forma abreviada el SUT ser a:
public class Orchestrator { private IServices _services ; public Orchestrator ( IServices services ) { _services = services ; } public void Orchestrate () { _services . MethodA (); _services . MethodB (); } }

La orquestacin consiste en invocar al servicio A y, a continuacin, al servicio o o B. Si escribimos un test con tales expectativas, queda un test idntico al e SUT como nos estaba pasando. Vamos a partirlo en dos para probar cada colaboracin por separado: o 105

6.1. Cundo usar un objeto real, un stub o un mock a


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

Cap tulo 6

[ TestFixture ] public class ServicesTests { [ Test ] public void OrchestratorCallsA () { IServices servicesMock = MockRepository . GenerateStrictMock < IServices >(); servicesMock . Expect ( a = > a . MethodA ()); servicesMock . Stub ( b = > b . MethodB ()); Orchestrator orchestrator = new Orchestrator ( servicesMock ); orchestrator . Orchestrate (); servicesMock . V e r i f y A l l E x p e c t a t i o n s (); } [ Test ] public void OrchestratorCallsB () { IServices servicesMock = MockRepository . GenerateStrictMock < IServices >(); servicesMock . Expect ( b = > b . MethodB ()); servicesMock . Stub ( a = > a . MethodA ()); Orchestrator orchestrator = new Orchestrator ( servicesMock ); orchestrator . Orchestrate (); servicesMock . V e r i f y A l l E x p e c t a t i o n s (); } }

El primer test tan slo se encarga de probar que el servicio A se llama, o mientras que se le dice al framework que da igual lo que se haga con el servicio B. El segundo test trabaja a la inversa. De esta forma, estamos diseando qu elementos toman parte en la orquestacin y no tanto el orden n e o mismo. As el test no es tan frgil y la posibilidad de romper los tests por a cambios en el SUT disminuye. No obstante, si resulta que el orden de las llamadas es algo tan cr tico que se decide escribir todas las expectativas en un solo test, se puede hacer siempre que tengamos conciencia de lo que ello signica: lanzar una alerta cada vez que alguien modique algo del SUT. Es programar una alerta, no un test. Si de verdad es lo que necesitamos, entonces est bien. Rhino.Mocks permite crear h a bridos7 entre mocks y stubs mediante
Leyendo a Fowler y Meszaros entiendo que en realidad no son h bridos sino stubs, ya que se les atribuye la posibilidad de recordar expectativas. Sin embargo la mayor de a los frameworks consideran que un stub s mplemente devuelve valores, que no recuerda
7

106

Cap tulo 6

6.1. Cundo usar un objeto real, un stub o un mock a

la llamada GenerateMock en lugar de GenerateStrictMock. As los tests , anteriores se podr reescribir con un resultado similar y menos l an neas de cdigo: o

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

[ TestFixture ] public class S e r v i c e s H y b r i d M o c k s T e s t s { [ Test ] public void OrchestratorCallsA () { IServices servicesMock = MockRepository . GenerateMock < IServices >(); servicesMock . Expect ( a = > a . MethodA ()); Orchestrator orchestrator = new Orchestrator ( servicesMock ); orchestrator . Orchestrate (); servicesMock . V e r i f y A l l E x p e c t a t i o n s (); } [ Test ] public void OrchestratorCallsB () { IServices servicesMock = MockRepository . GenerateMock < IServices >(); servicesMock . Expect ( b = > b . MethodB ()); Orchestrator orchestrator = new Orchestrator ( servicesMock ); orchestrator . Orchestrate (); servicesMock . V e r i f y A l l E x p e c t a t i o n s (); } }

La diferencia es que el framework slo falla si la llamada no se produce pero o admite que se hagan otras llamadas sobre el SUT, incluso que se repita la llamada que denimos en la expectativa. Nos ahorra declarar la llamada stub en el test pero, por contra, se calla la posible repeticin de la expectativa, lo o cual seguramente no nos conviene. Visto el ejemplo de los servicios, queda como ejercicio propuesto escribir tests para el ejemplo de los estudiantes que no hemos terminado de cerrar. Quizs haya observado que en todo el cap a tulo no hemos creado ningn u mock basado en una implementacin concreta de una base de datos ni de nino guna librer del sistema (.Net en este caso). No es recomendable crear mocks a basados en dependencias externas sino slo en nuestras propias interfaces. o De ah el uso de interfaces como IDataManager. Aunque es posible hacer
nada.

107

6.2. La metfora Record/Replay a

Cap tulo 6

mocks de clases, siempre usamos interfaces, puesto que la inyeccin de deo pendencias8 es la mejor forma en que podemos gestionar tales dependencias entre SUT y colaboradores. Una vez escritos los tests unitarios, aadiremos n tests de integracin que se encargan de probar que aquellas de nuestras clao ses que hablan con el sistema externo, lo hacen bien. Escribir amos tests de integracin para la clase DataManager. En el caso de Python, si la API de o la clase externa nos resulta sucientemente genrica, podemos hacer mock e de la misma directamente, dado que en este lenguaje no necesitamos denir interfaces al ser dbilmente tipado. Por eso, en Python, slo crear una clase e o a tipo DataManager si viese que las distintas entidades externas con las que hablar son muy heterogneas. Es un claro ejemplo en el que Python ahorra a e unas cuantas l neas de cdigo. o

6.2.

La metfora Record/Replay a

Algunos frameworks de mocks como EasyMock (y en versiones anteriores, tambin Rhino.Mocks) usan la metfora conocida como Record/Replay. e a Necesitan que les indiquemos expl citamente cundo hemos terminado de dea nir expectativas para comenzar el acto. Afortunadamente, es fcil hacerlo, a es una l nea de cdigo pero a la gente le choca esto de record y replay. Si heo mos comprendido todos los tests de este cap tulo, la metfora no ser ningn a a u problema. EasyMock es para Java. Veamos la versin Java del ejemplo del o calendario:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 8

6.1: EasyMock
static org . junit . Assert .*; org . junit . Test ; java . util . Calendar ; static org . easymock . EasyMock .*;

import import import import

public class CalendarTests { @Test public void drawHolidays () { int year = 2009; int month = 2; String townCode = " b002 " ; ICalendarService serviceMock = createMock ( ICalendarService . class ); expect ( serviceMock . getHolidays ( year , month , townCode ) ). andReturn ( new int [] { 1 , 5 }); replay ( serviceMock );

Ver Cap tulo 7 en la pgina 111 a

108

Cap tulo 6

6.2. La metfora Record/Replay a

22 23 24 25 26 27 28 29 30 31

Calendar calendar = new Calendar ( serviceMock ); calendar . set_currentTown ( townCode ); calendar . set_currentYear ( year ); calendar . set_currentMonth ( month ); calendar . drawMonth (); verify ( serviceMock ); } }

Prcticamente todo igual. Lo unico es que hemos tenido que decir expl a citamente replay(serviceMock) para cambiar de estado al mock. Si se nos olvida activar el replay, el resultado del test es bastante raro puesto que, lo que deber ser el acto se sigue considerando la preparacin y es desconcera o tante. Suele pasar al principio con este tipo de frameworks. La documentacin o de EasyMock es concisa, siendo recomendable dedicar unos minutos a revisarla para descubrir toda su funcionalidad. Al igual que la de Rhino.Mocks, contiene ejemplos que reforzarn los conceptos expresados en estas l a neas. La metfora que acabamos de ver no aporta ninguna ventaja al desarrollo y, a de hecho, en Rhino.Mocks ya se hace impl cita. El motivo por el que algunos frameworks la mantienen es para hacer ms sencilla la implementacin de los a o mocks internamente, es decir, que es una limitacin en lugar de una ventaja. o Mi framework de mocks favorito en Java, a d de hoy, es Mockito9 ya que a es ms sencillo que EasyMock. Produce un cdigo ms claro y la metfora de a o a a record/replay ya se ha superado. Para mockito no hay que utilizar distintos mtodos a la hora de crear un stub y un mock. En principio todos los dobles e se crean con el mtodo mock, aunque luego sea un stub. La diferencia queda e impl tica en la forma en que utilicemos el doble. As por ejemplo, para denir un mock con una expectativa, podemos hacer lo siguiente:

1 2 3 4 5 6 7 8 9 10 11

6.2: Mockito

@Test public void persistorSaves () throws Exception { EmailAddress emailMock = mock ( EmailAddress . class ); Persistor < EmailAddress > persistor = new Persistor < EmailAddress >(); persistor . Save ( emailMock ); verify ( emailMock ). save (); }

El test dice que nuestro SUT (Persistor) tiene que invocar forzosamente en algn momento al mtodo save del colaborador, que es de tipo EmailAddress. u e
9

http://mockito.org

109

6.2. La metfora Record/Replay a

Cap tulo 6

Los dobles de prueba en mockito son, por defecto, una especie de mock h brido o relajado10 que no tiene en cuenta el orden de las expectativas u otras posibles llamadas, salvo que se especique con cdigo, lo cual se agradece la o mayor de las veces. Es similar al GenerateMock de Rhino.Mocks. a Por ultimo, nos queda ver cmo se trabaja en Python pero lo haremos en o la parte prctica del libro, porque tanto con Python Mocker11 como con Pya Mox12 , se trabaja practicamente igual a como acabamos de estudiar. Mocker funciona francamente bien pero su desarrollo lleva tiempo parado, mientras que el de Pymox sigue activo y su sintxis se parece an ms a EasyMock o a u a Rhino.Mocks. Algunas de las preguntas que quedan abiertas en el presente cap tulo, se resolvern en los siguientes. a

Como dijimos antes, hay quien les llama stubs pero resulta confuso. La nomenclatura de facto, dice que los stubs se limitan a devolver valores cuando se les pregunta 11 http://labix.org/mocker 12 http://code.google.com/p/pymox/

10

110

Captulo

Diseo Orientado a Objetos n


TDD tiene una estrecha relacin con el buen diseo orientado a objetos y o n por tanto, como no, con los principios S.O.L.I.D que veremos a continuacin. o En el ultimo paso del algoritmo TDD, el de refactorizar, entra en juego nuestra pericia diseando clases y mtodos. Durante los cap n e tulos prcticos a haremos uso de todos estos principios y los nombraremos donde corresponda.

7.1.

Dise o Orientado a Objetos (OOD) n

Todos los lenguajes y plataformas actuales se basan en el paradigma de la programacin orientada a objetos (OOP por sus siglas en ingls). Aunque a o e diario trabajamos con objetos, no todo el mundo comprende realmente lo que es el polimorsmo o para qu sirve una clase abstracta, por poner un ejemplo. e La potencia de la orientacin a objetos lleva impl o cita mucha complejidad y una larga curva de aprendizaje. Lo que en unos casos es una buena manera de resolver un problema, en otros es la forma de hacer el cdigo ms frgil. o a a Es decir, no siempre conviene crear una jerarqu de clases, dependiendo a del caso puede ser ms conveniente crear una asociacin entre objetos que a o colaboran. Desafortunadamente no hay reglas universales que sirvan para toda la gama de problemas que nos podamos encontrar pero hay ciertos principios y patrones que nos pueden dar pistas sobre cual es el diseo ms n a conveniente en un momento dado. Con nes docentes se suele explicar la OOP mediante ejemplos relacionados con el mundo que conocemos: vase el t e pico ejemplo de la clase Animal, de la que hereda la clase Mam fero, de la que a su vez hereda la clase Cuadrpedo, de la que a su vez heredan las clases Perro y Gato... El u s no est mal en s mismo, lo que sucede es que las clases en el software mil a

111

7.2. Principios S.O.L.I.D

Cap tulo 7

no siempre casan directamente con los objetos del mundo real, porque el software diere mucho de ste. Las clasicaciones naturales de los objetos, e no tienen por qu ser clasicaciones adecuadas en el software. La concepe tualizacin y el modelado son una espada de doble lo, pues como vamos a o mostrar, la realidad es demasiado compleja de modelar mediante OOP y el resultado puede ser un cdigo muy acoplado, muy dif de reutilizar. o cil Veamos el ejemplo de la clase Rectangulo. El Rectangulo tiene dos atributos, Ancho y Alto y un mtodo que es capaz de calcular el rea. Ahora e a necesitamos una clase Cuadrado. En geometr el cuadrado es un rectngulo, a a por tanto, si copiamos esta clasicacin dir o amos que Cuadrado hereda de Rectangulo. Vale, entonces denimos Cuadrado extendiendo de Rectangulo. Ahora damos la clase Cuadrado a alguien que tiene que trabajar con ella y se encuentra con los atributos heredados, Ancho y Alto. Lo ms probable a es que se pregunte... Qu signican el ancho y el alto en un cuadrado? Un e cuadrado tiene todos sus lados iguales, no tiene ancho y alto. Este diseo n no tienen sentido. Para este caso particular, si lo que queremos es ahorrarnos reescribir el mtodo que calcula el rea, podemos crear ese mtodo en e a e una tercera clase que colabora con Rectangulo y Cuadrado para calcular el rea. As Rectngulo sabe que cuando necesite calcular el rea invocar al a a a a mtodo de esta clase colaboradora pasando Ancho y Alto como parmetros e a y Cuadrado pasar dos veces la longitud de uno de sus lados. a Una de las mejores formas que hay, de ver si la API que estamos diseando n es intuitiva o no, es usarla. TDD propone usarla antes de implementarla, lo que le da in giro completo a la forma en que creamos nuestras clases. Puesto que todo lo hacemos con objetos, el benecio de disear adecuadamente n cambia radicalmente la calidad del software.

7.2.

Principios S.O.L.I.D

Son cinco principios fundamentales, uno por cada letra, que hablan del diseo orientado a objetos en trminos de la gestin de dependencias. Las n e o dependencias entre unas clases y otras son las que hacen al cdigo ms frgil o a a o ms robusto y reutilizable. El problema con el modelado tradicional es que a no se ocupa en profundidad de la gestin de dependencias entre clases sino de o la conceptualizacin. Quin decidi resaltar estos principios y darles nombre o e o a algunos de ellos fue Robert C. Martin, all por el ao 1995. a n

112

Cap tulo 7

7.2. Principios S.O.L.I.D

7.2.1.

Single Responsibility Principle (SRP)

El principio que da origen a la S de S.O.L.I.D es el de una unica res ponsabilidad y dice que cada clase debe ocuparse de un solo menester. Visto de otro modo, R. Martin dice que cada clase deber tener un unico motivo a para ser modicada. Si estamos delante de una clase que se podr ver oblia gada a cambiar ante una modicacin en la base de datos y a la vez, ante o un cambio en el proceso de negocio, podemos armar que dicha clase tiene ms de una responsabilidad o ms de un motivo para cambiar, por poner un a a ejemplo. Se aplica tanto a la clase como a cada uno de sus mtodos, con e lo que cada mtodo tambin deber tener un solo motivo para cambiar. El e e a efecto que produce este principio son clases con nombres muy descriptivos y por tanto largos, que tienen menos de cinco mtodos, cada uno tambin e e con nombres que sirven perfectamente de documentacin, es decir, de varias o palabras: CalcularAreaRectangulo y que no contienen ms de 15 l a neas de cdigo. En la prctica la mayor de mis clases tienen uno o dos mtodos o a a e nada ms. Este principio es quizs el ms importante de todos, el ms sencillo a a a a y a la vez el ms complicado de llevar a cabo. Existen ejemplos de cdigo y a o una explicacin ms detallada del mismo en la propia web del autor1 . Martin o a tambin habla en profundidad sobre SRP en su libro[10]. Hay una antigua e tcnica llamada Responsibility Driven Design (RDD), que viene a decir lo e mismo que este principio, aunque es anterior a la aparicin de SRP como o tal. TDD es una excelente manera de hacer RDD o de seguir el SRP, como se quiera ver. All por el ao 1989, Kent Beck y Ward Cunningham usaban a n tarjetas CRC2 (Class, Responsibility, Collaboration) como ayuda para detectar responsabilidades y colaboraciones entre clases. Cada tarjeta es para una entidad, no necesariamente una clase. Desde que disponemos de herramientas que nos permiten el desarrollo dirigido por tests, las tarjetas CRC han pasado a un segundo plano pero puede ser buena idea usarlas parcialmente para casos donde no terminamos de ver claras las responsabilidades.

7.2.2.

Open-Closed Principle (OCP)

Una entidad software (una clase, mdulo o funcin) debe estar abierta o o a extensiones pero cerrada a modicaciones. Puesto que el software requiere cambios y que unas entidades dependen de otras, las modicaciones en el cdigo de una de ellas puede generar indeseables efectos colaterales en o cascada. Para evitarlo, el principio dice que el comportamiento de una entidad debe poder ser alterado sin tener que modicar su propio cdigo fuente. o
1 2

http://www.objectmentor.com/resources/articles/srp.pdf http://c2.com/doc/oopsla89/paper.html

113

7.2. Principios S.O.L.I.D

Cap tulo 7

Cmo se hace esto?, Hay varias tcnicas dependiendo del diseo, una podr o e n a ser mediante herencia y redenicin de los mtodos de la clase padre, donde o e dicha clase padre podr incluso ser abstracta. La otra podr ser inyectando a a dependencias que cumplen el mismo contrato (que tienen la misma interfaz) pero que implementan diferente funcionamiento. En prximos prrafos estuo a diaremos la inyeccin de dependencias. Como la totalidad del cdigo no se o o puede ni se debe cerrar a cambios, el diseador debe decidir contra cules n a protegerse mediante este principio. Su aplicacin requiere bastante experieno cia, no slo por la dicultad de crear entidades de comportamiento extensible o sino por el peligro que conlleva cerrar determinadas entidades o parte de ellas. Cerrar en exceso obliga a escribir demasiadas l neas de cdigo a la hora de o reutilizar la entidad en cuestin. El nombre de Open-Closed se lo debemos o a Bertrand Meyer y data del ao 1988. En espaol podemos denominarlo el n n principio Abierto-Cerrado. Para ejemplos de cdigo lase el art o e culo original de R. Martin3

7.2.3.

Liskov Substitution Principle (LSP)

Introducido por Barbara Liskov en 1987, lo que viene diciendo es que si una funcin recibe un objeto como parmetro, de tipo X y en su lugar le o a pasamos otro de tipo Y, que hereda de X, dicha funcin debe proceder correco tamente. Por el propio polimorsmo, los compiladores e intrpretes admiten e este paso de parmetros, la cuestin es si la funcin de verdad est diseada a o o a n para hacer lo que debe, aunque quien recibe como parmetro no es exaca tamente X, sino Y. El principio de sustitucin de Liskov est estrechamente o a relacionado con el anterior en cuanto a la extensibilidad de las clases cuando sta se realiza mediante herencia o subtipos. Si una funcin no cumple el e o LSP entonces rompe el OCP puesto que para ser capaz de funcionar con subtipos (clases hijas) necesita saber demasiado de la clase padre y por tanto, modicarla. El diseo por contrato (Design by Contract) es otra forma n de llamar al LSP. Lase el art e culo de R. Martin sobre este principio4 .

7.2.4.

Interface Segregation Principle (ISP)

Cuando empleamos el SRP tambin empleamos el ISP como efecto colae teral. El ISP deende que no obliguemos a los clientes a depender de clases o interfaces que no necesitan usar. Tal imposicin ocurre cuando una clao se o interfaz tiene ms mtodos de los que un cliente (otra clase o entidad) a e
3 4

http://www.objectmentor.com/resources/articles/ocp.pdf http://www.objectmentor.com/resources/articles/lsp.pdf

114

Cap tulo 7

7.2. Principios S.O.L.I.D

necesita para s mismo. Seguramente sirve a varios objetos cliente con respon sabilidades diferentes, con lo que deber estar dividida en varias entidades. a En los lenguajes como Java y C# hablamos de interfaces pero en lenguajes interpretados como Python, que no requieren interfaces, hablamos de clases. No slo es por motivos de robustez del software, sino tambin por motivos o e de despliegue. Cuando un cliente depende de una interfaz con funcionalidad que no utiliza, se convierte en dependiente de otro cliente y la posibilidad de catstrofe frente a cambios en la interfaz o clase base se multiplica. Lase el a e 5 art culo de R. Martin

7.2.5.

Dependency Inversin Principle (DIP) o

La inversin de dependencias da origen a la conocida inyeccin de depeno o dencias, una de las mejores tcnicas para lidiar con las colaboraciones entre e clases, produciendo un cdigo reutilizable, sobrio y preparado para cambiar o sin producir efectos bola de nieve. DIP explica que un mdulo concreto o A, no debe depender directamente de otro mdulo concreto B, sino de una o abstraccin de B. Tal abstraccin es una interfaz o una clase (que podr ser o o a abstracta) que sirve de base para un conjunto de clases hijas. En el caso de un lenguaje interpretado no necesitamos denir interfaces, ni siquiera jerarqu as pero el concepto se aplica igualmente. Vemoslo con un ejemplo sencillo: La a clase Logica necesita de un colaborador para guardar el dato Dato en algn u lugar persistente. Disponemos de una clase MyBD que es capaz de almacenar Dato en una base de datos MySQL y de una clase FS que es capaz de almacenar Dato en un chero binario sobre un sistema de cheros NTFS. Si en el cdigo de Logica escribimos literalmente el nombre de la clase MyBD o como colaborador para persistir datos, Cmo haremos cuando necesitamos o cambiar la base de datos por cheros binarios en disco?. No quedar otro a remedio que modicar el cdigo de Logica. o Si las clases MyDB y FS implementasen una misma interfaz IPersistor para salvar Dato, podriamos limitarnos a usar IPersistor (que es una abstraccin) en el cdigo de Logica. Cuando los requerimientos exigiesen un o o cambio de base de datos por cheros en disco o viceversa, slo tendr o amos que preocuparnos de que el atributo myPersistor de la clase Logica, que es de tipo IPersistor contuviese una instancia de MyDB o bien de FS. Cmo resolvemos esta ultima parte?. Con la inyeccin de dependencias, o o que vamos a ver dentro del siguiente apartado, Inversin del Control. En los o prximos cap o tulos haremos mucho uso de la inyeccin de dependencias con o gran cantidad de listados de cdigo. No se preocupe si el ejemplo no le queo
5

http://www.objectmentor.com/resources/articles/isp.pdf

115

7.3. Inversin del Control (IoC) o

Cap tulo 7

da demasiado claro. El art culo de R. Martin sobre DIP6 es uno de los ms a amenos y divertidos sobre los principios S.O.L.I.D.

7.3.

Inversin del Control (IoC) o

Inversin del Control es sinnimo de Inyeccin de Dependencias (DI). El o o o nombre fue popularizado por el clebre Martin Fowler pero el concepto es e de nales de los aos ochenta. Dado el principio de la inversin de depenn o dencias, nos queda la duda de cmo hacer para que la clase que requiere o colaboradores de tipo abstracto, funcione con instancias concretas. Dicho de otro modo, Quin crea las instancias de los colaboradores? Retomemos el e ejemplo de las clases de antes. Tradicionalmente la clase Logica tendr una a sentencia de tipo myPersistor = new MyDB() dentro de su constructor o de algn otro mtodo interno para crear una instancia concreta, ya que no u e podemos crear instancias de interfaces ni de clases abstractas. En tiempo de compilacin nos vale con tener el contrato pero en tiempo de ejecucin tiene o o que haber alguien que se ponga en el pellejo del contrato. Si lo hacemos as volvemos al problema de tener que modicar la clase Logica para salvar en cheros binarios. La solucin es invertir la forma en que se generan las o instancias. Habr una entidad externa que toma la decisin de salvar en base a o de datos o en cheros y en funcin de eso crea la instancia adecuada y se o la pasa a Logica para que la asigne a su miembro myPersistor. Hay dos formas, como parmetro en el constructor de Logica o bien mediante un a setter o mtodo que unicamente sirve para recibir el parmetro y asignarlo e a al atributo interno. La entidad externa puede ser otra clase o bien puede ser un contenedor de inyeccin de dependencias. o Qu son los IoC Containers? Son la herramienta externa que gestiona e las dependencias y las inyecta donde hacen falta. Los contenedores necesitan de un chero de conguracin o bien de un fragmento de cdigo donde se o o les indica qu entidades tienen dependencias, cules son y qu entidades son e a e independientes. Afortunadamente hay una gran variedad de contenedores libres para todos los lenguajes modernos. Algunos de los ms populares son a Pinsor para Python, Spring Container para Java y .Net, Pico y Nano para Java, Needle y Copland para Ruby y Castle.Windsor para .Net. Habiendo preparado las clases de nuestro ejemplo para la inversin del control, podeo mos especicar al contenedor que inyecte MyDB o FS mediante un chero de conguracin que lee la aplicacin al arrancar y conseguir diferente comporo o tamiento sin tocar una sola l nea de cdigo. Demoledor. Si la aplicacin es o o pequea no necesitamos ningn contenedor de terceros sino que en nuesn u
6

http://www.objectmentor.com/resources/articles/dip.pdf

116

Cap tulo 7

7.3. Inversin del Control (IoC) o

tro propio cdigo podemos inyectar las dependencias como queramos. Los o contenedores son una herramienta pero no son imprescindibles. Su curva de aprendizaje puede ser complicada. En nuestro pequeo ejemplo hemos seguin do la mayor parte de los principios S.O.L.I.D, aunque no hemos entrado a ver qu hacen las clases en detalle pero por lo menos queda una idea ilustrativa e del asunto que nos ocupa. No se asuste, resulta ms sencillo de lo que parece a y sino, TDD no lo va a ir cascando todo, ya ver. a

117

7.3. Inversin del Control (IoC) o

Cap tulo 7

118

Parte II

Ejercicios Prcticos a

119

Captulo

Inicio del proyecto - Test Unitarios


Vamos a escribir nuestra primera aplicacin de ejemplo porque practicano do es como realmente se aprende. En lugar de implementarla por completo y pulirla para luego escribir este cap tulo, vamos a disearla juntos desde n el principio para ver en realidad cmo se razona y se itera en el desarrollo o dirigido por tests. La aplicacin ocupar este cap o a tulo y los siguientes para que tengamos la oportunidad de afrontar toda la problemtica de cualquier a aplicacin real. Sin embargo, no es una aplicacin empresarial como pudiera o o ser un software de facturacin, sino que se basa en un dominio de negocio o que todos conocemos. Se podr argumentar que el software que vamos a a desarrollar no es muy comn o muy real pero incluye todas las piezas que u lleva una aplicacin ms real. Imag o a nese que para entender el cdigo fuente o que nos ocupa tuviese uno que estudiar primero contabilidad o derecho. Tenga conanza en que nuestra aplicacin de ejemplo es perfectamente vlida o a y representa a pequea escala el modo en que se desarrolla una aplicacin n o mucho ms grande. Nos adentramos ya en las vicisitudes de este pequeo a n gran desarrollo de software. Sentmonos con el cliente para escucharle hablar sobre su problema y e hacer un primer anlisis. Lo que nos cuenta es lo siguiente: a

121

Cap tulo 8
' $

Una vez escuchado el discurso del cliente y habiendo decidido que lo primero a implementar en los primeros sprints1 ser el motor de juego de la a aritmtica bsica, nos disponemos a formular los criterios de aceptacin para e a o que el cliente los valide. Tal como dijimos en el cap tulo sobre ATDD, los
1

&

Quiero lanzar al mercado un software educativo para ensear man temticas a nios. Necesito que puedan jugar o practicar a travs a n e de una pgina web pero tambin a travs del telfono mvil y a e e e o quizs ms adelante tambin en la consola Xbox. El juego sera a e vir para que los nios practiquen diferentes temas dentro de las a n matemticas y el sistema debe recordar a cada nio, que tendr un a n a nombre de usuario y una clave de acceso. El sistema registrar toa dos los ejercicios que han sido completados y la puntuacin obo tenida para permitirles subir de nivel si progresan. Existir un a usuario tutor que se registra a la vez que el nio y que tiene la n posibilidad de acceder al sistema y ver estad sticas de juego del nio. El tema ms importante ahora mismo es la aritmtica bsin a e a ca con nmeros enteros. Es el primero que necesito tener listo u para ofrecer a los profesores de enseanza primaria un refuerzo n para sus alumnos en el prximo comienzo de curso. El mdulo o o de aritmtica base incluye las cuatro operaciones bsicas (sumar, e a restar, multiplicar y dividir) con nmeros enteros. Los alumnos u no solo tendrn que resolver los clculos ms elementales sino a a a tambin resolver expresiones con parntesis y/o con varias opee e raciones encadenadas. As aprendern la precedencia de los ope a radores y el trabajo con parntesis: las propiedades distributiva, e asociativa y conmutativa. Los ejercicios estarn creados por el a usuario profesor que introducir las expresiones matemticas en a a el sistema para que su resultado sea calculado automticamente a y almacenado. El profesor decide en qu nivel va cada expresin e o matemtica. En otros ejercicios se le pedir al nio que se invente a a n las expresiones matemticas y les ponga un resultado. El prograa ma dispondr de una calculadora que slo ser accesible para los a o a profesores y los jugadores de niveles avanzados. La calculadora evaluar y resolver las mismas expresiones del sistema de juego. a a Cuando el jugador consigue un cierto nmero de puntos puede u pasar de nivel, en cuyo caso un email es enviado al tutor para que sea informado de los logros del tutelado. El nmero m u nimo de puntos para pasar de nivel debe ser congurable.

En Scrum un sprint es una iteracin o

122

Cap tulo 8 tests de aceptacin (o de cliente) son frases cortas y precisas escritas con el o lenguaje del dominio de negocio. Son tan sencillas que al verlas el cliente slo o tiene que decir si son armaciones correctas o no. Son ejemplos. Los ejemplos evitan la ambigedad que se puede inmiscuir en la descripcin de una tarea. u o Sabemos que es muy peligroso dar por sentado cuestiones referentes a la lgica de negocio, que debemos ceirnos exclusivamente a la funcionalidad o n que se requiere. El motor de juego para aritmtica implica muchos ejemplos e a poco que nos pongamos a pensar. Qu pasa si el profesor no ha aadido e n sucientes expresiones a un nivel como para alcanzar la puntuacin que pero mite pasar el nivel siguiente? se pueden eliminar expresiones de un nivel? se pueden aadir o modicar? cmo afecta eso a las puntuaciones? y si n o hay jugadores en medio del nivel? se puede trasladar una expresin de un o nivel a otro?. Cada pregunta se resolver con varios ejemplos. Tenemos por a delante un buen trabajo de anlisis. Sin embargo, por nes docentes, empea zaremos abordando cuestiones ms simples como el propio funcionamiento a de la calculadora. Los tests de aceptacin iniciales son: o
"2 + 2", devuelve 4 "5 + 4 * 2 / 2", devuelve 9 "3 / 2", produce el mensaje ERROR "* * 4 - 2": produce el mensaje ERROR "* 4 5 - 2": produce el mensaje ERROR "* 4 5 - 2 -": produce el mensaje ERROR "*45-2-": produce el mensaje ERROR

Estos ejemplos de funcionamiento parecen una chorrada pero si no estuvieran ah podr , amos haber pensado que se requer parntesis para denir an e la precedencia de los operadores o que la notacin de los comandos era polaca o inversa2 . A travs de los ejemplos queda claro que los diferentes elementos e del comando se operan segn la precedencia, donde la multiplicacin y la u o divisin se operan antes que la suma y la resta. Y tambin sabemos que o e un resultado con decimales, no se permite porque nuestra aritmtica bsica e a trabaja unicamente con enteros. Otra cosa que queda clara es que los ope radores y operandos de la expresin se separan por un espacio o sea, que las o expresiones son cadenas de texto. Lgicamente hay ms ejemplos pero vamos a empezar la implementacin o a o ya para no dilatar ms el anlisis. As de paso vemos el caso en que descubria a mos nuevos requisitos de negocio cuando ya estamos implementando cdigo, o algo que sucede la gran mayor de las veces. a
2

http://es.wikipedia.org/wiki/Notaci %C3 %B3n polaca inversa

123

Cap tulo 8 Manos a la obra. Abrimos un nuevo proyecto con nuestro IDE favorito por un lado y un editor de textos sencillo por otro. Le recomiendo encarecidamente que lo haga tal cual, literalmente, que vaya escribiendo todos los fragmentos de cdigo fuente que se exponen mientras lee; as ir practicando o a desde ya. Antes de copiar directamente del libro intente pensar por usted mismo cul es el siguiente paso a dar en cada momento. Este ser su primer a a proyecto TDD si lo tiene a bien. Sucede que las decisiones de diseo las esn toy tomando yo y pueden diferir de las que usted tome, lo cual no signica que las suyas sean inapropiadas, sino simplemente diferentes. No se desanime por ello. La limitacin de un libro impreso es su ausencia de interactividad y o no podemos hacer nada para solventar este inconveniente. Si cree que una determinada refactorizacin o decisin de diseo es inapropiada no dude en o o n compartirlo mediante la web del libro. A la pantalla del editor de texto le vamos a llamar libreta y es donde apuntaremos los ejemplos que nos quedan por hacer y todo lo que se nos ocurre mientras estamos escribiendo tests o lgica de negocio: normalmente o cuando estamos concentrados en el test que nos ocupa, es comn que vengan u a la mente casos de uso que no estaban contemplados o bien dudas sobre la lgica de negocio. Esto no debe distraernos de lo que estamos haciendo o ni tampoco inuenciarnos, por eso lo anotamos en la libreta con una frase breve que nos permita volver a retomarlo cuando acabemos. Tenemos que centrarnos en una sola cosa cada vez para llevarla a cabo como es debido. Abrimos tambin la herramienta con que se ejecuta la bater de tests (NUnit e a grco o una consola donde invocar a nunit-console o el script Python a que inicia unittest). T picamente creamos dos mdulos, uno para la aplicacin y otro para su o o bater de tests. Si estamos con C# sern dos DLLs y si estamos con Python a a sern dos paquetes distintos. Para no mezclar los dos lenguajes continuaa mente vamos a escribir primero en C# y luego veremos su contrapartida en Python. Los seguidores de uno de los lenguajes no deben saltarse los cap tulos en los que se usa el otro porque en ambos cap tulos existen explicaciones vlidas para los dos lenguajes y referencias de uno a otro. a Ahora toca escoger uno de los tests de aceptacin y pensar en una lista de o elementos que nos hacen falta para llevarlo a cabo. Hacer un pequeo anlisis n a del ejemplo y tratar de denir al menos tres o cuatro ejemplos ms sencillos a que podamos convertir en tests de desarrollo (unitarios y de integracin o o slo unitarios) para empezar a hacer TDD. Estamos combinando ATDD con o TDD. Los dos elementos que se me ocurren ahora mismo son una clase que sea capaz de operar enteros y un analizador que sea capaz de identicar los distintos elementos de una expresin contenida en una cadena de texto. Si no o tuviera que escribir este cap tulo con nes docentes seguramente empezar a 124

Cap tulo 8 por trabajar en el analizador pero los ejemplos de la clase que hace clculos a van a ser ms fciles de asimilar inicialmente. a a Generalmente los tests de aceptacin se guardan por separado, al margen o de la libreta que contiene las cuestiones relativas a desarrollo. Si empleamos un framework tipo Fit o Concordion los tests de aceptacin tendr su proo an pio lugar pero por simplicar y de nuevo con nes docentes, mantendremos ambas cosas en la libreta. Vamos a agregarle los tests unitarios que nos gustar hacer para resolver el primero de los tests de aceptacin. a o
# Aceptacin - "2 + 2", devuelve 4 o Sumar 2 al nmero 2, devuelve 4 u Restar 3 al nmero 5, devuelve 2 u La cadena "2 + 2"tiene dos nmeros y un operador que son 2, 2 y + u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "* * 4 - 2": produce ERROR o # Aceptacin -"* 4 5 - 2": produce ERROR o # Aceptacin -"* 4 5 - 2 : produce ERROR o # Aceptacin -"*45-2-": produce ERROR o

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

Vamos a por el primer test de desarrollo:


System ; System . Collections . Generic ; System . Text ; NUnit . Framework ; SuperCalculator ;

using using using using using

namespace UnitTests { [ TestFixture ] public class CalculatorTests { [ Test ] public void Add () { Calculator calculator = new Calculator (); int result = calculator . Add (2 , 2); Assert . AreEqual (4 , result ); } } }

El cdigo no compila porque todav no hemos creado la clase Calculator. o a Sin embargo, ya hemos diseado algo, casi sin darnos cuenta: hemos penn sado en el nombre de la clase, en cmo es su constructor y en cmo es su o o primer mtodo (cmo se llama, los parmetros que recibe y el resultado que e o a 125

Cap tulo 8 devuelve). Estamos diseando la API tal cual la necesitamos, sin limitaciones n ni funcionalidad extra. A continuacin escribimos el m o nimo cdigo posible o para que el test pase. Intente imaginar cul es literalmente el m a nimo cdigo o posible para que el test pase. Seguro que es ms largo que el que de verdad a es m nimo:

1 2 3 4 5 6 7 8 9 10

namespace SuperCalculator { public class Calculator { public int Add ( int arg1 , int arg2 ) { return 4; } } }

Qu cdigo ms simpln!, No sirve para nada!. Pues s s que sirve. Al e o a o , seguir el algoritmo TDD y escribir literalmente el m nimo cdigo posible para o que el test pase, descubrimos una cualidad de la funcin: para valores de o entrada diferentes presenta resultados diferentes. Vale vale, todos sab amos eso, en este caso es muy evidente pero y si no tuviramos un conocimiento e tan preciso del dominio?. Cuando no tenemos experiencia con TDD es muy importante que sigamos el algoritmo al pie de la letra porque de lo contrario no llegaremos a exprimir al mximo la tcnica como herramienta de diseo. a e n Ya que el test pasa, es decir, luz verde, necesitamos otro test que nos obligue a terminar de implementar la funcionalidad deseada, puesto que hasta ahora nuestra funcin de suma slo funciona en el caso 2 + 2. Es como si o o estuvisemos moldeando una gura con un cincel. Cada test es un golpe que e moldea el SUT. A este proceso de denir el SUT a golpe de test se le llama triangulacin. o

1 2 3 4 5 6 7

[ Test ] public void A d d W i t h D i f f e r e n t A r g u m e n t s () { Calculator calculator = new Calculator (); int result = calculator . Add (2 , 5); Assert . AreEqual (7 , result ); }

Ejecutamos, observamos que estamos en rojo y acto seguido modicamos el SUT:

1 2 3 4

public int Add ( int arg1 , int arg2 ) { return arg1 + arg2 ; }

Por qu no hemos devuelto 7 directamente que es el cdigo m e o nimo? Porque entonces el test anterior deja de funcionar y se trata de escribir el 126

Cap tulo 8 cdigo m o nimo para que todos los tests tengan resultado positivo. Una vez hemos conseguido luz verde, hay que pensar si existe algn bloque de cdigo u o susceptible de refactorizacin. La manera ms directa de identicarlo es buso a cando cdigo duplicado. En el SUT no hay nada duplicado pero en los tests o s La llamada al constructor de Calculator, (vase l : e nea 4 del ultimo test). Advertimos que la instancia de la calculadora es un xture y por lo tanto puede ser extra como variable de instancia de la clase CalculatorTests. da Eliminamos la duplicidad:
public class CalculatorTests { private Calculator _calculator ; [ SetUp ] public void SetUp () { _calculator = new Calculator (); } [ Test ] public void Add () { int result = _calculator . Add (2 , 2); Assert . AreEqual ( result , 4); } [ Test ] public void A d d W i t h D i f f e r e n t A r g u m e n t s () { int result = _calculator . Add (2 , 5); Assert . AreEqual ( result , 7); }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

'

Usar el SetUp no siempre es la opcin correcta. Si cada uno de o los tests necesitase de instancias de la calculadora distintas para funcionar (por ejemplo haciendo llamadas a diferentes versiones de un constructor sobrecargado), ser conveniente crearlas en a cada uno de ellos en lugar de en la inicializacin. Algunos como o James Newkirk son tajantes en lo que respecta al uso del SetUp y dice que si por l fuera eliminar esta funcionalidad de NUnita . El e a color de este libro no es blanco ni negro sino una escala de grises: haga usted lo que su experiencia le diga que es ms conveniente. a

Vamos a por la resta. Uy!, ya nos bamos directos a escribir el cdigo de o la funcin que resta! Tenemos que estar bien atentos para dirigirnos primero o 127

&

a http://jamesnewkirk.typepad.com/posts/2007/09/why-you-should.html

Cap tulo 8 al test unitario. Al comienzo es muy comn encontrarse inmerso en la imu plementacin de cdigo sin haber escrito un test que falla antes y hay que o o tener sto en cuenta para no desanimarse. La frustracin no nos ayudar. Si e o a se da cuenta de que ha olvidado escribir el test o de que est escribiendo ms a a cdigo del necesario para la correcta ejecucin del test, detngase y vuelva a o o e empezar.

1 2 3 4 5 6

[ Test ] public void Substract () { int result = calculator . Substract (5 , 3); Assert . AreEqual (2 , result ); }

Aqu acabamos de hacer otra decisin de diseo sin advertirlo. Estamos o n deniendo una API donde el orden de los parmetros de los mtodos es ima e portante. En el test que acabamos de escribir asumimos que a 5 se le resta 3 y no al revs. Esto es probablemente tan obvio que no nos lo hemos planteado e tal que as pero si por un momento le hemos dado la vuelta a los parmetros a mentalmente, ha tenido que llegarnos la pregunta de si se aceptan nmeros u negativos como resultado de la operacin. Entonces la apuntamos en la lio breta para no interrumpir lo que estamos haciendo y que no se nos olvide:
Puede ser negativo el resultado de una resta en nuestra calculadora? Confirmar que efectivamente el orden de los parmetros a produce resultados diferentes # Aceptacin - "2 + 2", devuelve 4 o Sumar 2 al nmero 2, devuelve 4 u Restar 3 al nmero 5, devuelve 2 u La cadena "2 + 2"tiene dos nmeros y un operador que son 2, 2 y + u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "* * 4 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2 : produce ERROR o # Aceptacin - "*45-2-": produce ERROR o

Como estamos en rojo vamos a ponernos en verde lo antes posible:

1 2 3 4

public int Substract ( int ag1 , int arg2 ) { return 2; }

De acuerdo, funciona. Ahora necesitamos otro test que nos permita pro128

Cap tulo 8 bar los otros casos de uso de la funcin y miramos a la libreta. Como existe o una duda, nos reunimos con el equipo y lo comentamos, preguntamos al cliente y .... decide que es aceptable devolver nmeros negativos porque al u n y al cabo es como si hubiese un signo menos delante del nmero. Para u aclararlo con un ejemplo se aade un test de aceptacin a la libreta: n o
# Aceptacin - "2 + 2", devuelve 4 o Sumar 2 al nmero 2, devuelve 4 u Restar 3 al nmero 5, devuelve 2 u La cadena "2 + 2"tiene dos nmeros y un operador que son 2, 2 y + u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "* * 4 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2 : produce ERROR o # Aceptacin - "*45-2-": produce ERROR o # Aceptacin - "2 + -2"devuelve 0 o

Y triangulamos. Escribimos el m nimo cdigo necesario para pedirle a la o resta que sea capaz de manejar resultados negativos:

1 2 3 4 5 6

[ Test ] public void S u b s t r a c t R e t u r n i n g N e g a t i v e () { int result = calculator . Substract (3 , 5); Assert . AreEqual ( -2 , result ); }

Busquemos el color verde:

1 2 3 4

public int Substract ( int arg1 , int arg2 ) { return arg1 - arg2 ; }

Y justo al escribir esto se nos viene otra serie de preguntas.

129

Cap tulo 8

# Aceptacin - "2 + 2", devuelve 4 o La cadena "2 + 2"tiene dos nmeros y un operador que son 2, 2 y + u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "* * 4 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2 : produce ERROR o # Aceptacin - "*45-2-": produce ERROR o # Aceptacin - "2 + -2"devuelve 0 o Cual es el nmero ms peque~o que se permite como parmetro? u a n a Y el ms grande? a Qu pasa cuando el resultado es menor que el nmero ms peque~o permitido? e u a n Qu pasa cuando el resultado es mayor que el nmero ms grande permitido? e u a

Como puede apreciar hemos eliminado las cuestiones resueltas de la libreta. Las nuevas cuestiones ataen a todas las operaciones de la calculadora. n Ciertamente no sabemos si la calculadora correr en un ordenador con altas a prestaciones o en un telfono mvil. Quizs no tenga sentido permitir ms e o a a d gitos de los que un determinado dispositivo puede mostrar en pantalla aunque el framework subyacente lo permita. Transformarmos las preguntas en nuevos tests de aceptacin: o

130

Cap tulo 8

# Aceptacin - "2 + 2", devuelve 4 o La cadena "2 + 2"tiene dos nmeros y un operador que son 2, 2 y + u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "* * 4 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2 : produce ERROR o # Aceptacin - "*45-2-": produce ERROR o # Aceptacin - "2 + -2"devuelve 0 o # Aceptacin - Configurar el nmero ms grande o u a que puede usarse como argumento y como resultado # Aceptacin - Configurar el nmero ms peque~o o u a n que puede usarse como argumento y como resultado # Aceptacin - Si el lmite superior es 100 y o alguno de los parmetros o el resultado es mayor que 100, ERROR a # Aceptacin - Si el lmite inferior es -100 y alguno o de los parmetros o el resultado es menor que -100, ERROR a

Simpliquemos las frases para tener unos tests de aceptacin ms claros: o a


# Aceptacin - "2 + 2", devuelve 4 o La cadena "2 + 2"tiene dos nmeros y un operador que son 2, 2 y + u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "* * 4 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2 : produce ERROR o # Aceptacin - "*45-2-": produce ERROR o # Aceptacin - "2 + -2"devuelve 0 o # Aceptacin - Lmite Superior =100 o # Aceptacin - Lmite Superior =500 o # Aceptacin - Lmite Inferior = -1000 o # Aceptacin - Lmite Inferior = -10 o # Aceptacin - Limite Superior=100 y parmetro mayor que 100, produce ERROR o a # Aceptacin - Limite Superior=100 y resultado mayor que 100, produce ERROR o # Aceptacin - Limite Inferior=10 y parmetro menor que 10, produce ERROR o a # Aceptacin - Limite Inferior=10 y resultado menor que 10, produce ERROR o

Ahora tenemos dos caminos. Podemos seguir adelante afrontando la segunda l nea de la libreta, el analizador o podemos resolver la cuestin de los o 131

Cap tulo 8 l mites. En TDD resolvemos el problema como si de un rbol se tratase. La a ra es el test de aceptacin y los nodos hijos son tests de desarrollo, que z o unas veces sern unitarios y otras veces quizs de integracin. Un rbol puea a o a de recorrerse de dos maneras; en profundidad y en amplitud. La decisin la o tomamos en funcin de nuestra experiencia, buscando lo que creemos que o nos va a dar ms benecio, bien en tiempo o bien en prestaciones. No hay a ninguna regla que diga que primero se hace a lo ancho y luego a lo largo. En este caso decidimos poder congurar la calculadora con el nmero u ms grande permitido y el ms pequeo. Vamos a explorar a lo ancho. Es a a n una funcionalidad que invocar amos de esta manera:
[ Test ] public void S u b s t r a c t S e t t i n g L i m i t V a l u e s () { Calculator calculator = new Calculator ( -100 , 100); int result = calculator . Substract (5 , 10); Assert . AreEqual ( -5 , result ); }

1 2 3 4 5 6 7

Estamos en rojo y nisiquiera es posible compilar porque el constructor de la clase Calculator no estaba preparado para recibir parmetros. Hay que a tomar una decisin inmediata que no necesitamos apuntar en la libreta, ya o que sin decidir no podemos ni compilar. El constructor recibe parmetros? Creamos dos versiones del a constructor, con parmetros y sin ellos? a Por un lado si cambiamos el constructor para que acepte argumentos, necesitamos modicar el SetUp porque usa la versin anterior, lo cual nos o recuerda que los tests requieren mantenimiento. Sin embargo, el costo de este cambio es m mino. Por otro lado podemos sobrecargar el constructor para tener ambas variantes pero... qu pasa entonces cuando no indicamos e los valores l mite y se usan argumentos que superan el l mite impuesto por el framework subyacente (en este caso .Net)?. No conviene decidir sin saber qu ocurrir, mejor hacemos la prueba de sumar nmeros muy grandes cuyo e a u resultado excede dicho l mite y observamos qu hace el runtime de .Net e (Int32.MaxValue + 1 por ejemplo). El resultado es el nmero ms pequeo u a n posible, un nmero negativo. Es como si un contador diese la vuelta. Es un u comportamiento muy raro para una calculadora. Nos interesa ms que sea a obligatorio denir los l mites. Bien pues ya podemos modicar el constructor para que admita los l mites y los tests existentes que tras el cambio no compilen, con vistas a conseguir luz verde.
public class Calculator { public Calculator ( int minValue , int maxValue ) { } ...

1 2 3 4

132

Cap tulo 8
}

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

El ultimo test que hab amos escrito (SubstractSettingLimitValues) no tiene nada diferente a los dems porque ya todos denen los l a mites; vamos a modicarlo escogiendo uno de los casos de uso de la lista. Tomamos el caso en que se excede el l mite inferior y decidimos que en tal situacin queremos o lanzar una excepcin de tipo OverflowException. o
[ Test ] public void S u b s t r a c t E x c e d i n g L o w e r L i m i t () { Calculator calculator = new Calculator ( -100 , 100); try { int result = calculator . Substract (10 , 150); Assert . Fail ( " Exception is not being thrown when " + " exceeding lower limit " ); } catch ( OverflowException ) { // Ok , the SUT works as e x p e c t e d } }

1 2 3 4 5 6

Efectivamente el test falla. Si el mtodo Substract hubiese lanzado la e excepcin, sta hubiese sido capturada por el bloque catch que silenciosao e mente hubiese concluido la ejecucin del test. Un test que concluye calladito o como este es un test con luz verde. No es obligatorio que exista una sentencia Assert, aunque es conveniente usarlas para aumentar la legibilidad del cdigo. Para conseguir luz verde ya no vale lanzar la excepcin sin hacer o o ninguna comprobacin porque los otros tests fallar o an. Necesitamos poder consultar los l mites denidos y actuar en consecuencia. Esto nos recuerda que hay l neas en nuestra lista que tenemos que resolver antes que la que nos ocupa. Por tanto dejamos aparcado ste (como el test falla no se nos e olvidar retomarlo, de lo contrario deber a amos apuntarlo en la libreta) y nos encargamos de los casos de denicin y consulta de l o mites:
[ Test ] public void SetAndGetUpperLimit () { Calculator calculator = new Calculator ( -100 , 100); Assert . AreEqual (100 , calculator . UpperLimit ); }

1 2

No compila, hay que denir la propiedad UpperLimit en Calculator. Puesto que la propiedad LowerLimit es exctamente del mismo tipo que a UpperLimit, aqu podemos atrevernos a escribir el cdigo que asigna y re o cupera ambas.
public class Calculator {

133

Cap tulo 8
private int _upperLimit ; private int _lowerLimit ; public int LowerLimit { get { return _lowerLimit ; } set { _lowerLimit = value ; } } public int UpperLimit { get { return _upperLimit ; } set { _upperLimit = value ; } } public Calculator ( int minValue , int maxValue ) { _upperLimit = maxValue ; _lowerLimit = minValue ; }

3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

As tiene sentido aadir otro Assert al test en que estamos trabajando y , n cambiarle el nombre ... No hab amos dicho que era conveniente que un test tuviera un unico Assert y probase una sla cosa? Es que semnticamente o o a funcionalmente ambas propiedades de la clase son para lo mismo, desde el punto de vista del test: asignar valores y recuperar valores de variables de instancia. O sea que no estamos infringiendo ninguna norma. Reconocer qu es e lo que el test est probando es important a simo para separar adecuadamente la funcionalidad en sus respectivos mtodos o clases. Cuando se escribe un e test sin tener claro lo que se pretende, se obtiene un resultado doblemente negativo: cdigo de negocio problemtico y un test dif de mantener. o a cil

1 2 3 4 5 6 7

[ Test ] public void SetAndGetLimits () { Calculator calculator = new Calculator ( -100 , 100); Assert . AreEqual (100 , calculator . UpperLimit ); Assert . AreEqual ( -100 , calculator . LowerLimit ); }

El valor de los tests es que nos obligan a pensar y a descubrir el sentido de lo que estamos haciendo. Escribir tests no debe convertirse en una cuestin o de copiar y pegar, sino en una toma de decisiones. Es por eso que en algunos casos es permisible incluir varios Assert dentro de un mismo test y en otros no; depende de si estamos probando la misma casu stica aplicada a varios elementos o no. Ejecutamos los tests y pasan todos menos SubstractExcedingLowerLimit por lo que nos ponemos manos a la obra y escribimos el m nimo cdigo posible o que le haga funcionar y no rompa los dems. a

public int Substract ( int arg1 , int arg2 )

134

Cap tulo 8
{ int result = arg1 - arg2 ; if ( result < _lowerLimit ) { throw new OverflowException ( " Lower limit exceeded " ); } return result ; }

2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9 10 11 12 13 14

Nos queda probar el caso en el que el resultado excede el l mite superior y los casos en que los argumentos tambin exceden los l e mites. Vamos paso a paso:
[ Test ] public void A d d E x c e d i n g U p p e r L i m i t () { Calculator calculator = new Calculator ( -100 , 100); try { int result = calculator . Add (10 , 150); Assert . Fail ( " This should fail : we re exceding upper limit " ); } catch ( OverflowException ) { // Ok , the SUT works as e x p e c t e d } }

1 2 3 4 5 6 7 8 9

He tomado el mtodo Add en lugar de restar para no olvidar que estas comproe baciones se aplican a todas las operaciones de la calculadora. Implementacin o m nima:
public int Add ( int arg1 , int arg2 ) { int result = arg1 + arg2 ; if ( result > _upperLimit ) { throw new OverflowException ( " Upper limit exceeded " ); } return result ; }

1 2 3 4 5 6

Funciona pero se ve claramente que este mtodo de suma no hace la e comprobacin del l o mite inferior. Es posible que el resultado de una suma sea un nmero menor que el l u mite inferior? Si uno de sus argumentos es un nmero ms pequeo que el propio l u a n mite inferior, entonces es posible. Entonces es el momento de atacar los casos en que los parmetros que se a pasan superan ya de por s los l mites establecidos.
[ Test ] public void A r g u m e n t s E x c e e d L i m i t s () { Calculator calculator = new Calculator ( -100 , 100); try {

135

Cap tulo 8
calculator . Add ( calculator . UpperLimit + 1 , calculator . LowerLimit - 1); Assert . Fail ( " This should fail : arguments exceed lim its " ); } catch ( OverflowException ) { // Ok , this works } }

7 8 9 10 11 12 13 14 15

Este test se asegura de no caer en el caso anterior (el de que el resultado de la suma es inferior al l mite) y aprovecha para probar ambos l mites. Dos comprobaciones en el mismo test, lo cual es vlido porque son realmente la a misma caracter stica. A por el verde:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

public int Add ( int arg1 , int arg2 ) { if ( arg1 > _upperLimit ) throw new OverflowException ( " First argument exceeds upper limit " ); if ( arg2 < _lowerLimit ) throw new OverflowException ( " Second argument exceeds lower limit " ); int result = arg1 + arg2 ; if ( result > _upperLimit ) { throw new OverflowException ( " Upper limit exceeded " ); } return result ; }

Y qu tal a la inversa? e

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

[ Test ] public void A r g u m e n t s E x c e e d L i m i t s I n v e r s e () { Calculator calculator = new Calculator ( -100 , 100); try { calculator . Add ( calculator . LowerLimit -1 , calculator . UpperLimit + 1); Assert . Fail ( " This should fail : arguments exceed lim its " ); } catch ( OverflowException ) { // Ok , this works } }

Pintmoslo de verde!: e

1 2 3 4 5 6

public int Add ( int arg1 , int arg2 ) { if ( arg1 > _upperLimit ) throw new OverflowException ( " First argument exceeds upper limit " ); if ( arg2 < _lowerLimit )

136

Cap tulo 8
throw new OverflowException ( " First argument exceeds lower limit " ); if ( arg1 < _lowerLimit ) throw new OverflowException ( " Second argument exceeds lower limit " ); if ( arg2 > _upperLimit ) throw new OverflowException ( " Second argument exceeds upper limit " ); int result = arg1 + arg2 ; if ( result > _upperLimit ) { throw new OverflowException ( " Upper limit exceeded " ); } return result ; }

7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

La resta deber comportarse igual: a

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

[ Test ] public void A r g u m e n t s E x c e e d L i m i t s O n S u b s t r a c t () { Calculator calculator = new Calculator ( -100 , 100); try { calculator . Substract ( calculator . UpperLimit + 1 , calculator . LowerLimit - 1); Assert . Fail ( " This should fail : arguments exceed lim its " ); } catch ( OverflowException ) { // Ok , this works } }

El test no pasa. Lo ms rpido ser copiar las l a a a neas de validacin de la o suma y pegarlas en la resta. Efectivamente podemos hacerlo, luego ver que los tests pasan y despus observar que existe duplicidad y exige refactorizar. e Esto es lo aconsejable para lo programadores menos experiementados. Sin embargo, algo tan evidente puede ser abreviado en un solo paso por el desarrollador experto. Estamos ante un caso perfecto para refactorizar extrayendo un mtodo: e

1 2 3 4 5 6 7 8 9 10 11 12 13

public bool ValidateArgs ( int arg1 , int arg2 ) { if ( arg1 > _upperLimit ) throw new OverflowException ( " First argument exceeds upper limit " ); if ( arg2 < _lowerLimit ) throw new OverflowException ( " First argument exceeds lower limit " ); if ( arg1 < _lowerLimit ) throw new OverflowException ( " Second argument exceeds lower limit " ); if ( arg2 > _upperLimit ) throw new OverflowException (

137

Cap tulo 8
" Second argument exceeds upper limit " ); return true ; } public int Add ( int arg1 , int arg2 ) { ValidateArgs ( arg1 , arg2 ); int result = arg1 + arg2 ; if ( result > _upperLimit ) { throw new OverflowException ( " Upper limit exceeded " ); } return result ; } public int Substract ( int arg1 , int arg2 ) { ValidateArgs ( arg1 , arg2 ); int result = arg1 - arg2 ; if ( result < _lowerLimit ) { throw new OverflowException ( " Lower limit exceeded " ); } return result ; }

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

Los tests pasan. Queda ms cdigo duplicado?. S todav queda algo en el a o , a SUT y es la l nea que llama al mtodo de validacin pero de eso nos encare o garmos despus. Tener una sola l e nea duplicada no es muy malo... lo es? (la duda es buena querido lector; y va a ser que s que es malo). Estn todos a los casos de uso probados?. La libreta dice:

138

Cap tulo 8

# Aceptacin - "2 + 2", devuelve 4 o La cadena "2 + 2"tiene dos nmeros y un operador que son 2, 2 y + u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "* * 4 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2 : produce ERROR o # Aceptacin - "*45-2-": produce ERROR o # Aceptacin - "2 + -2"devuelve 0 o # Aceptacin - Lmite Superior =100 o # Aceptacin - Lmite Superior =500 o # Aceptacin - Lmite Inferior = -1000 o # Aceptacin - Lmite Inferior = -10 o # Aceptacin - Limite Superior=100 y parmetro mayor que 100, produce ERROR o a # Aceptacin - Limite Superior=100 y resultado mayor que 100, produce ERROR o # Aceptacin - Limite Inferior=10 y parmetro menor que 10, produce ERROR o a # Aceptacin - Limite Inferior=10 y resultado menor que 10, produce ERROR o

Las ultimas l neas albergan mltiples ejemplos y retenerlos todos mentalu mente es peligroso, es fcil que dejemos algunos atrs por lo que expandimos a a la lista:

139

Cap tulo 8

# Aceptacin - "2 + 2", devuelve 4 o La cadena "2 + 2"tiene dos nmeros y un operador que son 2, 2 y + u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "* * 4 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2 : produce ERROR o # Aceptacin - "*45-2-": produce ERROR o # Aceptacin - "2 + -2"devuelve 0 o # Aceptacin - Lmite Superior =100 o # Aceptacin - Lmite Superior =500 o # Aceptacin - Lmite Inferior = -1000 o # Aceptacin - Lmite Inferior = -10 o A: El primer argumento sobrepasa el lmite superior B: El primer argumento sobrepasa el lmite inferior C: El segundo argumento sobrepasa el lmite superior D: El segundo argumento sobrepasa el lmite inferior E: El resultado de una operacin sobrepasa el lmite superior o F: El resultado de una operacin sobrepasa el lmite inferior o Todos los casos de uso anteriores se aplican a todas las operaciones aritmeticas

No hemos probado por completo que la resta valida sus dos argumentos, slo hemos probado los casos A y D restando. Necesitar o amos otro test ms. a Si escribimos dos tests para la validacin en cada operacin aritmtica, vao o e mos a terminar con una cantidad de tests muy grande e intil (porque en u verdad estn todos probando la misma cosa) a base de copiar y pegar. Esto a empieza a oler mal. Cuando se avecina la jugada de copiar y pegar tests a diestro y siniestro, la cosa huele mal. Qu necesitamos probar? Necesitae mos asegurarnos de que el validador valida y de que todas las operaciones aritmticas preguntan al validador. En verdad es sto lo que queremos. Nos e e hemos dado cuenta al identicar un mal olor. De acuerdo, modicamos los dos tests que hacen al validador comprobar los argumentos:

1 2 3 4 5 6 7 8 9

[ Test ] public void A r g u m e n t s E x c e e d L i m i t s () { Calculator calculator = new Calculator ( -100 , 100); try { calculator . ValidateArgs ( calculator . UpperLimit + 1 , calculator . LowerLimit - 1); Assert . Fail ( " This should fail : arguments exceed lim its " );

140

Cap tulo 8
} catch ( OverflowException ) { // Ok , this works } } [ Test ] public void A r g u m e n t s E x c e e d L i m i t s I n v e r s e () { Calculator calculator = new Calculator ( -100 , 100); try { calculator . ValidateArgs ( calculator . LowerLimit -1 , calculator . UpperLimit + 1); Assert . Fail ( " This should fail : arguments exceed lim its " ); } catch ( OverflowException ) { // Ok , this works } }

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

1 2 3 4 5 6 7 8 9

Cmo comprobamos ahora que las operaciones aritmticas validan primero o e sin repetir cdigo? Porque tal como est ahora el test ser el mismo cdigo, o a a o solo que cambiando ValidateArgs por Add o Substract. Lo que queremos validar no es el resultado de las funciones matemticas, que ya est probado a a con otros tests, sino su comportamiento. Y cuando aparece la necesidad de validar comportamiento hay que detenerse un momento y analizar si las clases cumplen el Principio de una sola responsabilidad3 . La clase Calculator se concibi para realizar operaciones aritmticas y ahora tambin est hacieno e e a do validaciones. Tiene ms de una responsabilidad. De hecho el modicador a public con que se deni el mtodo ValidateArgs quedaba bastante rao e ro, cualquiera hubiera dicho que se deber haber denido como privado. A a menudo los mtodos privados son indicadores de colaboracin entre clases, e o es decir, puede que en lugar de denir el mtodo como privado sea ms e a conveniente extraer una clase y hacer que ambas cooperen. Vamos a escribir el primer test que valida la cooperacin entre la calculao dora y el validador incluso aunque todav no hemos separado el cdigo... El a o test siempre primero! y para ello nos servimos del framework Rhino.Mocks.
[ Test ] public void S u b s t r a c t I s U s i n g V a l i d a t o r () { int arg1 = 10; int arg2 = -20; int upperLimit = 100; int lowerLimit = 100; var validatorMock = MockRepository . GenerateStrictMock < LimitsValidator >();
3

Ver Cap tulo 7 en la pgina 111 a

141

Cap tulo 8
validatorMock . Expect ( x = > x . ValidateArgs ( arg1 , arg2 )); Calculator calculator = new Calculator ( validatorMock ); calculator . Add ( arg1 , arg2 ); validatorMock . V e r i f y A l l E x p e c t a t i o n s (); }

10 11 12 13 14 15 16

El cdigo dice que hay un objeto que implementa la interfaz LimitsValidator o y que se espera que se llame a su mtodo ValidateArgs. Crea una instancia e nueva de la calculadora y le inyecta el validador como parmetro en el consa tructor, aunque no es el validador verdadero sino un impostor (un mock). A continuacin se ejecuta la llamada al mtodo de suma y nalmente se le o e pregunta al mock si las expectativas se cumplieron, es decir, si se produjo la llamada tal cual se especic. Hemos decidido modicar el constructor de la o calculadora para tomar una instancia de un validador en lugar de los valores l mite. Al n y al cabo los l mites slo le sirven al validador. Parece que es lo o que quer amos hacer pero... entonces, para comprobar que todas las operaciones aritmticas hablan con el validador tenemos que copiar y pegar este e test y modicarle una l nea? Sigue oliendo mal! Los mtodos de suma y resta no solo estn realizando sus operaciones e a aritmticas respectivas, sino que incluyen una parte extra de lgica de negoe o cio que es la que dice... antes y despus de operar hay que validar. No ser e a mejor si hubiese una clase que coordinase esto?. Desde luego el mal olor del copiar/pegar indica que hay que cambiar algo. Es cierto, si la responsabilidad de la calculadora (la clase Calculator, no la aplicacin) es resolver operao ciones pequeas, que sea otra quien se encargue de operar comandos ms n a complejos. Lo que queremos hacer tiene toda la pinta del patrn Decorador4 . o
'

&
4

En Python los decoradores existen como parte del lenguaje; son funciones que interceptan la llamada al mtodo decorado. En este e sentido tienen ms potencia que los atributos de C# (lo que va a entre corchetes), que no interceptan sino slo marcan. Por lo tano to un decorador a lo Python parece apropiado aqu Sin embargo . s por experiencia que tal herramienta del lenguaje debe limitarse e a funciones muy pequeas que aaden atributos a la funcin decon n o rada. A veces con la propia carga de mdulos Python en memoria o se ejecuta el cdigo de los decoradores con resultados impredecio bles. Adems si el cdigo del decorador va ms all de etiquetar a o a a al decorado, estamos dejando de hacer programacin orientada a o objetos para regresar a la vieja programacin procedimental. o

o n http://es.wikipedia.org/wiki/Decorator (patrn de diseo)

142

Cap tulo 8 En C# tenemos varias alternativas. La ms comn ser que la clase coordia u a nadora implementase la misma interfaz de la calculadora y que tuviese una instancia de la calculadora internamente de manera que envolviese la llamada y le aadiese cdigo5 . Lo malo de esta solucin es que nos lleva de n o o nuevo a mucho cdigo duplicado. Lo ms elegante ser el patrn Proxy6 o a a o para interceptar la llamada. Una opcin es Castle.DynamicProxy2, que es o la base de Rhino.Mocks pero la curva de aprendizaje que conlleva usarlo, aunque es suave nos desv de la materia que estamos tratando, por lo que a vamos a implementar nuestra propia forma de proxy. Vamos a modicar el test anterior para explicar con un ejemplo qu es lo que queremos: e

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

[ Test ] public void C o o r d i n a t e V a l i d a t i o n () { int arg1 = 10; int arg2 = -20; int result = 1000; int upperLimit = 100; int lowerLimit = -100; var validatorMock = MockRepository . GenerateStrictMock < LimitsValidator >(); validatorMock . Expect ( x = > x . SetLimits ( lowerLimit , upperLimit )). Repeat . Once (); validatorMock . Expect ( x = > x . ValidateArgs ( arg1 , arg2 )). Repeat . Once (); var calculatorMock = MockRepository . GenerateStrictMock < BasicCalculator >(); calculatorMock . Expect ( x = > x . Add ( arg1 , arg2 )). Return ( r esult ); validatorMock . Expect ( x = > x . ValidateResult ( result )). Repeat . Once (); CalcProxy calcProxy = new CalcProxy ( validatorMock , calculatorMock , lowerLimit , upperLimit ); calcProxy . BinaryOperation ( calculatorMock . Add , arg1 , arg2 ); validatorMock . V e r i f y A l l E x p e c t a t i o n s (); calculatorMock . V e r i f y A l l E x p e c t a t i o n s (); }

Lo que dice este ejemplo o test es lo siguiente: Existe un validador al cual se invocar mediante los mtodos SetLimits y ValidateArgs consecutivaa e mente (y una sola vez cada uno). Existe una calculadora7 que ejecutar su a operacin de suma y acto seguido el validador chequear el resultado. Haso a
http://www.dofactory.com/Patterns/PatternDecorator.aspx# self1 o n http://es.wikipedia.org/wiki/Proxy (patrn de diseo) 7 Ntese que estamos usando interfaces como punto de partida para la generacin de o o los mocks; el principal motivo es que as nos aseguramos que no se ejecuta la llamada en ninguna clase particular sino slo en el mock o
6 5

143

Cap tulo 8 ta ah hemos denido las expectativas. Ahora decimos que hay un proxy (CalcProxy) que recibe como parmetros de su constructor al validador, la a calculadora y los l mites mximos permitidos para las operaciones aritmtia e cas. Queremos que exista un mtodo BinaryOperation donde se indique e el mtodo de la calculadora a invocar y sus parmetros. Finalmente verie a camos que la ejecucin del proxy ha satisfecho las expectativas denidas. o Complicado ,no? Como vimos en el cap tulo anterior, el test es realmente frgil. Cuenta con a todo lujo de detalles lo que hace el SUT. Es como si quisiramos implemene tarlo. Personalmente descarto esta opcin. Pensar en este test y escribirlo me o ha ayudado a pensar en el diseo pero he ido demasiado lejos. Si puedo evitar n los mocks en este punto mejor y como ninguna de las operaciones requeridas infringen las reglas de los tests unitarios, voy a seguir utilizando validacin o de estado. Es momento de replantearse la situacin. o De qu manera podemos probar que el supuesto proxy colabora con e validador y calculadora sin usar mocks? Respuesta: Podemos ejercitar toda la funcionalidad de que disponemos a travs del proxy y jarnos en que no e haya duplicidad. Si no hay duplicidad y todos los casos de uso se gestionan mediante el proxy, entonces tiene que ser que est trabajando bien. Plantearlo a as nos supone el esfuerzo de mover tests de sitio. Por ejemplo los de suma y resta los quitar amos de la calculadora y los pondr amos en el proxy, ya que no los vamos a tener por duplicado. Empecemos por implementar primero el test de suma en el proxy:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

[ TestFixture ] public class CalcProxyTests { private Calculator _calculator ; private CalcProxy _calcProxy ; [ Test ] public void Add () { _calculator = new Calculator (); _calcProxy = new CalcProxy ( _calculator ); int result = _calcProxy . BinaryOperation ( _calculator . Add , 2 , 2); Assert . AreEqual (4 , result ); } }

Por cierto, hemos eliminado el test de suma del conjunto CalculatorTests (para no duplicar). De la clase Calculator he movido las propiedades de l mite inferior y l mite superior a una clase Validator junto con el mtodo e ValidateArgs por si en breve los reutilizase. El SUT m mimo es:

1 2

public class CalcProxy {

144

Cap tulo 8
private BasicCalculator _calculator ; public CalcProxy ( BasicCalculator calculator ) { _calculator = calculator ; } public int BinaryOperation ( S i n g l e B i n a r y O p e r a t i o n operation , int arg1 , int arg2 ) { return _calculator . Add ( arg1 , arg2 ); } }

3 4 5 6 7 8 9 10 11 12 13 14 15 16

He decidido que el primer parmetro del SUT es un delegado: a


1

public delegate int S i n g l e B i n a r y O p e r a t i o n ( int a , int b );

'

1 2 3 4 5 6 7

En el cap tulo 11 repetiremos la implementacin con TDD pero sobre Python, o as que no se preocupe si algo no le queda del todo claro. Sern los mismos a casos que en este cap tulo pero marcados por las particularidades de Python. Adems en el cap a tulo 9 continuamos trabajando con TDD, avanzando en la implementacin de la solucin. o o Vamos a triangular el proxy trasladando el test de la resta hasta l: e
[ TestFixture ] public class CalcProxyTests { private Calculator _calculator ; private CalcProxy _calcProxy ; [ SetUp ]

&

En lugar de pasar una funcin como primer parmetro de o a BinaryOperation podr amos haber usado una cadena de texto (Add) pero la experiencia nos dice que las cadenas son frgiles y a hacen el cdigo propenso a errores dif o ciles de corregir y detectar. Si la persona que se est enfrentando a estas decisiones tuviese a poca experiencia y hubiese decidido utilizar cadenas, igualmente tendr muchas ventajas al usar TDD. Seguramente su cdigo ina o cluir un gran bloque switch-case para actuar en funcin de a o las cadenas de texto y en algn momento pudiera ser que tuviese u que reescribir funciones pero al tener toda una bater de pruebas a detrs, tales cambios ser menos peligrosos, le dar mucha a an an ms conanza. As aunque TDD no nos da siempre la respuesta a , a cul es la mejor decisin de diseo, nos echa una mano cuando a o n tenemos que retroceder y enmendar una decisin problemtica. o a

145

Cap tulo 8
public void SetUp () { _calculator = new Calculator (); _calcProxy = new CalcProxy ( _calculator ); } [ Test ] public void Add () { int result = _calcProxy . BinaryOperation ( _calculator . Add , 2 , 2); Assert . AreEqual (4 , result ); } [ Test ] public void Substract () { int result = _calcProxy . BinaryOperation ( _calculator . Substract , 5 , 3); Assert . AreEqual (2 , result ); } }

8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

Ya est ms dif buscar el cdigo m a a cil o nimo para que los dos tests pasen. No vamos a escribir un bloque condicional para conseguir luz verde porque eso no triangula a ninguna parte. Es hora de implementar algo ms serio. a

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

public int BinaryOperation ( S i n g l e B i n a r y O p e r a t i o n operation , int arg1 , int arg2 ) { int result = 0; MethodInfo [] calcultatorMethods = _calculator . GetType (). GetMethods ( BindingFlags . Publi c | BindingFlags . Instance ); foreach ( MethodInfo method in calcultatorMethods ) { if ( method == operation . Method ) { result = ( int ) method . Invoke ( _calculator , new Object [] { arg1 , arg2 }); } } return result ; }

Hemos usado un poco de magia para buscar dinmicamente el a mtodo de la clase calculadora que toca invocar. Los dos tests pasan y ya han e sido eliminados del conjunto en que se encontraban inicialmente. Estamos empezando a notar que reescribir no cuesta mucho cuando hacemos TDD. Una pregunta frecuente de quienes comienzan a aprender TDD es si los tests se pueden modicar. Aqu estamos viendo claramente que s Se pueden . modicar tantas veces como haga falta porque un test es cdigo vivo, tan o
8

Reection8

http://msdn.microsoft.com/es-es/library/system.reection(VS.95).aspx

146

Cap tulo 8 importante como el SUT. Lo unico inamovible es el test de aceptacin porque o ha sido denido por el cliente. Al menos es inamovible hasta la siguiente reunin de n de sprint con el cliente (sprint si usamos Scrum). Terminemos o de mover los tests de calculadora al proxy:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

[ TestFixture ] public class CalcProxyTests { private Calculator _calculator ; private CalcProxy _calcProxy ; [ SetUp ] public void SetUp () { _calculator = new Calculator (); _calcProxy = new CalcProxy ( _calculator ); } [ Test ] public void Add () { int result = _calcProxy . BinaryOperation ( _calculator . Add , 2 , 2); Assert . AreEqual (4 , result ); } [ Test ] public void Substract () { int result = _calcProxy . BinaryOperation ( _calculator . Substract , 5 , 3); Assert . AreEqual (2 , result ); } [ Test ] public void A d d W i t h D i f f e r e n t A r g u m e n t s () { int result = _calcProxy . BinaryOperation ( _calculator . Add , 2 , 5); Assert . AreEqual (7 , result ); } [ Test ] public void S u b s t r a c t R e t u r n i n g N e g a t i v e () { int result = _calcProxy . BinaryOperation ( _calculator . Substract , 3 , 5); Assert . AreEqual ( -2 , result ); } }

Perfecto, todos pasan estupendamente con un esfuerzo m nimo. Repasemos la libreta:

147

Cap tulo 8

# Aceptacin - "2 + 2", devuelve 4 o La cadena "2 + 2"tiene dos nmeros y un operador que son 2, 2 y + u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "* * 4 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2 : produce ERROR o # Aceptacin - "*45-2-": produce ERROR o # Aceptacin - "2 + -2"devuelve 0 o # Aceptacin - Lmite Superior =100 o # Aceptacin - Lmite Superior =500 o # Aceptacin - Lmite Inferior = -1000 o # Aceptacin - Lmite Inferior = -10 o A: El primer argumento sobrepasa el lmite superior B: El primer argumento sobrepasa el lmite inferior C: El segundo argumento sobrepasa el lmite superior D: El segundo argumento sobrepasa el lmite inferior E: El resultado de una operacin sobrepasa el lmite superior o F: El resultado de una operacin sobrepasa el lmite inferior o Todos los casos de uso anteriores se aplican a todas las operaciones aritmticas e

Hab amos llegado a este punto para coordinar la validacin de argumentos o y resultados. No vamos a implementar el validador con su propio conjunto de tests para luego moverlos al proxy sino que ya con las ideas claras y el diseo ms denido podemos ejercitar el SUT desde el proxy: n a

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

[ Test ] public void A r g u m e n t s E x c e e d L i m i t s () { CalcProxy calcProxyWithLimits = new CalcProxy ( new Validator ( -10 , 10) , _calculator ); try { _calcProxy . BinaryOperation ( _calculator . Add , 30 , 50); Assert . Fail ( " This should fail as arguments exceed both limits " ); } catch ( OverflowException ) { // Ok , this works } }

He decidido que el proxy tiene un constructor que recibe al validador y a la 148

Cap tulo 8 calculadora. Al validador se le indican los valores l mite v constructor. El a SUT:
public int BinaryOperation ( S i n g l e B i n a r y O p e r a t i o n operation , int arg1 , int arg2 ) { _validator . ValidateArgs ( arg1 , arg2 ); int result = 0; MethodInfo [] calcultatorMethods = _calculator . GetType (). GetMethods ( BindingFlags . Publi c | BindingFlags . Instance ); foreach ( MethodInfo method in calcultatorMethods ) { if ( method == operation . Method ) { result = ( int ) method . Invoke ( _calculator , new Object [] { arg1 , arg2 }); } } return result ; }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

El mtodo simplemente aade una l e n nea al cdigo anterior. Para que el test o pase rescatamos el mtodo de validacin que ten e o amos guardado en el validador.
public class Validator : LimitsValidator { private int _upperLimit ; private int _lowerLimit ; public Validator ( int lowerLimit , int upperLimit ) { SetLimits ( lowerLimit , upperLimit ); } public int LowerLimit { get { return _lowerLimit ; } set { _lowerLimit = value ; } } public int UpperLimit { get { return _upperLimit ; } set { _upperLimit = value ; } } public void ValidateArgs ( int arg1 , int arg2 ) { if ( arg1 > _upperLimit ) throw new OverflowException ( " ERROR " ); if ( arg2 > _upperLimit ) throw new OverflowException ( " ERROR " ); } public void SetLimits ( int lower , int upper )

149

Cap tulo 8
{ _lowerLimit = lower ; _upperLimit = upper ; } }

32 33 34 35 36


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

Nos queda probar el l mite inferior.


[ Test ] public void A r g u m e n t s E x c e e d L i m i t s I n v e r s e () { CalcProxy calcProxyWithLimits = new CalcProxy ( new Validator ( -10 , 10) , _calculator ); try { calcProxyWithLimits . BinaryOperation ( _calculator . Add , -30 , -50); Assert . Fail ( " This should fail as arguments exceed both limits " ); } catch ( OverflowException ) { // Ok , this works } }


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

El SUT junto con su posterior refactoring:


public class Validator : LimitsValidator { private int _upperLimit ; private int _lowerLimit ; public Validator ( int lowerLimit , int upperLimit ) { SetLimits ( lowerLimit , upperLimit ); } public int LowerLimit { get { return _lowerLimit ; } set { _lowerLimit = value ; } } public int UpperLimit { get { return _upperLimit ; } set { _upperLimit = value ; } } public void ValidateArgs ( int arg1 , int arg2 ) { breakIfOverflow ( arg1 , " First argument exceeds limits " ); breakIfOverflow ( arg2 , " Second argument exceeds limit s " ); } private void breakIfOverflow ( int arg , string msg ) {

150

Cap tulo 8
if ( ValueExceedLimits ( arg )) throw new OverflowException ( msg ); } public bool ValueExceedLimits ( int arg ) { if ( arg > _upperLimit ) return true ; if ( arg < _lowerLimit ) return true ; return false ; } public void SetLimits ( int lower , int upper ) { _lowerLimit = lower ; _upperLimit = upper ; } }

31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49

Ya podemos quitar de la libreta unas cuantas l neas:


# Aceptacin - "2 + 2", devuelve 4 o La cadena "2 + 2"tiene dos nmeros y un operador que son 2, 2 y + u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "* * 4 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2 : produce ERROR o # Aceptacin - "*45-2-": produce ERROR o # Aceptacin - "2 + -2"devuelve 0 o E: El resultado de una operacin sobrepasa el lmite superior o F: El resultado de una operacin sobrepasa el lmite inferior o Todos los casos de uso anteriores se aplican a todas las operaciones aritmticas e

Solo nos queda validar el resultado. Los dos ejemplos y su implementacin o son inmediatos. Pero siempre de uno en uno:

1 2 3 4 5 6 7 8 9 10 11

[ Test ] public void V a l i d a t e R e s u l t E x c e e d i n g U p p e r L i m i t () { try { _ c a l c P r o x y W i t h L i m i t s . BinaryOperation ( _calculator . Add , 10 , 10); Assert . Fail ( " This should fail as result exceed upper limit " ); } catch ( OverflowException )

151

Cap tulo 8
{ // Ok , this works } }

12 13 14 15


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

8.1: CalcProxy

public int BinaryOperation ( S i n g l e B i n a r y O p e r a t i o n operation , int arg1 , int arg2 ) { _validator . ValidateArgs ( arg1 , arg2 ); int result = 0; MethodInfo [] calcultatorMethods = _calculator . GetType (). GetMethods ( BindingFlags . Publi c | BindingFlags . Instance ); foreach ( MethodInfo method in calcultatorMethods ) { if ( method == operation . Method ) { result = ( int ) method . Invoke ( _calculator , new Object [] { arg1 , arg2 }); } } _validator . ValidateResult ( result ); return result ; }

Le hemos aadido una l n nea al mtodo para validar el resultado. El resto del e SUT en el validador:

1 2 3 4

8.2: Validator

public void ValidateResult ( int result ) { breakIfOverflow ( result , " Result exceeds limits " ); }


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

[ Test ] public void V a l i d a t e R e s u l t E x c e e d i n g L o w e r L i m i t () { try { _ c a l c P r o x y W i t h L i m i t s . BinaryOperation ( _calculator . Add , -20 , -1); Assert . Fail ( " This should fail as result exceed lower limit " ); } catch ( OverflowException ) { // Ok , this works } }

Para este ultimo test ni siquiera ha hecho falta tocar el SUT. La libreta queda as : 152

Cap tulo 8

# Aceptacin - "2 + 2", devuelve 4 o La cadena "2 + 2"tiene dos nmeros y un operador que son 2, 2 y + u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "* * 4 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2 : produce ERROR o # Aceptacin - "*45-2-": produce ERROR o # Aceptacin - "2 + -2"devuelve 0 o

Para recapitular un poco veamos de nuevo todos los tests que hemos escrito hasta el momento, que han quedado bajo el mismo conjunto de tests, CalcProxyTests. Al nal no hemos utilizado ningn doble de test y todos u son tests unitarios puesto que cumplen todas sus reglas.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

using using using using using using

System ; System . Collections . Generic ; System . Text ; NUnit . Framework ; // only nunit . f r a m e w o r k dll is r e q u i r e d SuperCalculator ; Rhino . Mocks ;

namespace UnitTests { [ TestFixture ] public class CalcProxyTests { private Calculator _calculator ; private CalcProxy _calcProxy ; private CalcProxy _ c a l c P r o x y W i t h L i m i t s ; [ SetUp ] public void SetUp () { _calculator = new Calculator (); _calcProxy = new CalcProxy ( new Validator ( -100 , 100) , _calculator ); _calcProxyWithLimits = new CalcProxy ( new Validator ( -10 , 10) , _calculator ); } [ Test ] public void Add () { int result = _calcProxy . BinaryOperation ( _calculator . Add , 2 , 2); Assert . AreEqual (4 , result ); } [ Test ]

153

Cap tulo 8
public void Substract () { int result = _calcProxy . BinaryOperation ( _calculator . Substract , 5 , 3); Assert . AreEqual (2 , result ); } [ Test ] public void A d d W i t h D i f f e r e n t A r g u m e n t s () { int result = _calcProxy . BinaryOperation ( _calculator . Add , 2 , 5); Assert . AreEqual (7 , result ); } [ Test ] public void S u b s t r a c t R e t u r n i n g N e g a t i v e () { int result = _calcProxy . BinaryOperation ( _calculator . Substract , 3 , 5); Assert . AreEqual ( -2 , result ); } [ Test ] public void A r g u m e n t s E x c e e d L i m i t s () { try { _ c a l c P r o x y W i t h L i m i t s . BinaryOperation ( _calculator . Add , 30 , 50); Assert . Fail ( " This should fail as arguments exceed both limits " ); } catch ( OverflowException ) { // Ok , this works } } [ Test ] public void A r g u m e n t s E x c e e d L i m i t s I n v e r s e () { try { _ c a l c P r o x y W i t h L i m i t s . BinaryOperation ( _calculator . Add , -30 , -50); Assert . Fail ( " This should fail as arguments exceed both limits " ); } catch ( OverflowException ) { // Ok , this works } } [ Test ] public void V a l i d a t e R e s u l t E x c e e d i n g U p p e r L i m i t ()

37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95

154

Cap tulo 8
{ try { _ c a l c P r o x y W i t h L i m i t s . BinaryOperation ( _calculator . Add , 10 , 10); Assert . Fail ( " This should fail as result exceed upper limit " ); } catch ( OverflowException ) { // Ok , this works } } [ Test ] public void V a l i d a t e R e s u l t E x c e e d i n g L o w e r L i m i t () { try { _ c a l c P r o x y W i t h L i m i t s . BinaryOperation ( _calculator . Add , -20 , -1); Assert . Fail ( " This should fail as result exceed upper limit " ); } catch ( OverflowException ) { // Ok , this works } } } }

96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126

Es un buen momento para hacer un commit en el sistema de control de versiones y cerrar el cap tulo. Puede encontrar un archivo comprimido con el estado actual del proyecto en la web, para que lo pueda revisar si lo desea. En prximos cap o tulos podr amos hacer modicaciones sobre las clases actuales, por eso el archivo contiene expresamente la versin que hemos desarrollado o hasta aqu . En el prximo cap o tulo continuaremos el desarrollo de la Supercalculadora con C#, para seguir profundizando en la tcnica del diseo dirigido por e n ejemplos o TDD. En el cap tulo 11 implementaremos lo mismo desde el inicio con Python.

155

Cap tulo 8

156

Captulo

Continuacin del proyecto - Test o Unitarios


En el ultimo cap tulo llegamos a conseguir que nuestra calculadora sumase y restase teniendo en cuenta los valores l mite de los parmetros y del a resultado. Continuamos el desarrollo por donde lo dejamos atendiendo a lo que pone la libreta:
# Aceptacin - "2 + 2", devuelve 4 o La cadena "2 + 2"tiene dos nmeros y un operador que son 2, 2 y + u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "* * 4 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2 : produce ERROR o # Aceptacin - "*45-2-": produce ERROR o # Aceptacin - "2 + -2", devuelve 0 o

Es el momento de evaluar la cadena de texto que se utiliza para introducir expresiones y conectarla con la funcionalidad que ya tenemos. Empezamos a disear partiendo de un ejemplo, como siempre: n

1 2 3 4 5 6 7 8

[ TestFixture ] public class ParserTests { [ Test ] public void GetTokens () { MathParser parser = new MathParser (); List < MathToken > tokens = parser . GetTokens ( " 2 + 2 " );

157

Cap tulo 9

9 10 11 12 13 14 15

Assert . AreEqual (3 , tokens . Count ); Assert . AreEqual ( " 2 " , tokens [0]. Token ); Assert . AreEqual ( " + " , tokens [1]. Token ); Assert . AreEqual ( " 2 " , tokens [2]. Token ); } }

Acabo de tomar varias decisiones de diseo: MathParser es una clase con n un mtodo GetTokens que recibe la expresin como una cadena y devuelve e o una lista de objetos tipo MathToken. Tales objetos todav no existen pero a preero pensar en la expresin como en una lista de objetos en lugar de o una lista de cadenas. La experiencia me dice que devolver cadenas no me har progresar mucho. La implementacin m a o nima para alcanzar verde:

1 2 3 4 5 6 7 8 9 10 11 12 13

public class MathParser { public List < MathToken > GetTokens ( string expression ) { List < MathToken > tokens = new List < MathToken >(); tokens . Add ( new MathToken ( " 2 " )); tokens . Add ( new MathToken ( " + " )); tokens . Add ( new MathToken ( " 2 " )); return tokens ; } }

La simplicidad de este SUT nos sirve para traer varias preguntas a la mente. Afortunadamente las respuestas ya se encuentran en la libreta: sabemos qu expresiones son vlidas y cuales no. Adems sabemos que en caso de ene a a contrar una cadena incorrecta lanzaremos una excepcin. Podr o amos triangular hacia el reconocimiento de las expresiones con sentencias y bloques de cdigo varios pero las expresiones regulares son la mejor opcin llegado o o este punto. En lugar de construir de una vez la expresin regular que valio da todo tipo de expresiones matemticas, vamos a triangular paso a paso. a Una expresin regular compleja nos puede llevar d de trabajo depurando. Si o as construimos la expresin basndonos en pequeos ejemplos que vayan casano a n do con cada subexpresin regular, ms tarde, su modicacin y sosticacin, o a o o nos resultara mas sencilla. TDD es ideal para disear expresiones regulares n si mantenemos la mxima de escribir un test exclusivo para cada posible caa dena vlida. Vamos con el ejemplo que construir la primera versin de la a a o expresin regular: o

1 2 3 4 5

[ Test ] public void V a l i d a t e M o s t S i m p l e E x p r e s s i o n () { string expression = " 2 + 2 " ; bool result = _parser . IsExpressionValid ( expression );

158

Cap tulo 9

6 7 8

Assert . IsTrue ( result ); }

En lugar de un mtodo void me ha parecido mejor idea que devuelva verdae dero o falso para facilitar la implementacin de los tests. o En vez de retornar verdadero directamente podemos permitirnos construir la expresin regular ms sencilla que resuelve el ejemplo: o a

1 2 3 4 5

9.1: MathParser

public bool IsExpressionValid ( string expression ) { Regex regex = new Regex ( @ " \ d \+ \ d " ); return regex . IsMatch ( expression ); }


1 2 3 4 5 6 7 8

Qu tal se comporta con nmeros de ms de una cifra? e u a


[ Test ] public void V a l i d a t e M o r e T h a n O n e D i g i t E x p r e s s i o n () { string expression = " 25 + 287 " ; bool result = _parser . IsExpressionValid ( expression ); Assert . IsTrue ( result ); }

1 2 3 4 5 6 7 8 9 10 11 12 13

Funciona! No hemos tenido que modicar el SUT. Ahora vamos a probar con los cuatro operadores aritmticos. En lugar de hacer cuatro tests nos e damos cuenta de que la expresin que queremos probar es la misma, aunque o variando el operador. Eso nos da permiso para agrupar los cuatro usos en un solo ejemplo:
[ Test ] public void V a l i d a t e S i m p l e E x p r e s s i o n W i t h A l l O p e r a t o r s () { string operators = " + -*/ " ; string expression = String . Empty ; foreach ( char operatorChar in operators ) { expression = " 2 " + operatorChar + " 2 " ; Assert . IsTrue ( _parser . IsExpressionValid ( expression ) , " Failure with operator : " + operatorChar ); } }

El peligro de este ejemplo es que estemos construyendo mal la cadena, en cuyo caso disear n amos mal el SUT. Despus de escribirlo la he mostrado por e consola para asegurarme que era la que quer En mi opinin merece la pena a. o asumir el riesgo para agrupar tests de una forma ordenada. Fijse que en el e Assert he aadido una explicacin para que sea ms sencilla la depuracin n o a o de bugs. 159

Cap tulo 9 Incrementamos la expresin regular para hacer el test pasar. o

1 2 3 4 5 6

9.2: MathParser

public bool IsExpressionValid ( string expression ) { Regex regex = new Regex ( @ " \ d [+|\ -|/|*] \ d " ); return regex . IsMatch ( expression ); }

El test pasa. Podemos eliminar el primero que hab amos escrito (ValidateMostSimpleExpression) ya que est contenido en el ultimo. Es importante a recordar que el cdigo de los tests es tan importante como el del SUT y que o por tanto debemos cuidarlo y mantenerlo. Me asalta una duda... podr haber varios espacios entre los distintos a elementos de la expresin? Preguntamos, nos conrman que s es posible y o anotamos en la libreta.
# Aceptacin - "2 + 2", devuelve 4 o La cadena "2 + 2"tiene dos nmeros y un operador que son 2, 2 y + u Se permiten varios espacios entre smbolos # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "* * 4 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2 : produce ERROR o # Aceptacin - "*45-2-": produce ERROR o # Aceptacin - "2 + -2", devuelve 0 o

1 2 3 4 5 6 7

De acuerdo, probmoslo: e

[ Test ] public void ValidateWithSpaces () { string expression = " 2 + 287 " ; bool result = _parser . IsExpressionValid ( expression ); Assert . IsTrue ( result ); }

Mejoramos la expresin regular: o

1 2 3 4 5 6

9.3: MathParser

public bool IsExpressionValid ( string expression ) { Regex regex = new Regex ( @ " \ d \ s +[+|\ -|/|*]\ s +\ d " ); return regex . IsMatch ( expression ); }

160

Cap tulo 9 Estar cubierto el caso en que no se dejan espacios? a


[ Test ] public void V a l i d a t e F a i l s N o S p a c e s () { string expression = " 2+7 " ; bool result = _parser . IsExpressionValid ( expression ); Assert . IsFalse ( result ); }

1 2 3 4 5 6 7

1 2 3 4 5 6 7 8

Pues s funciona sin que tengamos que tocar el SUT. Escogemos nuevas , expresiones de la libreta:
[ Test ] public void V a l i d a t e C o m p l e x E x p r e s s i o n () { string expression = " 2 + 7 - 2 * 4 " ; bool result = _parser . IsExpressionValid ( expression ); Assert . IsTrue ( result ); }

1 2 3 4 5 6 7 8

Vaya, esta pasa incluso sin haber modicado la expresin regular. Resulta o que, como una subcadena de la expresin casa, nos la est dando por buena. o a Busquemos un test que nos obligue a modicar la expresin regular: o
[ Test ] public void V a l i d a t e C o m p l e x W r o n g E x p r e s s i o n () { string expression = " 2 + 7 a 2 b 4 " ; bool result = _parser . IsExpressionValid ( expression ); Assert . IsFalse ( result ); }


1 2 3 4 5 6 7

9.4: MathParser

public bool IsExpressionValid ( string expression ) { Regex regex = new Regex ( @ " ^\ d ((\ s +)[+|\ -|/|*](\ s +)\ d )+ $ " ); return regex . IsMatch ( expression , 0); }

Algunos tests que antes funcionaban estn fallando. Vamos a retocar ms la a a expresin regular: o

1 2 3 4 5 6 7

9.5: MathParser

public bool IsExpressionValid ( string expression ) { Regex regex = new Regex ( @ " ^\ d +((\ s +)[+|\ -|/|*](\ s +)\ d +)+ $ " ); return regex . IsMatch ( expression , 0); }

161

Cap tulo 9 El hecho de que algunos otros tests se hubieran roto me ha creado cierta desconanza. Vamos a probar unas cuantas expresiones ms para vericar a que nuestra validacin es buena. o
[ Test ] public void V a l i d a t e S i m p l e W r o n g E x p r e s s i o n () { string expression = " 2 a7 " ; bool result = _parser . IsExpressionValid ( expression ); Assert . IsFalse ( result ); }

1 2 3 4 5 6 7 8


1 2 3 4 5 6 7 8

El test pasa. A por otro caso:


[ Test ] public void V a l i d a t e W r o n g E x p r e s s i o n W i t h V a l i d S u b e x p r e s s i o n () { string expression = " 2 + 7 - 2 a 3 b " ; bool result = _parser . IsExpressionValid ( expression ); Assert . IsFalse ( result ); }


1 2 3 4 5 6 7 8

Tambin funciona. Qu tal con dos operadores consecutivos? e e


[ Test ] public void V a l i d a t e W i t h S e v e r a l O p e r a t o r s T o g e t h e r () { string expression = " + + 7 " ; bool result = _parser . IsExpressionValid ( expression ); Assert . IsFalse ( result ); }

1 2 3 4 5

Correcto, luz verde. La expresin que nos queda por probar de las que tiene o la libreta es aquella que contiene nmeros negativos: u
[ Test ] public void V a l i d a t e W i t h N e g a t i v e N u m e r s () { Assert . IsTrue ( _parser . IsExpressionValid ( " -7 + 1 " )); }

He aprovechado para simplicar el test sin que pierda legibilidad. Por cierto, est en rojo; hay que retocar la expresin regular. a o

1 2 3 4 5 6

9.6: MathParser

public bool IsExpressionValid ( string expression ) { Regex regex = new Regex ( @ " ^ -{0 ,1}\ d +((\ s +)[+|\ -|/|*](\ s +) -{0 ,1}\ d +)+ $ " ); return regex . IsMatch ( expression , 0); }

Funciona. Probemos alguna variante: 162

Cap tulo 9

1 2 3 4 5

[ Test ] public void V a l i d a t e W i t h N e g a t i v e N u m e r s A t T h e E n d () { Assert . IsTrue ( _parser . IsExpressionValid ( " 7 - -1 " )); }


1 2 3 4 5 6

Sigue funcionando. Vamos a por la ultima prueba del validador de expresiones.


[ Test ] public void V a l i d a t e S u p e r C o m p l e x E x p r e s s i o n () { Assert . IsTrue ( _parser . IsExpressionValid ( " -7 - -1 * 2 / 3 + -5 " )); }

Me da la sensacin de que nuestro validador de expresiones ya es sucienteo mente robusto. Contiene toda la funcionalidad que necesitamos por ahora. Dnde estbamos? o a
# Aceptacin - "2 + 2", devuelve 4 o La cadena "2 + 2"tiene dos nmeros y un u operador que son 2, 2 y + # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "* * 4 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2": produce ERROR o # Aceptacin - "* 4 5 - 2 : produce ERROR o # Aceptacin - "*45-2-": produce ERROR o # Aceptacin - "2 + -2", devuelve 0 o

1 2 3 4 5 6 7 8 9

Ah si!, le estbamos pidiendo al analizador que nos devolviera una lista a con los elementos de la expresin. Hab o amos hecho pasar un test con una implementacin m o nima pero no llegamos a triangular:
[ Test ] public void G e t T o k e n s L o n g E x p r e s s i o n () { List < MathToken > tokens = _parser . GetTokens ( " 2 - 1 + 3 " ); Assert . AreEqual (5 , tokens . Count ); Assert . AreEqual ( " + " , tokens [3]. Token ); Assert . AreEqual ( " 3 " , tokens [4]. Token ); }

Ntese que no repetimos las armaciones referentes a los tokens 0, 1 y 2 que o ya se hicieron en el test anterior para una expresin que es casi igual a la o actual.

9.7: MathParser 163

Cap tulo 9
public List < MathToken > GetTokens ( string expression ) { List < MathToken > tokens = new List < MathToken >(); String [] items = expression . Split ( ); foreach ( String item in items ) { tokens . Add ( new MathToken ( item )); } return tokens ; }

1 2 3 4 5 6 7 8 9 10

Tengo la sensacin de que la clase Parser empieza a tener demasiadas reso ponsabilidades. Refactoricemos:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

public class MathLexer { public List < MathToken > GetTokens ( string expression ) { List < MathToken > tokens = new List < MathToken >(); String [] items = expression . Split ( ); foreach ( String item in items ) { tokens . Add ( new MathToken ( item )); } return tokens ; } } public class ExpressionValidator { public bool IsExpressionValid ( string expression ) { Regex regex = new Regex ( @ " ^ -{0 ,1}\ d +((\ s +)[+|\ -|/|*](\ s +) -{0 ,1}\ d +)+ $ " ); return regex . IsMatch ( expression , 0); } } public class MathParser { }

Hemos tenido que renombrar algunas variables en los tests para que pasen despus de esta refactorizacin pero ha sido rpido. Los he dejado dentro del e o a conjunto de tests ParserTests aunque ahora se ha quedado vac la clase a Parser. La libreta dice que ante una expresin invlida el analizador producir una o a a excepcin. Escribamos un ejemplo que lo provoque: o

1 2 3 4 5

[ Test ] public void G e t T o k e n s W r o n g E x p r e s s i o n () { try {

164

Cap tulo 9
List < MathToken > tokens = _lexer . GetTokens ( " 2 - 1++ 3 " ); Assert . Fail ( " Exception did not arise ! " ); } catch ( I n v a l i d O p e r a t i o n E x c e p t i o n ) { } }

6 7 8 9 10 11

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

Nos hemos decantado por InvalidOperationException. Ahora podr amos escribir un hack veloz y triangular pero es un poco absurdo teniendo ya un validador de expresiones. Inyectemos el validador:
public class MathLexer { ExpressionValidator _validator ; public MathLexer ( ExpressionValidator validator ) { _validator = validator ; } public List < MathToken > GetTokens ( string expression ) { if (! _validator . IsExpressionValid ( expression )) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( expression ); List < MathToken > tokens = new List < MathToken >(); String [] items = expression . Split ( ); foreach ( String item in items ) { tokens . Add ( new MathToken ( item )); } return tokens ; } }

1 2 3 4 5 6 7 8

Se crear bien la lista de tokens cuando haya varios espacios seguidos? Mejor a lo apuntalamos con un test:
[ Test ] public void GetTokensWithSpaces () { List < MathToken > tokens = _lexer . GetTokens ( " 5 Assert . AreEqual ( " 5 " , tokens [0]. Token ); Assert . AreEqual ( " -" , tokens [1]. Token ); Assert . AreEqual ( " 88 " , tokens [2]. Token ); }

88 " );

Pues resulta que no funciona. Luz roja. Deber amos poder partir por cualquier carcter en blanco: a

1 2 3 4 5

9.8: MathLexer

public List < MathToken > GetTokens ( string expression ) { if (! _validator . IsExpressionValid ( expression )) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( expression );

165

Cap tulo 9
List < MathToken > tokens = new List < MathToken >(); String [] items = expression . Split (( new char [] { , \ t }) , StringSplitOptions . RemoveEmptyEntries ); foreach ( String item in items ) { tokens . Add ( new MathToken ( item )); } return tokens ; }

6 7 8 9 10 11 12 13 14

OK luz verde. Refactorizo un poco:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

9.9: MathLexer

public List < MathToken > GetTokens ( string expression ) { if (! _validator . isExpressionValid ( expression )) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( expression ); string [] items = splitExpression ( expression ); return c r e a t e T o k e n s F r o m S t r i n g s ( items ); } private string [] splitExpression ( string expression ) { return expression . Split (( new char [] { , \ t }) , StringSplitOptions . RemoveEmptyEntries ); } private List < MathToken > c r e a t e T o k e n s F r o m S t r i n g s ( strin g [] items ) { List < MathToken > tokens = new List < MathToken >(); foreach ( String item in items ) { tokens . Add ( new MathToken ( item )); } return tokens ; }

Limpiemos la lista para ver qu toca ahora: e


# Aceptacin - "2 + 2", devuelve 4 o # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "2 + -2", devuelve 0 o

El primer test de aceptacin de la lista nos exige comenzar a unir las o distintas piezas que hemos ido creando. Por una lado sabemos que somos capaces de sumar y por otro ya conseguimos la lista de tokens de la expresin. o Queda conectar ambas cosas. En este caso concreto el test de aceptacin lo o podemos expresar con NUnit. Aunque voy a intentar que cumpla con algunas normas de los tests unitarios (inocuo y rpido) no es atmico as que no me a o atrever a llamarle unitario sino simplemente funcional. a 166

Cap tulo 9

1 2 3 4 5 6

[ Test ] public void P r o c e s s S i m p l e E x p r e s s i o n () { MathParser parser = new MathParser (); Assert . AreEqual (4 , parser . ProcessExpression ( " 2 + 2 " )); }

Antes de implementar el SUT m nimo, me parece buena idea escribir un par de tests unitarios que fuercen la colaboracin entre los objetos que teo nemos. As no se me olvida utilizarlos cuando me adentre en detalles de implementacin: o

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

[ Test ] public void P a r s e r W o r k s W i t h C a l c P r o x y () { CalculatorProxy calcProxyMock = MockRepository . GenerateMock < CalculatorProxy >(); calcProxyMock . Expect ( x = > x . Calculator ). Return ( _calculator ); calcProxyMock . Expect ( x = > x . BinaryOperation ( _calculator . Add , 2 , 2)). Return (4); MathParser parser = new MathParser ( calcProxyMock ); parser . ProcessExpression ( " 2 + 2 " ); calcProxyMock . V e r i f y A l l E x p e c t a t i o n s (); }

Para escribir el test tuve que extraer la interfaz CalculatorProxy a partir de la clase CalcProxy. La intencin es forzar la colaboracin. No me gusta o o tener que ser tan expl cito al denir la llamada a la propiedad Calculator del proxy en la l nea 6. Siento que me gustar que Calculator estuviese mejor a encapsulado dentro del proxy. Es algo que tengo en mente arreglar tan pronto como un requisito me lo pida. Y seguro que aparece pronto. Conseguimos el verde rpido: a

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

public class MathParser { CalculatorProxy _calcProxy ; public MathParser ( CalculatorProxy calcProxy ) { _calcProxy = calcProxy ; } public int ProcessExpression ( string expression ) { return _calcProxy . BinayOperation ( _calcProxy . Calculator . Add , 2 , 2); } }

167

Cap tulo 9 Forcemos tambin la colaboracin con MathLexer: e o


[ Test ] public void P a r s e r W o r k s W i t h L e x e r () { List < MathToken > tokens = new List < MathToken >(); tokens . Add ( new MathToken ( " 2 " )); tokens . Add ( new MathToken ( " + " )); tokens . Add ( new MathToken ( " 2 " )); Lexer lexerMock = MockRepository . GenerateStrictMock < Lexer >(); lexerMock . Expect ( x = > x . GetTokens ( " 2 + 2 " )). Return ( tokens ); MathParser parser = new MathParser ( lexerMock , new CalcProxy ( new Validator ( -100 , 100) , new Calculator ())); parser . ProcessExpression ( " 2 + 2 " ); lexerMock . V e r i f y A l l E x p e c t a t i o n s (); }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

Extraje la interfaz Lexer para generar el mock. El SUT va tomando forma:


public class MathParser { Lexer _lexer ; CalculatorProxy _calcProxy ; public MathParser ( Lexer lexer , CalculatorProxy calcProx y ) { _lexer = lexer ; _calcProxy = calcProxy ; } public int ProcessExpression ( string expression ) { List < MathToken > tokens = _lexer . GetTokens ( expression ); return _calcProxy . BinaryOperation ( _calcProxy . Calculator . Add , tokens [0]. IntValue , tokens [2]. IntValue ); } }

1 2 3 4 5 6

Modiqu el test anterior ya que el constructor ha cambiado. Estos dos tests e nos posicionan en un cdigo m o nimo sin llegar a ser el clsico return 4. a Buen punto de partida para triangular hacia algo ms util. a
[ Test ] public void P r o c e s s E x p r e s s i o n 2 O p e r a t o r s () { Assert . AreEqual (6 , _parser . ProcessExpression ( " 3 + 1 + 2 " )); }

Voy a implementar un cdigo m o nimo que resuelva la operacin procesando o la entrada de izquierda a derecha: 168

Cap tulo 9 9.10: MathParser

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

public int ProcessExpression ( string expression ) { List < MathToken > tokens = _lexer . GetTokens ( expression ); MathToken total = tokens [0]; for ( int i = 0; i < tokens . Count ; i ++) { if ( tokens [ i ]. isOperator ()) { MathToken totalForNow = total ; MathToken nextNumber = tokens [ i + 1]; int partialResult = _calcProxy . BinaryOperation ( _calcProxy . Calculator . Add , totalForNow . IntValue , nextNumber . IntValue ); total = new MathToken ( partialResult . ToString ()); i ++; } } return total . IntValue ; }

Lo ms sencillo que se me ha ocurrido es coger los operadores y dar por sena tado que los operandos (nmeros) estn a su izquierda y derecha. Al escribir u a el cdigo me he dado cuenta que necesitaba la funcin isOperator en la o o clase MathToken y he hecho uso de ella sin que est implementada. As pues e dejo a un lado el SUT y voy a por un test que me ayude a implementar dicha funcin, sea, cambio de SUT un momento. o o

1 2 3 4 5 6 7 8 9 10

[ TestFixture ] public class MathTokenTests { [ Test ] public void isOperator () { MathToken numberToken = new MathToken ( " 22 " ); Assert . IsFalse ( numberToken . isOperator ()); } }


1 2 3 4 5 6 7 8 9 10

9.11: MathToken

public bool isOperator () { string operators = " + -*/ " ; foreach ( char op in operators ) { if ( _token == op . ToString ()) return true ; } return false ; }

Ahora el caso positivo: 169

Cap tulo 9

1 2 3 4 5 6

[ Test ] public void isOperatorTrue () { MathToken numberToken = new MathToken ( " * " ); Assert . IsTrue ( numberToken . isOperator ()); }

Funciona. Podemos regresar al SUT anterior y ejecutar el test para comprobar que ... tambin funciona!. La cosa marcha. Revisamos la lista a ver cmo e o vamos:
# Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "2 + -2", devuelve 0 o

No hemos tenido en cuenta la precedencia de operadores! El test anterior no la exig y me centr tanto en el cdigo m a e o nimo que cumpl con la especia cacin, que olvid el criterio de aceptacin de la precedencia de operadores. o e o No pasa nada, seguro que es ms fcil partir de aqu hacia la solucin del a a o problema que haber partido de cero. Cuando el problema se hace complejo como en el caso que nos ocupa, es especialmente importante no saltarse la regla de disear en pequeos pasos. Quizs si hubisemos contemplado el n n a e caso complejo desde el principio hubisemos olvidado casos que a la larga se e hubiesen traducido en bugs. Antes de seguir voy a refactorizar un poco los tests, moviendo los que son de validacin de expresiones a un conjunto fuera de ParserTests ya que se o est convirtiendo en una clase con demasiados tests1 . a Una vez movidos los tests tenemos que disear una estrategia para la n precedencia de los operadores. De paso podemos aadir a la lista los casos n en que se utilizan parntesis en las expresiones, para irles teniendo en mente e a la hora de tomar decisiones de diseo: n
# Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "(2 + 2) * (3 + 1)", devuelve 16 o # Aceptacin - "2 + -2", devuelve 0 o

1 2 3 4 5

Una vez ms. Un ejemplo sencillo primero para afrontar el SUT: a

[ Test ] public void P r o c e s s E x p r e s s i o n W i t h P r e c e d e n c e () { Assert . AreEqual (9 , _parser . ProcessExpression ( " 3 + 3 * 2 " )); }

Ver los cambios en el cdigo fuente que acompaa al libro o n

170

Cap tulo 9 Si el operador tuviera asociado un valor para su precedencia, podr buscar a aquellos de mayor precedencia, operar y a continuacin hacer lo mismo con o los de menor. De paso para encapsular mejor la calculadora podr mover a las llamadas al proxy a una clase operador. Como el test que nos ocupa me parece demasiado grande para implementar el SUT de una sola vez, voy a escribir otro test que me sirva para obtener la precedencia ms alta de la a expresin. Un poco ms adelante retomar el test que acabo de escribir. o a e

1 2 3 4 5 6 7

[ Test ] public void GetMaxPrecedence () { List < MathToken > tokens = _lexer . GetTokens ( " 3 + 3 * 2 " ); MathOperator op = _parser . GetMaxPrecedence ( tokens ); Assert . AreEqual ( op . Token , " * " ); }

El SUT:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

9.12: MathParser

public MathOperator GetMaxPrecedence ( List < MathToken > t okens ) { int precedence = 0; MathOperator m a x P r e c e d e n c e O p e r a t o r = null ; foreach ( MathToken token in tokens ) { if ( token . isOperator ()) { MathOperator op = OperatorFactory . Create ( token ); if ( op . Precedence >= precedence ) { precedence = op . Precedence ; m a x P r e c e d e n c e O p e r a t o r = op ; } } } return m a x P r e c e d e n c e O p e r a t o r ; }

No compila porque las clases MathOperator y OperatorFactory no existen y el mtodo Create tampoco. Voy a intentar que compile lo ms rpidamente e a a posible:

1 2 3 4 5 6 7 8 9 10 11

public class MathOperator { int _precedence = 0; string _token = String . Empty ; public string Token { get { return _token ; } set { _token = value ; } }

171

Cap tulo 9
public int Precedence { get { return _precedence ; } } public class OperatorFactory { public static MathOperator Create ( MathToken token ) { MathOperator op = new MathOperator (); op . Token = token . Token ; return op ; } }

12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

Bien. El test para el mtodo de obtener la mxima precedencia funciona e a parcialmente pero no hemos triangulado. Para ello tenemos que probar que la factor de operadores les pone precedencia: a

1 2 3 4 5 6 7 8 9 10

[ TestFixture ] public class O p e r a t o r F a c t o r y T e s t s { [ Test ] public void C r e a t e M u l t i p l y O p e r a t o r () { MathOperator op = OperatorFactory . Create ( new MathToken ( " * " )); Assert . AreEqual ( op . Precedence , 2); } }

SUT m nimo:

1 2 3 4 5 6 7 8 9 10

9.13: OperatorFactory

public static MathOperator Create ( MathToken token ) { MathOperator op ; if ( token . Token == " * " ) op = new MathOperator (2); else op = new MathOperator (0); op . Token = token . Token ; return op ; }

He tenido que aadir un constructor para MathOperator que reciba el valor n de precedencia. El test pasa. Si escribo otro test para la divisin creo que por o ahora tendr las precedencias resueltas: e

1 2 3 4 5 6

[ Test ] public void C r e a t e D i v i s i o n O p e r a t o r () { MathOperator op = OperatorFactory . Create ( new MathToken ( " / " )); Assert . AreEqual ( op . Precedence , 2); }

172

Cap tulo 9 9.14: OperatorFactory

1 2 3 4 5 6 7 8 9 10

public static MathOperator Create ( MathToken token ) { MathOperator op ; if (( token . Token == " * " ) || ( token . Token == " / " )) op = new MathOperator (2); else op = new MathOperator (0); op . Token = token . Token ; return op ; }

1 2 3 4 5

Perfecto los tests pasan. Tanto MathToken como MathOperator comparten la propiedad Token. Empiezo a pensar que deber compartir una interfaz. an Podr refactorizar pero habr que refactorizar ms cosas pronto. Primero a a a voy a terminar de implementar el SUT para el test que hab amos dejado en el aire:
[ Test ] public void P r o c e s s E x p r e s s i o n W i t h P r e c e d e n c e () { Assert . AreEqual (9 , _parser . ProcessExpression ( " 3 + 3 * 2 " )); }

El SUT:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

9.15: MathParser

public int ProcessExpression ( string expression ) { List < MathToken > tokens = _lexer . GetTokens ( expression ); while ( tokens . Count > 1) { MathOperator op = GetMaxPrecedence ( tokens ); int firstNumber = tokens [ op . Index -1]. IntValue ; int secondNumber = tokens [ op . Index +1]. IntValue ; int result = op . Resolve ( firstNumber , secondNumber ); tokens [ op . Index - 1] = new MathToken ( result . ToString ()); tokens . RemoveAt ( op . Index ); tokens . RemoveAt ( op . Index ); } return tokens [0]. IntValue ; }

He simplicado el algoritmo. Me limito a buscar el operador de mayor prioridad, operar los nmeros a su izquierda y derecha y sustituir los tres elementos u por el resultado. He necesitado una propiedad Index en el operator que no exist as que para poder compilar la aado: a n

1 2 3 4

9.16: MathParser

public MathOperator GetMaxPrecedence ( List < MathToken > t okens ) { int precedence = 0; MathOperator m a x P r e c e d e n c e O p e r a t o r = null ;

173

Cap tulo 9

5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

int index = -1; foreach ( MathToken token in tokens ) { index ++; if ( token . isOperator ()) { MathOperator op = OperatorFactory . Create ( token ); if ( op . Precedence >= precedence ) { precedence = op . Precedence ; m a x P r e c e d e n c e O p e r a t o r = op ; m a x P r e c e d e n c e O p e r a t o r . Index = index ; } } } return m a x P r e c e d e n c e O p e r a t o r ; }

El mtodo Resolve del operator tampoco existe. En mi cabeza es el mtodo e e que encapsula el uso del proxy y a su vez la calculadora. Voy a implementar un stub rpido para compilar. a

1 2 3 4 5 6 7 8

9.17: MathOperator

public int Resolve ( int a , int b ) { if ( Token == " * " ) return a * b ; if ( Token == " + " ) return a + b ; return 0; }

Muy bien. Ahora pasan todos los tests menos aquel en el que forzbamos al a analizador a utilizar el proxy, porque en el mtodo Resolve hemos hecho un e apao rpido y feo. El primer cambio que voy a hacer es inyectar el proxy n a como parmetro en el mtodo para utilizarlo: a e

1 2 3 4 5 6 7 8 9

9.18: MathOperator

public int Resolve ( int a , int b , CalculatorProxy calcProxy ) { if ( Token == " * " ) return a * b ; if ( Token == " + " ) return calcProxy . BinaryOperation ( calcProxy . Calculator . Add , a , b ); return 0; }

Perfecto. Todos los tests pasan. No he utilizado el proxy para la multiplicacin porque no la tenemos implementada. Voy a aadir un par de tests o n sencillos de multiplicacin y divisin para completar la funcionalidad de la o o clase Calculator. Los tests estaban dentro del conjunto de tests del proxy: 174

Cap tulo 9

1 2 3 4 5 6 7

[ Test ] public void Multiply () { Assert . AreEqual ( _calcProxy . BinaryOperation ( _calculator . Multiply , 2 , 5) , 10); }


1 2 3 4

9.19: Calculator

public int Multiply ( int arg1 , int arg2 ) { return arg1 * arg2 ; }


1 2 3 4 5 6 7

[ Test ] public void Division () { Assert . AreEqual ( _calcProxy . BinaryOperation ( _calculator . Divide , 10 , 2) , 5); }


1 2 3 4

9.20: Calculator

public int Divide ( int arg1 , int arg2 ) { return arg1 / arg2 ; }

Bien, voy a completar el mtodo que resuelve en el operador: e

1 2 3 4 5 6 7 8 9 10

9.21: MathOperator

public int Resolve ( int a , int b , CalculatorProxy calcProxy ) { if ( Token == " * " ) return calcProxy . BinaryOperation ( calcProxy . Calculator . Multiply , a , b ); if ( Token == " + " ) return calcProxy . BinaryOperation ( calcProxy . Calculator . Add , a , b ); return 0; }

Todos los tests pasan. Aprovecho para refactorizar:

1 2 3 4 5 6

9.22: MathParser

public int ProcessExpression ( string expression ) { List < MathToken > tokens = _lexer . GetTokens ( expression ); while ( tokens . Count > 1) { MathOperator op = GetMaxPrecedence ( tokens );

175

Cap tulo 9
int firstNumber = tokens [ op . Index -1]. IntValue ; int secondNumber = tokens [ op . Index +1]. IntValue ; int result = op . Resolve ( firstNumber , secondNumber , _calcProxy ); r e p l a c e T o k e n s W i t h R e s u l t ( tokens , op . Index , result ); } return tokens [0]. IntValue ; } private void r e p l a c e T o k e n s W i t h R e s u l t ( List < MathToken > tokens , int indexOfOperator , int result ) { tokens [ indexOfOperator - 1] = new MathToken ( result . ToString ()); tokens . RemoveAt ( indexOfOperator ); tokens . RemoveAt ( indexOfOperator ); }

7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

Cmo est la libreta? o a


# Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "(2 + 2) * (3 + 1)", devuelve 16 o # Aceptacin - "2 + -2", devuelve 0 o

1 2 3 4 5

Estamos preparados para escribir un test de aceptacin para la primera o l nea:


[ Test ] public void P r o c e s s A c c e p t a n c e E x p r e s s i o n () { Assert . AreEqual (9 , _parser . ProcessExpression ( " 5 + 4 * 2 / 2 " )); }

Se esperaba 9 pero se devolvi 5. Ah claro! Es que el mtodo de resolver no o e implementa la divisin ni la resta. Qu despiste. Voy a aadir la divisin para o e n o que el test pase y luego escribo otro test con una resta para estar obligado a implementarla.

1 2 3 4 5 6 7 8 9 10 11 12 13

9.23: MathOperator

public int Resolve ( int a , int b , CalculatorProxy calcProxy ) { if ( Token == " * " ) return calcProxy . BinaryOperation ( calcProxy . Calculator . Multiply , a , b ); if ( Token == " + " ) return calcProxy . BinaryOperation ( calcProxy . Calculator . Add , a , b ); if ( Token == " / " ) return calcProxy . BinaryOperation ( calcProxy . Calculator . Divide , a , b ); return 0; }

176

Cap tulo 9

1 2 3 4 5 6

[ Test ] public void P r o c e s s A c c e p t a n c e E x p r e s s i o n W i t h A l l O p e r a t o r s () { Assert . AreEqual (8 , _parser . ProcessExpression ( " 5 + 4 - 1 * 2 / 2 " )); }


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

9.24: MathOperator

public int Resolve ( int a , int b , CalculatorProxy calcProxy ) { if ( Token == " * " ) return calcProxy . BinaryOperation ( calcProxy . Calculator . Multiply , a , b ); else if ( Token == " + " ) return calcProxy . BinaryOperation ( calcProxy . Calculator . Add , a , b ); else if ( Token == " / " ) return calcProxy . BinaryOperation ( calcProxy . Calculator . Divide , a , b ); else if ( Token == " -" ) return calcProxy . BinaryOperation ( calcProxy . Calculator . Substract , a , b ); return 0; }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

Luz verde! Algo por refactorizar? Ciertamente hay dos condicionales repetidas. En la factor de operadores se pregunta por el token para asignar a precedencia al operador y crearlo. En la resolucin tambin se pregunta por o e el token para invocar al proxy. Si utilizamos polimorsmo podemos eliminar la condicional del mtodo Resolve haciendo que cada operador espec e co implemente su propia resolucin. Esta refactorizacin de hecho se llama as o o : reemplazar condicional con polimorsmo:
public abstract class MathOperator { protected int _precedence = 0; protected string _token = String . Empty ; int _index = -1; public MathOperator ( int precedence ) { _precedence = precedence ; } public int Index { get { return _index ; } set { _index = value ; } } public string Token { get { return _token ; } }

177

Cap tulo 9

22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80

public int Precedence { get { return _precedence ; }

public abstract int Resolve ( int a , int b , CalculatorProxy calcProxy ); } } public class MultiplyOperator : MathOperator { public MultiplyOperator () : base (2) { _token = " * " ; } public override int Resolve ( int a , int b , CalculatorProxy calcProxy ) { return calcProxy . BinaryOperation ( calcProxy . Calculator . Multiply , a , b ); } } public class DivideOperator : MathOperator { public DivideOperator () : base (2) { _token = " / " ; } public override int Resolve ( int a , int b , CalculatorProxy calcProxy ) { return calcProxy . BinaryOperation ( calcProxy . Calculator . Divide , a , b ); } } public class AddOperator : MathOperator { public AddOperator () : base (1) { _token = " + " ; } public override int Resolve ( int a , int b , CalculatorProxy calcProxy ) { return calcProxy . BinaryOperation ( calcProxy . Calculator . Add , a , b ); } }

178

Cap tulo 9
public class SubstractOperator : MathOperator { public SubstractOperator () : base (1) { _token = " -" ; } public override int Resolve ( int a , int b , CalculatorProxy calcProxy ) { return calcProxy . BinaryOperation ( calcProxy . Calculator . Substract , a , b ); } } public class OperatorFactory { public static MathOperator Create ( MathToken token ) { if ( token . Token == " * " ) return new MultiplyOperator (); else if ( token . Token == " / " ) return new DivideOperator (); else if ( token . Token == " + " ) return new AddOperator (); else if ( token . Token == " -" ) return new SubstractOperator (); throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( " The given token is not a valid operator " ); } }

81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113

El cdigo queda ms claro y la condicional en un unico sitio. Parecen muchas o a l neas pero ha sido una refactorizacin de tres minutos. o Aunque no hay ningn test que pruebe que el mtodo Create lanza una u e excepcin en caso que el token recibido no sea vlido, el compilador me obliga o a a hacerlo, ya que de lo contrario tendr que devolver un objeto sin sentido. a No veo la necesidad de escribir un test para ese caso porque ya tenemos un validador de expresiones y mtodos que comprueban si un token es nmero e u u operador y ambas cosas estn debidamente probadas. a Recordemos que la nalidad de TDD no es alcanzar una cobertura de tests del 100 % sino disear acorde a los requisitos mediante los ejemplos. n Repasemos la libreta:
# Aceptacin - "(2 + 2) * (3 + 1)", devuelve 16 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "2 + -2", devuelve 0 o

Pensemos en las operaciones con parntesis. En cmo resolver el problee o 179

Cap tulo 9 ma. Aumentar la complejidad de la expresin regular que valida las expreo siones matemticas no me parece sostenible. Me da la sensacin de que ir a o por ese camino har el cdigo ms dif de mantener, demasiado engorroso. a o a cil Por otro lado no sabr utilizar expresiones regulares para comprobar que un a parntesis abierto casa con uno cerrado y cosas as Si nos jamos bien, el e . contenido de un parntesis ha de ser una expresin de las que ya sabemos e o validar y resolver. Una expresin a su vez puede verse como una lista de o tokens que en ultima instancia contiene un solo elemento que es un nmero. u Vamos a partir el test de aceptacin en unos cuantos tests de granularidad o ms na para ir abordando poco a poco la implementacin. a o
# Aceptacin - "(2 + 2) * (3 + 1)", devuelve 16 o "(2 + 2)", se traduce en la expresin "2 + 2" o "((2) + 2)", se traduce en la expresin "2 + 2" o "(2 + 2", produce una excepcin o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "2 + -2", devuelve 0 o

El primero de los tests:

1 2 3 4 5 6 7 8

[ Test ] public void G e t E x p r e s s i o n s W i t h 1 P a r e n t h e s i s () { List < string > expressions = _lexer . GetExpressions ( " (2 + 2) " ); Assert . AreEqual (1 , expressions . Count ); Assert . AreEqual ( " 2 + 2 " , expressions [0]); }

De momento el test est dentro de ParserTests pero ya estoy pensando a moverlo a un nuevo conjunto LexerTests. No voy a devolver un resultado jo directamente sino a dar los primeros pasos en el algoritmo que encuentra expresiones dentro de los parntesis. e

1 2 3 4 5 6 7 8 9 10 11 12 13 14

9.25: MathLexer

public List < string > GetExpressions ( string expression ) { List < string > expressions = new List < string >(); Stack < char > parenthesis = new Stack < char >(); foreach ( char ch in expression ) { if ( ch == ( ) { parenthesis . Push ( ch ); expressions . Add ( String . Empty ); } else if ( ch == ) ) {

180

Cap tulo 9
parenthesis . Pop (); } else { expressions [ expressions . Count -1] += ch . ToString (); } } return expressions ; }

15 16 17 18 19 20 21 22 23 24

Cada vez que se encuentra un parntesis abierto se crea una nueva expresin. e o El algoritmo es simple. He utilizado una pila para llevar control de parntesis e abiertos y cerrados en vista de los prximos tests que hay en la libreta, aunque o no estoy haciendo uso de ella al nal del algoritmo. Eso lo dejar para un e test que lo requiera.

1 2 3 4 5 6 7 8

[ Test ] public void G e t E x p r e s s i o n s W i t h N e s t e d P a r e n t h e s i s () { List < string > expressions = _lexer . GetExpressions ( " ((2) + 2) " ); Assert . AreEqual (1 , expressions . Count ); Assert . AreEqual ( " 2 + 2 " , expressions [0]); }

El test falla porque la funcin est devolviendo dos expresiones, la primera o a de ellas vac Hay que limpiar expresiones vac a. as:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

9.26: MathLexer

public List < string > GetExpressions ( string expression ) { List < string > expressions = new List < string >(); Stack < char > parenthesis = new Stack < char >(); foreach ( char ch in expression ) { if ( ch == ( ) { parenthesis . Push ( ch ); expressions . Add ( String . Empty ); } else if ( ch == ) ) { parenthesis . Pop (); } else { expressions [ expressions . Count -1] += ch . ToString (); } } c l e a n E m p t y E x p r e s s i o n s ( expressions ); return expressions ; }

181

Cap tulo 9

26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

private void c l e a n E m p t y E x p r e s s i o n s ( List < string > expres sions ) { bool endOfList = false ; while (! endOfList ) { endOfList = true ; for ( int i = 0; i < expressions . Count ; i ++) if ( expressions [ i ]. Length == 0) { expressions . RemoveAt ( i ); endOfList = false ; break ; } } }

Ya tenemos luz verde. Se me acaba de venir a la mente una pregunta. Y si al leer las expresiones se forma una que no empieza por un nmero?. Ejemplo: u

1 2 3 4 5 6 7 8 9 10 11 12 13

[ Test ] public void G e t N e s t e d E x p r e s s i o n s () { List < string > expressions = _lexer . GetExpressions ( " ((2 + 1) + 2) " ); Assert . AreEqual (3 , expressions . Count ); foreach ( string exp in expressions ) if (( exp != " 2 + 1 " ) && ( exp != " + " ) && ( exp != " 2 " )) Assert . Fail ( " Wrong expression split " ); }

El test expresa mi decisin de evitar devolver expresiones del tipo + 1 preo riendo los tokens sueltos, a las expresiones que no tienen sentido matemtico a por s mismas. He tenido cuidado de no especicar en las armaciones las posiciones de las expresiones dentro del vector de expresiones para no escribir un test frgil. Lo que me interesa es el contenido de las cadenas y no la a posicin. o

1 2 3 4 5 6 7 8 9 10 11 12 13

9.27: MathLexer

public List < string > GetExpressions ( string expression ) { List < string > expressions = new List < string >(); Stack < int > parenthesis = new Stack < int >(); int index = 0; foreach ( char ch in expression ) { if ( ch == ( ) { parenthesis . Push ( index ); index ++; expressions . Add ( String . Empty );

182

Cap tulo 9
} else if ( ch == ) ) { index = parenthesis . Pop (); } else { expressions [ index -1] += ch . ToString (); } } c l e a n E m p t y E x p r e s s i o n s ( expressions ); s p l i t E x p r e s s i o n s S t a r t i n g W i t h O p e r a t o r ( expressions ); return expressions ; } private void s p l i t E x p r e s s i o n s S t a r t i n g W i t h O p e r a t o r ( List < string > expressions ) { Regex regex = new Regex ( @ " ^(\ s *)[+|\ -|/|*](\ s +) " ); bool endOfList = false ; while (! endOfList ) { endOfList = true ; for ( int i = 0; i < expressions . Count ; i ++) if ( regex . IsMatch ( expressions [ i ])) { string exp = expressions [ i ]; exp = exp . Trim (); string [] nexExps = exp . Split ( new char [] { , \ t } , 2 , StringSplitOptions . RemoveEmptyEntries ); expressions [ i ] = nexExps [0]; expressions . Insert ( i + 1 , nexExps [1]); endOfList = false ; } } }

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52

La nueva funcin busca expresiones que empiecen por un operador y entonces o las parte en dos; por un lado el operador y por otro el resto de la expresin. o Por ahora no hace nada ms. a El cdigo est empezando a ser una maraa. Al escribir esta funcin me o a n o doy cuenta de que probablemente quiera escribir unos cuantos tests unitarios para cubrir otros usos de la misma pero el mtodo es privado y eso limita e la granularidad de los tests ya que no tengo acceso directo. Tener acceso desde el test a la funcin que se quiere probar sin pasar por otras funciones o o mtodos de entrada acelera la deteccin y correccin de defectos. e o o Recordemos que a veces los mtodos privados sugieren ser movidos a e clases colaboradoras. Vamos a hacer un poco de limpieza:

1 2

public class ExpressionFixer {

183

Cap tulo 9
public void C l e a n E m p t y E x p r e s s i o n s ( List < string > express ions ) { bool endOfList = false ; while (! endOfList ) { endOfList = true ; for ( int i = 0; i < expressions . Count ; i ++) if ( expressions [ i ]. Length == 0) { expressions . RemoveAt ( i ); endOfList = false ; break ; } } } public void S p l i t E x p r e s s i o n s S t a r t i n g W i t h O p e r a t o r ( List < string > expressions ) { Regex regex = new Regex ( @ " ^(\ s *)[+|\ -|/|*](\ s +) " ); bool endOfList = false ; while (! endOfList ) { endOfList = true ; for ( int i = 0; i < expressions . Count ; i ++) if ( regex . IsMatch ( expressions [ i ])) { string exp = expressions [ i ]; exp = exp . Trim (); string [] nexExps = exp . Split ( new char [] { , \ t } , 2 , StringSplitOptions . RemoveEmptyEntries ); expressions [ i ] = nexExps [0]; expressions . Insert ( i + 1 , nexExps [1]); endOfList = false ; } } } } public class MathLexer : Lexer { ExpressionValidator _validator ; ExpressionFixer _fixer ; public MathLexer ( ExpressionValidator validator , ExpressionFixer fixer ) { _validator = validator ; _fixer = fixer ; } public List < string > GetExpressions ( string expression ) { List < string > expressions = new List < string >(); Stack < int > parenthesis = new Stack < int >(); int index = 0;

3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61

184

Cap tulo 9
foreach ( char ch in expression ) { if ( ch == ( ) { parenthesis . Push ( index ); index ++; expressions . Add ( String . Empty ); } else if ( ch == ) ) { index = parenthesis . Pop (); } else { expressions [ index -1] += ch . ToString (); } } _fixer . C l e a n E m p t y E x p r e s s i o n s ( expressions ); _fixer . S p l i t E x p r e s s i o n s S t a r t i n g W i t h O p e r a t o r ( express ions ); return expressions ; } ...

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84

He creado la nueva clase ExpressionFixer (reparador de expresiones) que se inyecta a MathLexer. Lgicamente he tenido que modicar las llamadas o al constructor de lexer en los tests. Ahora me sigue pareciendo que hay duplicidad en los bucles de los dos mtodos del reparador de expresiones. e Vamos a anar un poco ms: a

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

public class ExpressionFixer { public void FixExpressions ( List < string > expressions ) { bool listHasChanged = true ; while ( listHasChanged ) { listHasChanged = false ; for ( int i = 0; i < expressions . Count ; i ++) if ( D o e s E x p r e s s i o n S t a r t s W i t h O p e r a t o r ( expressions , i ) || IsEmptyExpression ( expressions , i )) { listHasChanged = true ; break ; } } } public bool IsEmptyExpression ( List < string > expressions , int index ) { if ( expressions [ index ]. Length == 0) { expressions . RemoveAt ( index ); return true ; } return false ;

185

Cap tulo 9
} public void D o e s E x p r e s s i o n S t a r t s W i t h O p e r a t o r ( List < string > expressions , int index ) { Regex regex = new Regex ( @ " ^(\ s *)[+|\ -|/|*](\ s +) " ); if ( regex . IsMatch ( expressions [ index ])) { string exp = expressions [ index ]; exp = exp . Trim (); string [] nexExps = exp . Split ( new char [] { , \ t } , 2 , StringSplitOptions . RemoveEmptyEntries ); expressions [ i ] = nexExps [0]; expressions . Insert ( i + 1 , nexExps [1]); return true ; } return false ; } }

28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

1 2 3 4 5 6 7 8 9 10 11

En MathLexer cambi las dos llamadas de las l e neas 79 y 80 del penltimo u listado por una sola a FixExpressions. Todav me parece que el cdigo a o del ultimo mtodo tiene varias responsabilidades pero por ahora voy a parar e de refactorizar y a anotarlo en la libreta para retomarlo en breve. Ejecuto toda la bater de tests despus de tanto cambio y veo que el test a e est funcionando pero que se ha roto el que hab a amos escrito anteriormente. Voy a reescribirlo. Como no es un test de aceptacin puedo cambiarlo sin o problema.
[ Test ] public void G e t E x p r e s s i o n s W i t h N e s t e d P a r e n t h e s i s () { List < string > expressions = _lexer . GetExpressions ( " ((2) + 2) " ); foreach ( string exp in expressions ) if (( exp != " 2 " ) && ( exp != " + " )) Assert . Fail ( " Wrong expression split " ); }

Ahora ya funciona. Hay cdigo duplicado en los tests. Lo arreglamos: o

1 2 3 4 5 6 7 8 9

9.28: LexerTests

[ Test ] public void G e t E x p r e s s i o n s W i t h N e s t e d P a r e n t h e s i s () { List < string > expressions = _lexer . GetExpressions ( " ((2) + 2) " ); f a i l I f O t h e r S u b E x p r e s s i o n T h a n ( expressions , " 2 " , " + " ); } [ Test ]

186

Cap tulo 9
public void G e t N e s t e d E x p r e s s i o n s () { List < string > expressions = _lexer . GetExpressions ( " ((2 + 1) + 2) " ); Assert . AreEqual (3 , expressions . Count ); failIfOtherSubExpressionThan ( expressions , " 2 + 1 " , " + " , " 2 " ); } private void f a i l I f O t h e r S u b E x p r e s s i o n T h a n ( List < string > expressions , params string [] e x p e c t e d S u b E x p r e s s i o n s ) { bool isSubExpression = false ; foreach ( string subExpression in e x p e c t e d S u b E x p r e s s i o n s ) { isSubExpression = false ; foreach ( string exp in expressions ) if ( exp == subExpression ) { isSubExpression = true ; break ; } if (! isSubExpression ) Assert . Fail ( " Wrong expression split : " + subExpression ); } }

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

Cmo se comporta nuestra clase cuando el parntesis aparece en la parte o e nal de la expresin? Me ha surgido la duda mientras escrib el ultimo SUT. o a

1 2 3 4 5 6 7 8

[ Test ] public void G e t E x p r e s s i o n W i t h P a r e n t h e s i s A t T h e E n d () { List < string > expressions = _lexer . GetExpressions ( " 2 + (3 * 1) " ); failIfOtherSubExpressionThan ( expressions , " 3 * 1 " , " + " , " 2 " ); }

De momento falla con una excepcin. Corrijo el SUT: o

1 2 3 4 5 6 7 8 9 10 11 12 13 14

9.29: MathLexer

public List < string > GetExpressions ( string expression ) { List < string > expressions = new List < string >(); Stack < int > parenthesis = new Stack < int >(); int index = 1; expressions . Add ( String . Empty ); foreach ( char ch in expression ) { if ( ch == ( ) { parenthesis . Push ( index ); index ++; expressions . Add ( String . Empty );

187

Cap tulo 9
} else if ( ch == ) ) { index = parenthesis . Pop (); } else { expressions [ index -1] += ch . ToString (); } } _fixer . FixExpressions ( expressions ); return expressions ; }

15 16 17 18 19 20 21 22 23 24 25 26 27 28

El SUT no contemplaba que la expresin comenzase sin parntesis (la l o e nea 7 lo corrige). Ahora la ejecucin no se interrumpe pero seguimos en rojo porque o se estn devolviendo las cadenas "2 +" y "3 * 1". No estoy partiendo la a expresin en caso que el operador quede al nal. Vamos a escribir un test o espec co para el reparador de expresiones a n de corregir el problema:

1 2 3 4 5 6 7 8 9 10 11 12 13 14

[ TestFixture ] public class E x p r e s s i o n F i x e r T e s t s { [ Test ] public void S p l i t E x p r e s s i o n W h e n O p e r a t o r A t T h e E n d () { ExpressionFixer fixer = new ExpressionFixer (); List < string > expressions = new List < string >(); expressions . Add ( " 2 + " ); fixer . FixExpressions ( expressions ); Assert . Contains ( " 2 " , expressions ); Assert . Contains ( " + " , expressions ); } }

Efectivamente est fallando ah Voy a corregirlo y de paso a modicar un a . poco los nombres de los dos mtodos para denotar que, a pesar de devolver e verdadero o falso, modican la lista de expresiones:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

public class ExpressionFixer { public void FixExpressions ( List < string > expressions ) { bool listHasChanged = true ; while ( listHasChanged ) { listHasChanged = false ; for ( int i = 0; i < expressions . Count ; i ++) if ( I s N u m b e r A n d O p e r a t o r T h e n S p l i t ( expressions , i ) || I s E m p t y E x p r e s s i o n T h e n R e m o v e ( expressions , i )) { listHasChanged = true ; break ; } }

188

Cap tulo 9
} public bool I s N u m b e r A n d O p e r a t o r T h e n S p l i t ( List < string > expressions , int index ) { Regex startsWithOperator = new Regex ( @ " ^(\ s *)([+|\ -|/|*])(\ s +) " ); Regex endsWithOperator = new Regex ( @ " (\ s +)([+|\ -|/|*])(\ s *) $ " ); string exp = expressions [ index ]; exp = exp . Trim (); if ( startsWithOperator . IsMatch ( exp ) || endsWithOperator . IsMatch ( exp )) { splitByOperator ( expressions , exp , index ); return true ; } return false ; } private void splitByOperator ( List < string > expressions , string inputExpression , int position ) { string [] nextExps = Regex . Split ( inputExpression , @ " ([+|\ -|/|*]) " ); int j = position ; expressions . RemoveAt ( j ); foreach ( string subExp in nextExps ) { expressions . Insert (j , subExp . Trim ()); j ++; } } public bool I s E m p t y E x p r e s s i o n T h e n R e m o v e ( List < string > e xpressions , int index ) { if ( expressions [ index ]. Length == 0) { expressions . RemoveAt ( index ); return true ; } return false ; } }

17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62

Ntese que tambin extraje el mtodo splitByOperator como resultado de o e e otra refactorizacin. La hice en dos pasos aunque haya pegado el cdigo una o o sola vez (todo sea por ahorrar papel). Me cost trabajo decidir qu nombre ponerle a los mtodos y al nal o e e el que hemos puesto denota claramente que cada mtodo hace dos cosas. e Est indicando que estamos violando el principio de una unica responsabia lidad. Tratemos de mejorar el diseo. Puesto que tenemos una clase que n gestiona expresiones regulares (ExpressionValidator) tiene sentido que la 189

Cap tulo 9 pregunta de si la expresin contiene un nmero y un operador pase a estar o u ah :


public class ExpressionFixer { ExpressionValidator _validator ; public ExpressionFixer ( ExpressionValidator validator ) { _validator = validator ; } public void FixExpressions ( List < string > expressions ) { bool listHasChanged = true ; while ( listHasChanged ) { listHasChanged = false ; for ( int i = 0; i < expressions . Count ; i ++) { if ( _validator . IsNumberAndOperator ( expressions [ i ])) { splitByOperator ( expressions , expressions [ i ] , i ); listHasChanged = true ; break ; } if ( expressions [ i ]. Length == 0) { expressions . RemoveAt ( i ); listHasChanged = true ; break ; } } } } private void splitByOperator ( List < MathExpression > expr essions , string inputExpression , int position ) { string [] nextExps = Regex . Split ( inputExpression , @ " ([+|\ -|/|*]) " ); int j = position ; expressions . RemoveAt ( j ); foreach ( string subExp in nextExps ) { expressions . Insert (j , new MathExpression ( subExp . Trim ( ))); j ++; } } } public class ExpressionValidator { public bool IsExpressionValid ( string expression ) { Regex fullRegex = new Regex ( @ " ^ -{0 ,1}\ d +((\ s +)[+|\ -|/|*](\ s +) -{0 ,1}\ d +)+ $ " );

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56

190

Cap tulo 9
Regex singleOperator = new Regex ( @ " ^[+|\ -|/|*] $ " ); Regex singleNumber = new Regex ( @ " ^\ d + $ " ); return ( fullRegex . IsMatch ( expression , 0) || singleOperator . IsMatch ( expression , 0) || singleNumber . IsMatch ( expression , 0)); } public bool IsNumberAndOperator ( string expression ) { Regex startsWithOperator = new Regex ( @ " ^(\ s *)([+|\ -|/|*])(\ s +) " ); Regex endsWithOperator = new Regex ( @ " (\ s +)([+|\ -|/|*])(\ s *) $ " ); string exp = expression ; if ( startsWithOperator . IsMatch ( exp ) || endsWithOperator . IsMatch ( exp )) return true ; return false ; } }

57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

Ahora las responsabilidades estn bien repartidas y ningn nombre de mtodo a u e suena extrao. El cdigo fuente debe poderse entender fcilmente al leerlo n o a y para eso es fundamental que los nombres describan con total precisin o qu hacen los mtodos. e e Algo sigue sin encajar del todo. ExpressionValidator es realmente un validador? Ms bien es un clase de consulta de expresiones regulares del a dominio. El que valida es lexer. Buen momento para hacer el cambio:
public class ExpressionFixer { MathRegex _mathRegex ; public ExpressionFixer ( MathRegex mathRegex ) { _mathRegex = mathRegex ; } ... } public class MathRegex { public bool IsExpressionValid ( string expression ) { ... } public bool IsNumberAndOperator ( string expression ) { ... } }

Hemos renombrado ExpressionValidator por MathRegex. Mucho mejor ahora. 191

Cap tulo 9 El test pasa y as tambin el anterior. Luz verde en toda la bater de e a tests. Revisamos la libreta:
# Aceptacin - "(2 + 2) * (3 + 1)", devuelve 16 o "(2 + 2", produce una excepcin o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "2 + -2", devuelve 0 o

Si un parntesis abierto no encuentra correspondencia con otro cerrado, e entonces excepcin: o

1 2 3 4 5 6 7 8 9 10 11 12

[ Test ] public void T h r o w E x c e p t i o n O n O p e n P a r e n t h e s i s () { try { List < string > expressions = _lexer . GetExpressions ( " (2 + 3 * 1 " ); Assert . Fail ( " Exception didn t arise ! " ); } catch ( I n v a l i d O p e r a t i o n E x c e p t i o n ) { } }

Se arregla con dos l neas (26 y 27):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

9.30: MathLexer

public List < string > GetExpressions ( string expression ) { List < string > expressions = new List < string >(); Stack < int > parenthesis = new Stack < int >(); int index = 1; expressions . Add ( String . Empty ); foreach ( char ch in expression ) { if ( ch == ( ) { parenthesis . Push ( index ); index ++; expressions . Add ( String . Empty ); } else if ( ch == ) ) { index = parenthesis . Pop (); } else { expressions [ index -1] += ch . ToString (); } } if ( parenthesis . Count > 0) throw new

192

Cap tulo 9
I n v a l i d O p e r a t i o n E x c e p t i o n ( " Parenthesis do not match " ); _fixer . FixExpressions ( expressions ); return expressions ; }

28 29 30 31 32

Aprovecho tambin para mover algunos tests a sus clases ya que Mathe Lexer ha crecido bastante. El cdigo de GetExpressions me est empezando a hacer dao a la o a n vista. Vamos a ver cmo nos las apaamos para refactorizarlo: o n

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

9.31: MathLexer

public List < string > GetExpressions ( string expression ) { List < string > t o t a l E x p r e s s i o n s F o u n d = new List < string >(); int openedParenthesis = 0; getExpressions ( expression , 0 , String . Empty , totalExpressionsFound , ref openedParenthesis ); if ( openedParenthesis != 0) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( " Parenthesis do not match " ); _fixer . FixExpressions ( t o t a l E x p r e s s i o n s F o u n d ); return t o t a l E x p r e s s i o n s F o u n d ; } // / < summary > // / Returns the p o s i t i o n where the close p a r e n t h e s i s is found or // / the p o s i t i o n of the last char in the string . // / Also p o p u l a t e s the list of e x p r e s s i o n s along the way // / </ summary > private int getExpressions ( string fullInputExpression , int subExpressionStartIndex , string subExpressionUnderConstruction , List < string > totalSubexpressionsFound , ref int openedParanthesis ) { for ( int currentIndex = s u b E x p r e s s i o n S t a r t I n d e x ; currentIndex < fullInputExpression . Length ; currentIndex ++) { char currentChar = fullInputExpression [ currentIndex ]; if ( currentChar == OPEN_SUBEXPRESSION ) { openedParanthesis ++; int closePosition = getExpressions ( fullInputExpression , currentIndex + 1 , String . Empty , totalSubexpressionsFound , ref openedParanthesis ); currentIndex = closePosition ; } else if ( currentChar == CLOSE_SUBEXPRESSION )

193

Cap tulo 9
{ t o t a l S u b e x p r e s s i o n s F o u n d . Add ( s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n ); openedParanthesis - -; return currentIndex ; } else { s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n += fullInputExpression [ currentIndex ]. ToString (); } } t o t a l S u b e x p r e s s i o n s F o u n d . Add ( s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n ); return fullInputExpression . Length ; }

46 47 48 49 50 51 52 53 54 55 56 57 58 59 60

Despus de un rato dndole vueltas no encuentro una manera de deshacerme e a de ese bloque if-else. Si utilizo polimorsmo tengo la sensacin de que se va a o liar todav ms. Ahora la funcin es ms natural, tiene la naturaleza recursiva a a o a de las propias expresiones con parntesis anidados pero tambin es compleja. e e Lo que no me gusta nada es devolver un valor en la funcin recursiva que o en algunos casos no utilizo: en la l nea 7 del listado estoy invocndola sin a utilizar para nada su valor de retorno. Esta es la razn por la que he cre o do conveniente aadir un comentario a la funcin, para sopesar la poca claridad n o de su cdigo. Si pudiera cambiar eso me dar por satisfecho de momento. o a Reintento:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

9.32: MathLexer

public List < string > GetExpressions ( string expression ) { List < string > t o t a l E x p r e s s i o n s F o u n d = new List < string >(); int openedParenthesis = 0; int startSearchingAt = 0; getExpressions ( expression , ref startSearchingAt , String . Empty , totalExpressionsFound , ref openedParenthesis ); if ( openedParenthesis != 0) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( " Parenthesis do not match " ); _fixer . FixExpressions ( t o t a l E x p r e s s i o n s F o u n d ); return t o t a l E x p r e s s i o n s F o u n d ; } private void getExpressions ( string fullInputExpression , ref int subExpressionStartIndex , string subExpressionUnderConstruction , List < string > totalSubexpressionsFound , ref int openedParanthesis ) { for ( int currentIndex = s u b E x p r e s s i o n S t a r t I n d e x ; currentIndex < fullInputExpression . Length ; currentIndex ++)

194

Cap tulo 9
{ char currentChar = fullInputExpression [ currentIndex ]; if ( currentChar == OPEN_SUBEXPRESSION ) { openedParanthesis ++; s u b E x p r e s s i o n S t a r t I n d e x = currentIndex +1; getExpressions ( fullInputExpression , ref subExpressionStartIndex , String . Empty , totalSubexpressionsFound , ref openedParanthesis ); currentIndex = s u b E x p r e s s i o n S t a r t I n d e x ; } else if ( currentChar == CLOSE_SUBEXPRESSION ) { t o t a l S u b e x p r e s s i o n s F o u n d . Add ( s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n ); s u b E x p r e s s i o n S t a r t I n d e x = currentIndex ; openedParanthesis - -; return ; } else { s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n += fullInputExpression [ currentIndex ]. ToString (); } } t o t a l S u b e x p r e s s i o n s F o u n d . Add ( s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n ); s u b E x p r e s s i o n S t a r t I n d e x = s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n . Length ; }

27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56

Ahora todos los tests estn pasando y el cdigo pinta algo mejor. Segua o ramente ms adelante tengamos oportunidad de anarlo. a Volvamos a la libreta:
# Aceptacin - "(2 + 2) * (3 + 1)", devuelve 16 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "2 + -2", devuelve 0 o

Voy a ver qu tal se maneja MathLexer con la expresin que tiene dos e o parntesis: e

1 2 3 4 5 6 7 8

[ Test ] public void G e t E x p r e s s i o n W i t h T w o G r o u p s () { List < string > expressions = _lexer . GetExpressions ( " (2 + 2) * (3 + 1) " ); failIfOtherSubExpressionThan ( expressions , " 3 + 1 " , " 2 + 2 " , " * " ); }

Vaya no funciona. Devuelve las subcadenas que queremos pero se han colado algunos espacios en blanco delante y detrs del asterisco. Escribimos otro test a de grano ms no para solucionarlo: a 195

Cap tulo 9

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

[ TestFixture ] public class E x p r e s s i o n F i x e r T e s t s { ExpressionFixer _fixer ; List < string > _expressions ; [ SetUp ] public void SetUp () { _fixer = new ExpressionFixer (); _expressions = new List < string >(); } [ Test ] public void S p l i t E x p r e s s i o n W h e n O p e r a t o r A t T h e E n d () { _expressions . Add ( " 2 + " ); _fixer . FixExpressions ( _expressions ); Assert . Contains ( " 2 " , _expressions ); Assert . Contains ( " + " , _expressions ); } [ Test ] public void Trim () { _expressions . Add ( " * " ); _fixer . FixExpressions ( _expressions ); Assert . AreEqual ( " * " , _expressions [0]); }

Luz roja. Ya podemos arreglarlo:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

9.33: EspressionFixer

public void FixExpressions ( List < string > expressions ) { bool listHasChanged = true ; while ( listHasChanged ) { listHasChanged = false ; for ( int i = 0; i < expressions . Count ; i ++) { expressions [ i ] = expressions [ i ]. Trim (); if ( _mathRegex . IsNumberAndOperator ( expressions [ i ])) { splitByOperator ( expressions , expressions [ i ] , i ); listHasChanged = true ; break ; } if ( expressions [ i ]. Length == 0) { expressions . RemoveAt ( i ); listHasChanged = true ; break ; } }

196

Cap tulo 9
} }

25 26

1 2 3 4 5 6 7

Ahora funciona el test actual y el anterior (arreglado con l nea 9). Excelente. Hora de resolver una expresin que tiene parntesis: o e
[ Test ] public void P r o c e s s A c c e p t a n c e E x p r e s s i o n W i t h P a r e n t h e s i s () { Assert . AreEqual (16 , _parser . ProcessExpression ( " (2 + 2) * (3 + 1) " )); }

Se lanza una excepcin porque parser est intentando extraer de lexer o a los tokens en lugar de las expresiones2 . Acabamos de disear para que se n extraigan las expresiones primero y luego los tokens de cada una, como si tuviramos dos niveles. Voy a adaptar el cdigo: e o

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 2

9.34: MathParser

public int ProcessExpression ( string expression ) { List < string > subExpressions = _lexer . GetExpressions ( expression ); String flatExpression = String . Empty ; foreach ( string subExp in subExpressions ) { if ( isSubExpression ( subExp )) flatExpression += r e s o l v e S i m p l e E x p r e s s i o n ( subExp ); else flatExpression += " " + subExp + " " ; } return r e s o l v e S i m p l e E x p r e s s i o n ( flatExpression ); } private bool isSubExpression ( string exp ) { Regex operatorRegex = new Regex ( @ " [+|\ -|/|*] " ); Regex numberRegex = new Regex ( @ " \ d + " ); if ( numberRegex . IsMatch ( exp ) && operatorRegex . IsMatch ( exp )) return true ; return false ; } private int r e s o l v e S i m p l e E x p r e s s i o n ( string expression ) { List < MathToken > mathExp = _lexer . GetTokens ( expression ); while ( mathExp . Count > 1) { MathOperator op = GetMaxPrecedence ( mathExp ); int firstNumber , secondNumber ; firstNumber = mathExp [ op . Index - 1]. IntValue ;

Ver listado 11.22 en pgina 155 a

197

Cap tulo 9
secondNumber = mathExp [ op . Index + 1]. IntValue ; int result = op . Resolve ( firstNumber , secondNumber , _calcProxy ); r e p l a c e T o k e n s W i t h R e s u l t ( mathExp , op . Index , result ); } return mathExp [0]. IntValue ; }

35 36 37 38 39 40 41

El mtodo resolveSimpleExpression ya lo ten e amos escrito antes pero con el nombre ProcessExpression. El algoritmo aplana las expresiones hasta llegar a una expresin simple de las que sabe resolver. He escrito mucho o cdigo para implementar el SUT, no he sido estricto con la regla del cdigo o o m nimo y esto me ha hecho anotar en la libreta que me gustar escribir a algunos tests para isSubExpression ms adelante. Si no lo anoto en la a libreta seguro que me olvido. El test sigue fallando porque el orden en que llegan las subexpresiones del lexer est cambiado. Est llegando "2 + 2" "3 a a + 1" y "*". Para resolverlo tendr que escribir un nuevo test para lexer que e exija que el orden de aparicin de las subexpresiones se mantenga: o

1 2 3 4 5 6 7 8 9 10 11

[ Test ] public void G e t S e v e r a l P a r e n t h e s i s E x p r e s s i o n s I n O r d e r () { List < string > expressions = _lexer . GetExpressions ( " 2 + 2) * (3 + 1) " ); foreach ( string exp in expressions ) Console . Out . WriteLine ( " x : " + exp + " . " ); Assert . AreEqual ( " 2 + 2 " , expressions [0]); Assert . AreEqual ( " * " , expressions [1]); Assert . AreEqual ( " 3 + 1 " , expressions [2]); }

Tal como tenemos la funcin en el lexer puedo hacer que se registre el orden o de aparicin de cada subexpresin y luego ordenar si me ayudo de una nueva o o clase (MathExpression):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

9.35: MathLexer

public List < MathExpression > GetExpressions ( string expr ession ) { List < MathExpression > t o t a l E x p r e s s i o n s F o u n d = new List < MathExpression >(); int openedParenthesis = 0; int startSearchingAt = 0; getExpressions ( expression , ref startSearchingAt , new MathExpression ( String . Empty ) , totalExpressionsFound , ref openedParenthesis ); if ( openedParenthesis != 0) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( " Parenthesis do not match " ); _fixer . FixExpressions ( t o t a l E x p r e s s i o n s F o u n d ); return t o t a l E x p r e s s i o n s F o u n d ; }

198

Cap tulo 9

18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62

private void getExpressions ( string fullInputExpression , ref int subExpressionStartIndex , MathExpression subExpressionUnderConstruction , List < MathExpression > totalSubexpressionsFound , ref int openedParanthesis ) { for ( int currentIndex = s u b E x p r e s s i o n S t a r t I n d e x ; currentIndex < fullInputExpression . Length ; currentIndex ++) { char currentChar = fullInputExpression [ currentIndex ]; if ( currentChar == OPEN_SUBEXPRESSION ) { openedParanthesis ++; s u b E x p r e s s i o n S t a r t I n d e x = currentIndex +1; getExpressions ( fullInputExpression , ref subExpressionStartIndex , new MathExpression ( String . Empty , subExpressionStartIndex ), totalSubexpressionsFound , ref openedParanthesis ); currentIndex = s u b E x p r e s s i o n S t a r t I n d e x ; } else if ( currentChar == CLOSE_SUBEXPRESSION ) { t o t a l S u b e x p r e s s i o n s F o u n d . Add ( s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n ); s u b E x p r e s s i o n S t a r t I n d e x = currentIndex ; openedParanthesis - -; return ; } else { s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n . Expression += fullInputExpression [ currentIndex ]. ToString (); if ( s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n . Order == -1) s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n . Order = currentIndex ; } } t o t a l S u b e x p r e s s i o n s F o u n d . Add ( s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n ); subExpressionStartIndex = s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n . Expression . Length ; }


1 2 3 4 5 6 7 8 9 10

9.36: MathExpression

public class MathExpression { private string _expression ; private int _order ; public MathExpression ( string expression ) { _expression = expression ; _order = -1; }

199

Cap tulo 9

11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

public MathExpression ( string expression , int order ) { _expression = expression ; _order = order ; } public string Expression { get { return _expression ; } set { _expression = value ; } } public int Order { get { return _order ; } set { _order = value ; } } }

He cambiado las cadenas por un objeto sencillo llamado MathExpression que guarda la posicin en que aparece la subexpresin dentro de la expresin o o o de entrada. Solamente me falta ordenar las subexpresiones para hacer pasar el test:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

9.37: MathLexer

public List < MathExpression > GetExpressions ( string expr ession ) { List < MathExpression > t o t a l E x p r e s s i o n s F o u n d = new List < MathExpression >(); int openedParenthesis = 0; int startSearchingAt = 0; getExpressions ( expression , ref startSearchingAt , new MathExpression ( String . Empty ) , totalExpressionsFound , ref openedParenthesis ); if ( openedParenthesis != 0) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( " Parenthesis do not match " ); _fixer . FixExpressions ( t o t a l E x p r e s s i o n s F o u n d ); b u b b l e S o r t E x p r e s s i o n s ( t o t a l E x p r e s s i o n s F o u n d ); return t o t a l E x p r e s s i o n s F o u n d ; } private void b u b b l e S o r t E x p r e s s i o n s ( List < MathExpression > subExpressions ) { for ( int i = 0; i < subExpressions . Count ; i ++) { for ( int j = 0; j < subExpressions . Count -1; j ++) { MathExpression exp1 = subExpressions [ j ]; MathExpression exp2 = subExpressions [ j + 1]; if ( exp2 . Order < exp1 . Order ) { subExpressions [ j ] = exp2 ;

200

Cap tulo 9
subExpressions [ j + 1] = exp1 ; } } } }

32 33 34 35 36 37

Una ordenacin sencilla bastar por ahora. Luego hago un spike para ver si o a .Net me ordena la lista (lo anoto en la libreta). El ultimo test ya pasa y todos los dems tambin, con la excepcin de ProcessAcceptanceExpressiona e o WithParenthesis, que todav tiene un pequeo bug por falta de adaptar a n el cdigo a la nueva clase MathExpression. o Corrijo:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

9.38: parte de MathParser

public int ProcessExpression ( string expression ) { List < MathExpression > subExpressions = _lexer . GetExpressions ( expression ); String flatExpression = String . Empty ; foreach ( MathExpression subExp in subExpressions ) { if ( isSubExpression ( subExp . Expression )) flatExpression += r e s o l v e S i m p l e E x p r e s s i o n ( subExp . Expression ); else flatExpression += " " + subExp . Expression + " " ; } return r e s o l v e S i m p l e E x p r e s s i o n ( flatExpression ); } private bool isSubExpression ( string exp ) { Regex operatorRegex = new Regex ( @ " [+|\ -|/|*] " ); Regex numberRegex = new Regex ( @ " \ d + " ); if ( numberRegex . IsMatch ( exp ) && operatorRegex . IsMatch ( exp )) { Console . Out . WriteLine ( " YES : " + exp ); return true ; } return false ; } private int r e s o l v e S i m p l e E x p r e s s i o n ( string expression ) { List < MathToken > mathExp = _lexer . GetTokens ( expression ); while ( mathExp . Count > 1) { MathOperator op = GetMaxPrecedence ( mathExp ); int firstNumber , secondNumber ; firstNumber = mathExp [ op . Index - 1]. IntValue ; secondNumber = mathExp [ op . Index + 1]. IntValue ; int result = op . Resolve ( firstNumber , secondNumber , _calcProxy );

201

Cap tulo 9
r e p l a c e T o k e n s W i t h R e s u l t ( mathExp , op . Index , result ); } return mathExp [0]. IntValue ; }

42 43 44 45

Ya funciona! Hay algo que refactorizar ahora? S hay unos cuantos mtodos , e que no estn en el sitio ms adecuado. a a Para recordar al lector cmo est ahora mismo nuestro diagrama de clases o a emergente veamos la siguiente gura:

1 2 3 4

Movamos isSubExpression a nuestro MathRegex. Vaya, en ese caso necesito inyectar MathRegex en MathParser aunque ya estaba inyectado en MathLexer. Eso de que parser necesite de lexer y que ambos tengan inyectado al que entiende de expresiones regulares, pinta mal. Para qu estamos e usando lexer? Para extraer elementos de la expresin de entrada. Y parser? o Para encontrarles el sentido a los elementos. Entonces, a quin corresponde e la validacin de la expresin que est haciendo ahora lexer? A parser! De o o a acuerdo, lo primero que muevo es la validacin de expresiones: o
public class MathParser { Lexer _lexer ; MathRegex _mathRegex ;

202

Cap tulo 9
CalculatorProxy _calcProxy ; public MathParser ( Lexer lexer , CalculatorProxy calcProxy , MathRegex mathRegex ) { _lexer = lexer ; _mathRegex = mathRegex ; _calcProxy = calcProxy ; } public MathOperator GetMaxPrecedence ( List < MathToken > t okens ) { int precedence = 0; MathOperator m a x P r e c e d e n c e O p e r a t o r = null ; int index = -1; foreach ( MathToken token in tokens ) { index ++; if ( token . isOperator ()) { MathOperator op = OperatorFactory . Create ( token ); if ( op . Precedence >= precedence ) { precedence = op . Precedence ; m a x P r e c e d e n c e O p e r a t o r = op ; m a x P r e c e d e n c e O p e r a t o r . Index = index ; } } } return m a x P r e c e d e n c e O p e r a t o r ; } public int ProcessExpression ( string expression ) { List < MathExpression > subExpressions = _lexer . GetExpressions ( expression ); String flatExpression = String . Empty ; foreach ( MathExpression subExp in subExpressions ) { if ( _mathRegex . IsSubExpression ( subExp . Expression )) flatExpression += r e s o l v e S i m p l e E x p r e s s i o n ( subExp . Expression ); else flatExpression += " " + subExp . Expression + " " ; } return r e s o l v e S i m p l e E x p r e s s i o n ( flatExpression ); } private int r e s o l v e S i m p l e E x p r e s s i o n ( string expression ) { if (! _mathRegex . IsExpressionValid ( expression )) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( expression ); List < MathToken > mathExp = _lexer . GetTokens ( expression ); while ( mathExp . Count > 1) { MathOperator op = GetMaxPrecedence ( mathExp );

5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

203

Cap tulo 9
int firstNumber , secondNumber ; firstNumber = mathExp [ op . Index - 1]. IntValue ; secondNumber = mathExp [ op . Index + 1]. IntValue ; int result = op . Resolve ( firstNumber , secondNumber , _calcProxy ); r e p l a c e T o k e n s W i t h R e s u l t ( mathExp , op . Index , result ); } return mathExp [0]. IntValue ; } private void r e p l a c e T o k e n s W i t h R e s u l t ( List < MathToken > tokens , int indexOfOperator , int result ) { tokens [ indexOfOperator - 1] = new MathToken ( result . ToString ()); tokens . RemoveAt ( indexOfOperator ); tokens . RemoveAt ( indexOfOperator ); } } public class MathLexer : Lexer { ExpressionFixer _fixer ; static char OPEN_SUBEXPRESSION = ( ; static char CLOSE_SUBEXPRESSION = ) ; public MathLexer ( ExpressionFixer fixer ) { _fixer = fixer ; } public List < MathToken > GetTokens ( string expression ) { string [] items = splitExpression ( expression ); return c r e a t e T o k e n s F r o m S t r i n g s ( items ); } private string [] splitExpression ( string expression ) { return expression . Split (( new char [] { , \ t }) , StringSplitOptions . RemoveEmptyEntries ); } private List < MathToken > c r e a t e T o k e n s F r o m S t r i n g s ( strin g [] items ) { List < MathToken > tokens = new List < MathToken >(); foreach ( String item in items ) { tokens . Add ( new MathToken ( item )); } return tokens ; } public List < MathExpression > GetExpressions ( string expr ession ) { List < MathExpression > t o t a l E x p r e s s i o n s F o u n d = new List < MathExpression >();

64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122

204

Cap tulo 9
int openedParenthesis = 0; int startSearchingAt = 0; getExpressions ( expression , ref startSearchingAt , new MathExpression ( String . Empty ) , totalExpressionsFound , ref openedParenthesis ); if ( openedParenthesis != 0) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( " Parenthesis do not match " ); _fixer . FixExpressions ( t o t a l E x p r e s s i o n s F o u n d ); b u b b l e S o r t E x p r e s s i o n s ( t o t a l E x p r e s s i o n s F o u n d ); return t o t a l E x p r e s s i o n s F o u n d ; } private void b u b b l e S o r t E x p r e s s i o n s ( List < MathExpression > subExpressions ) { for ( int i = 0; i < subExpressions . Count ; i ++) { for ( int j = 0; j < subExpressions . Count -1; j ++) { MathExpression exp1 = subExpressions [ j ]; MathExpression exp2 = subExpressions [ j + 1]; if ( exp2 . Order < exp1 . Order ) { subExpressions [ j ] = exp2 ; subExpressions [ j + 1] = exp1 ; } } } } private void getExpressions ( string fullInputExpression , ref int subExpressionStartIndex , MathExpression subExpressionUnderConstruction , List < MathExpression > totalSubexpressionsFound , ref int openedParanthesis ) { for ( int currentIndex = s u b E x p r e s s i o n S t a r t I n d e x ; currentIndex < fullInputExpression . Length ; currentIndex ++) { char currentChar = fullInputExpression [ currentIndex ]; if ( currentChar == OPEN_SUBEXPRESSION ) { openedParanthesis ++; s u b E x p r e s s i o n S t a r t I n d e x = currentIndex +1; getExpressions ( fullInputExpression , ref subExpressionStartIndex , new MathExpression ( String . Empty , subExpressionStartIndex ), totalSubexpressionsFound , ref openedParanthesis ); currentIndex = s u b E x p r e s s i o n S t a r t I n d e x ; } else if ( currentChar == CLOSE_SUBEXPRESSION ) { t o t a l S u b e x p r e s s i o n s F o u n d . Add (

123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181

205

Cap tulo 9
s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n ); s u b E x p r e s s i o n S t a r t I n d e x = currentIndex ; openedParanthesis - -; return ; } else { s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n . Expression += fullInputExpression [ currentIndex ]. ToString (); if ( s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n . Order == -1) s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n . Order = currentIndex ; } } t o t a l S u b e x p r e s s i o n s F o u n d . Add ( s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n ); subExpressionStartIndex = s u b E x p r e s s i o n U n d e r C o n s t r u c t i o n . Expression . Length ; } } public class MathRegex { public bool IsExpressionValid ( string expression ) { Regex fullRegex = new Regex ( @ " ^ -{0 ,1}\ d +((\ s +)[+|\ -|/|*](\ s +) -{0 ,1}\ d +)+ $ " ); Regex singleOperator = new Regex ( @ " ^[+|\ -|/|*] $ " ); Regex singleNumber = new Regex ( @ " ^\ d + $ " ); return ( fullRegex . IsMatch ( expression , 0) || singleOperator . IsMatch ( expression , 0) || singleNumber . IsMatch ( expression , 0)); } public bool IsNumberAndOperator ( string expression ) { Regex startsWithOperator = new Regex ( @ " ^(\ s *)([+|\ -|/|*])(\ s +) " ); Regex endsWithOperator = new Regex ( @ " (\ s +)([+|\ -|/|*])(\ s *) $ " ); string exp = expression ; if ( startsWithOperator . IsMatch ( exp ) || endsWithOperator . IsMatch ( exp )) return true ; return false ; } public bool IsSubExpression ( string expression ) { Regex operatorRegex = new Regex ( @ " [+|\ -|/|*] " ); Regex numberRegex = new Regex ( @ " \ d + " ); if ( numberRegex . IsMatch ( expression ) && operatorRegex . IsMatch ( expression )) return true ; return false ; } }

182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238

He corregido los tests para que compilen despus de los cambios y falla uno. e 206

Cap tulo 9 Aquel que le dice al lexer que ante una expresin invlida lance una excepcin. o a o Se debe a que ya lexer no valida expresiones sino parser. Entonces tenemos que mover el test a parser. Sin embargo, el mtodo resolveSimpleExe pression de parser es privado y no lo podemos testear directamente. Me empieza a parecer que parser tiene demasiadas responsabilidades. Est bien a que entienda la expresin pero preero que sea otra clase la que resuelva las o operaciones:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

public class Resolver { MathRegex _mathRegex ; Lexer _lexer ; CalculatorProxy _calcProxy ; public Resolver ( MathRegex mathRegex , Lexer lexer , CalculatorProxy calcProxy ) { _mathRegex = mathRegex ; _lexer = lexer ; _calcProxy = calcProxy ; } public MathOperator GetMaxPrecedence ( List < MathToken > t okens ) { int precedence = 0; MathOperator m a x P r e c e d e n c e O p e r a t o r = null ; int index = -1; foreach ( MathToken token in tokens ) { index ++; if ( token . isOperator ()) { MathOperator op = OperatorFactory . Create ( token ); if ( op . Precedence >= precedence ) { precedence = op . Precedence ; m a x P r e c e d e n c e O p e r a t o r = op ; m a x P r e c e d e n c e O p e r a t o r . Index = index ; } } } return m a x P r e c e d e n c e O p e r a t o r ; } public int R e s o l v e S i m p l e E x p r e s s i o n ( string expression ) { if (! _mathRegex . IsExpressionValid ( expression )) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( expression ); List < MathToken > mathExp = _lexer . GetTokens ( expression ); while ( mathExp . Count > 1) { MathOperator op = GetMaxPrecedence ( mathExp ); int firstNumber , secondNumber ;

207

Cap tulo 9
firstNumber = mathExp [ op . Index - 1]. IntValue ; secondNumber = mathExp [ op . Index + 1]. IntValue ; int result = op . Resolve ( firstNumber , secondNumber , _calcProxy ); r e p l a c e T o k e n s W i t h R e s u l t ( mathExp , op . Index , result ); } return mathExp [0]. IntValue ; } private void r e p l a c e T o k e n s W i t h R e s u l t ( List < MathToken > tokens , int indexOfOperator , int result ) { tokens [ indexOfOperator - 1] = new MathToken ( result . ToString ()); tokens . RemoveAt ( indexOfOperator ); tokens . RemoveAt ( indexOfOperator ); } } public class MathParser { Lexer _lexer ; MathRegex _mathRegex ; Resolver _resolver ; public MathParser ( Lexer lexer , MathRegex mathRegex , Resolver resolver ) { _lexer = lexer ; _resolver = resolver ; _mathRegex = mathRegex ; } public int ProcessExpression ( string expression ) { List < MathExpression > subExpressions = _lexer . GetExpressions ( expression ); String flatExpression = String . Empty ; foreach ( MathExpression subExp in subExpressions ) { if ( _mathRegex . IsSubExpression ( subExp . Expression )) flatExpression += _resolver . R e s o l v e S i m p l e E x p r e s s i o n ( subExp . Expression ); else flatExpression += " " + subExp . Expression + " " ; } return _resolver . R e s o l v e S i m p l e E x p r e s s i o n ( flatExpress ion ); } }

49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98

Tuve que mover un test de sitio y modicar las llamadas a los constructores puesto que han cambiado. Ahora hay demasiadas inyecciones y dependencias. Demasiadas clases necesitan a MathRegex. Es hora de que se convierta en una serie de mtodos estticos3 y deje de ser una dependencia inyectada. e a
3

Usar mtodos estticos es casi siempre una mala idea, al nal del cap e a tulo se vuelve

208

Cap tulo 9 9.39: Resolver

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

public int R e s o l v e S i m p l e E x p r e s s i o n ( string expression ) { if (! MathRegex . IsExpressionValid ( expression )) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( expression ); List < MathToken > mathExp = _lexer . GetTokens ( expression ); while ( mathExp . Count > 1) { MathOperator op = GetMaxPrecedence ( mathExp ); int firstNumber , secondNumber ; firstNumber = mathExp [ op . Index - 1]. IntValue ; secondNumber = mathExp [ op . Index + 1]. IntValue ; int result = op . Resolve ( firstNumber , secondNumber , _calcProxy ); r e p l a c e T o k e n s W i t h R e s u l t ( mathExp , op . Index , result ); } return mathExp [0]. IntValue ; }

Por cierto, quer aadir un test para las subexpresiones: a n

1 2 3 4 5

9.40: MathRegexTests

[ Test ] public void IsSubExpression () { Assert . IsTrue ( MathRegex . IsSubExpression ( " 2 + 2 " )); }

Luz verde. Este test me simplicar la bsqueda de posibles defectos. a u Veamos el diagrama de clases resultante: Los mtodos que empiezan en mayscula son pblicos y los que empiezan e u u en minscula privados. u Queda algo ms por mejorar? Si quisiramos ser eles a la teor de a e a compiladores entonces la funcin GetExpressions de lexer se mover a o a parser ya que en ella se hace la validacin de parntesis y se le busca sentido o e a las expresiones. Como no estoy diseando un compilador ni siguiendo el n mtodo tradicional de construccin de una herramienta de anlisis de cdigo, e o a o no me importa que mis clases no coincidan exactamente con la teor a. Por otra parte, hab amos hecho un mtodo de ordenacin que probablee o mente no sea necesario ya que .Net seguramente lo resuelve. En el momento de escribirlo me result ms rpido utilizar el conocido mtodo de la burbuja o a a e que hacer un spike. Ahora acabo de hacer el spike y veo que slo con imo plementar la interfaz IComparable en MathExpression ya puedo utilizar el mtodo Sort de las listas: e

a hablar sobre esto

9.41: MathExpression

209

Cap tulo 9

Figura 9.1: Diagrama de clases actual


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

public class MathExpression : IComparable { private string _expression ; private int _order ; public MathExpression ( string expression ) { _expression = expression ; _order = -1; } public MathExpression ( string expression , int order ) { _expression = expression ; _order = order ; } public string Expression { get { return _expression ; } set { _expression = value ; } } public int Order { get { return _order ; } set { _order = value ; }

210

Cap tulo 9
} public bool IsEmpty () { return _expression . Length == 0; } public int CompareTo ( Object obj ) { MathExpression exp = ( MathExpression ) obj ; return _order . CompareTo ( exp . Order ); } }

28 29 30 31 32 33 34 35 36 37 38 39 40


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

9.42: MathLexer

public List < MathExpression > GetExpressions ( string expr ession ) { List < MathExpression > t o t a l E x p r e s s i o n s F o u n d = new List < MathExpression >(); int openedParenthesis = 0; int startSearchingAt = 0; getExpressions ( expression , ref startSearchingAt , new MathExpression ( String . Empty ) , totalExpressionsFound , ref openedParenthesis ); if ( openedParenthesis != 0) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( " Parenthesis do not match " ); _fixer . FixExpressions ( t o t a l E x p r e s s i o n s F o u n d ); t o t a l E x p r e s s i o n s F o u n d . Sort (); // - - - > O r d e n a c i o n return t o t a l E x p r e s s i o n s F o u n d ; }

El mtodo de ordenacin de la burbuja fue eliminado por completo. e o Aad el mtodo IsEmpty a MathExpression para encapsular esta can e racter stica que se usaba aqu (l nea 18):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

9.43: ExpressionFixer

public void FixExpressions ( List < MathExpression > expres sions ) { bool listHasChanged = true ; while ( listHasChanged ) { listHasChanged = false ; for ( int i = 0; i < expressions . Count ; i ++) { expressions [ i ]. Expression = expressions [ i ]. Expression . Trim (); if ( MathRegex . IsNumberAndOperator ( expressions [ i ]. Expression )) { splitByOperator ( expressions , expressions [ i ]. Expression , i ); listHasChanged = true ;

211

Cap tulo 9
break ; } if ( expressions [ i ]. IsEmpty ()) { expressions . RemoveAt ( i ); listHasChanged = true ; break ; } } } }

17 18 19 20 21 22 23 24 25 26 27

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38

Por ultimo nos hab quedado pendiente estudiar la relacin entre las a o clases MathToken y MathOperator. La verdad es que el mtodo GetMaxe Precedence de Resolver est haciendo demasiadas cosas. No slo busca a o la precedencia sino que devuelve un objeto operador. Al leer el cdigo me da o la sensacin de que ese metodo deber ser privado o mejor, estar en otra o a clase. Voy a refactorizar:
public class MathToken { protected int _precedence = 0; protected string _token = String . Empty ; protected int _index = -1; public MathToken ( string token ) { _token = token ; } public MathToken ( int precedence ) { _precedence = precedence ; } public int Index { get { return _index ; } set { _index = value ; } } public string Token { get { return _token ; } } public int Precedence { get { return _precedence ; } } // E l i m i n a d o metodo I s O p e r a t o r } public abstract class MathOperator : MathToken { public MathOperator ( int precedence )

212

Cap tulo 9
: base ( precedence ) { } public abstract int Resolve ( int a , int b , CalculatorProxy calcProxy ); } public class MathNumber : MathToken { public MathNumber () : base (0) {} public MathNumber ( string token ) : base ( token ) {} public int IntValue { get { return Int32 . Parse ( _token ); } } public static int GetTokenValue ( string token ) { return Int32 . Parse ( token ); } } public class Resolver { Lexer _lexer ; CalculatorProxy _calcProxy ; public Resolver ( Lexer lexer , CalculatorProxy calcProxy ) { _lexer = lexer ; _calcProxy = calcProxy ; } public MathToken GetMaxPrecedence ( List < MathToken > toke ns ) { int precedence = 0; MathToken maxPrecedenceToken = null ; int index = -1; foreach ( MathToken token in tokens ) { index ++; if ( token . Precedence >= precedence ) { precedence = token . Precedence ; maxPrecedenceToken = token ; maxPrecedenceToken . Index = index ; } } return maxPrecedenceToken ; }

39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97

213

Cap tulo 9
public int R e s o l v e S i m p l e E x p r e s s i o n ( string expression ) { if (! MathRegex . IsExpressionValid ( expression )) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( expression ); List < MathToken > mathExp = _lexer . GetTokens ( expression ); while ( mathExp . Count > 1) { MathToken token = GetMaxPrecedence ( mathExp ); MathOperator op = OperatorFactory . Create ( token ); int firstNumber , secondNumber ; firstNumber = MathNumber . GetTokenValue ( mathExp [ op . Index - 1]. Token ); secondNumber = MathNumber . GetTokenValue ( mathExp [ op . Index + 1]. Token ); int result = op . Resolve ( firstNumber , secondNumber , _calcProxy ); r e p l a c e T o k e n s W i t h R e s u l t ( mathExp , op . Index , result ); } return MathNumber . GetTokenValue ( mathExp [0]. Token ); } private void r e p l a c e T o k e n s W i t h R e s u l t ( List < MathToken > tokens , int indexOfOperator , int result ) { tokens [ indexOfOperator - 1] = new MathToken ( result . ToString ()); tokens . RemoveAt ( indexOfOperator ); tokens . RemoveAt ( indexOfOperator ); } }

98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130

Al ejecutar los tests veo que todos los del parser fallan porque OperatorFactory.Create est intentando crear operadores a partir de tokens que no a lo son. Lexer est construyendo todos los tokens con precedencia cero, por a lo que no hay distincin: o

1 2 3 4 5 6 7 8 9

9.44: MathLexer

private List < MathToken > c r e a t e T o k e n s F r o m S t r i n g s ( strin g [] items ) { List < MathToken > tokens = new List < MathToken >(); foreach ( String item in items ) { tokens . Add ( new MathToken ( item )); } return tokens ; }

Si este mtodo crease tokens de tipo operador o nmero tendr e u amos el problema resuelto. Voy a pedirle a MathRegex que me responda si los tokens son operadores o nmeros: u

9.45: MathRegexTests 214

Cap tulo 9
[ Test ] public void IsNumber () { Assert . IsTrue ( MathRegex . IsNumber ( " 22 " )); }

1 2 3 4 5

Ahora el SUT:

1 2 3 4 5

9.46: MathRegex

public static bool IsNumber ( string token ) { Regex exactNumber = new Regex ( @ " ^\ d + $ " ); return exactNumber . IsMatch ( token , 0); }

No necesito un test para comprobar que el token es un nmero si tiene u espacios delante o detrs porque la funcin va a ser usada cuando ya se han a o ltrado los espacios. Es operador?

1 2 3 4 5 6 7

[ Test ] public void IsOperator () { string operators = " *+/ - " ; foreach ( char op in operators ) Assert . IsTrue ( MathRegex . IsOperator ( op . ToString ())); }


1 2 3 4 5

9.47: MathRegex

public static bool IsOperator ( string token ) { Regex exactOperator = new Regex ( @ " ^[*|\ -|/|+] $ " ); return exactOperator . IsMatch ( token , 0); }

Hay cdigo duplicado en MathRegex? S se repiten a menudo partes de o , expresiones regulares. Refactorizo:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

public class MathRegex { public static string operators = @ " [*|\ -|/|+] " ; public static bool IsExpressionValid ( string expression ) { Regex fullRegex = new Regex ( @ " ^ -{0 ,1}\ d +((\ s +) " + operators + @ " (\ s +) -{0 ,1}\ d +)+ $ " ); return ( fullRegex . IsMatch ( expression , 0) || IsNumber ( expression ) || IsOperator ( expression )); } public static bool IsNumberAndOperator ( string expressio n ) { Regex startsWithOperator =

215

Cap tulo 9
new Regex ( @ " ^(\ s *)( " + operators + @ " )(\ s +) " ); Regex endsWithOperator = new Regex ( @ " (\ s +)( " + operators + @ " )(\ s *) $ " ); string exp = expression ; if ( startsWithOperator . IsMatch ( exp ) || endsWithOperator . IsMatch ( exp )) return true ; return false ; } public static bool IsSubExpression ( string expression ) { Regex operatorRegex = new Regex ( operators ); Regex numberRegex = new Regex ( @ " \ d + " ); if ( numberRegex . IsMatch ( expression ) && operatorRegex . IsMatch ( expression )) return true ; return false ; } public static bool IsNumber ( string token ) { return IsExactMatch ( token , @ " \ d + " ); } public static bool IsOperator ( string token ) { return IsExactMatch ( token , operators ); } public static bool IsExactMatch ( string token , string rege x ) { Regex exactRegex = new Regex ( @ " ^ " + regex + " $ " ); return exactRegex . IsMatch ( token , 0); }

17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52

Por dnde o bamos? Ejecuto toda la bater de tests y veo que lexer est dea a volviendo tokens genricos. Ahora que ya sabemos distinguir nmeros de e u operadores podemos hacer que lexer construya los objetos adecuadamente:

1 2 3 4 5 6 7 8

9.48: LexerTests

[ Test ] public void G e t T o k e n s R i g h t S u b c l a s s e s () { List < MathToken > tokens = _lexer . GetTokens ( " 2 + 2 " ); Assert . IsTrue ( tokens [0] is MathNumber ); Assert . IsTrue ( tokens [1] is MathOperator ); }

SUT:

1 2

9.49: MathLexer

private List < MathToken > c r e a t e T o k e n s F r o m S t r i n g s ( strin g [] items ) {

216

Cap tulo 9
List < MathToken > tokens = new List < MathToken >(); foreach ( String item in items ) { if ( MathRegex . IsOperator ( item )) tokens . Add ( OperatorFactory . Create ( item )); else tokens . Add ( new MathNumber ( item )); } return tokens ; }

3 4 5 6 7 8 9 10 11 12

El test pasa. Pasan todos los tests?. No, an faltaba adaptar replaceTou kensWithResult de Resolver para que devuelva un MathNumber en lugar de un token genrico. Ya pasan todos los tests menos uno que discutiremos e un poco ms adelante4 . El cdigo queda as a o :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 4

9.50: Resolver

public class Resolver { Lexer _lexer ; CalculatorProxy _calcProxy ; public Resolver ( Lexer lexer , CalculatorProxy calcProxy ) { _lexer = lexer ; _calcProxy = calcProxy ; } public MathToken GetMaxPrecedence ( List < MathToken > toke ns ) { int precedence = 0; MathToken maxPrecedenceToken = null ; int index = -1; foreach ( MathToken token in tokens ) { index ++; if ( token . Precedence >= precedence ) { precedence = token . Precedence ; maxPrecedenceToken = token ; maxPrecedenceToken . Index = index ; } } return maxPrecedenceToken ; } public int R e s o l v e S i m p l e E x p r e s s i o n ( string expression ) { if (! MathRegex . IsExpressionValid ( expression )) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( expression ); List < MathToken > mathExp = _lexer . GetTokens ( expression );

Lo pospongo por nes docentes

217

Cap tulo 9
while ( mathExp . Count > 1) { MathToken token = GetMaxPrecedence ( mathExp ); MathOperator op = ( MathOperator ) token ; int firstNumber , secondNumber ; firstNumber = (( MathNumber ) mathExp [ op . Index - 1]). IntValue ; secondNumber = (( MathNumber ) mathExp [ op . Index + 1]). IntValue ; int result = op . Resolve ( firstNumber , secondNumber , _calcProxy ); r e p l a c e T o k e n s W i t h R e s u l t ( mathExp , op . Index , result ); } return (( MathNumber ) mathExp [0]). IntValue ; } private void r e p l a c e T o k e n s W i t h R e s u l t ( List < MathToken > tokens , int indexOfOperator , int result ) { tokens [ indexOfOperator - 1] = new MathNumber ( result . ToSt ring ()); tokens . RemoveAt ( indexOfOperator ); tokens . RemoveAt ( indexOfOperator ); } }

38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

Movemos el clculo de precedencia a una clase espec a ca?:


public interface TokenPrecedence { MathToken GetMaxPrecedence ( List < MathToken > tokens ); } public class Precedence : TokenPrecedence { public MathToken GetMaxPrecedence ( List < MathToken > toke ns ) { int precedence = 0; MathToken maxPrecedenceToken = null ; int index = -1; foreach ( MathToken token in tokens ) { index ++; if ( token . Precedence >= precedence ) { precedence = token . Precedence ; maxPrecedenceToken = token ; maxPrecedenceToken . Index = index ; } } return maxPrecedenceToken ; } } public class Resolver { Lexer _lexer ; CalculatorProxy _calcProxy ; TokenPrecedence _precedence ;

218

Cap tulo 9

33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54

public Resolver ( Lexer lexer , CalculatorProxy calcProxy , TokenPrecedence precedence ) { _lexer = lexer ; _calcProxy = calcProxy ; _precedence = precedence ; } public int R e s o l v e S i m p l e E x p r e s s i o n ( string expression ) { if (! MathRegex . IsExpressionValid ( expression )) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( expression ); List < MathToken > mathExp = _lexer . GetTokens ( expression ); while ( mathExp . Count > 1) { MathToken token = _precedence . GetMaxPrecedence ( mathExp ); ... ... } }

Hubo que recticar un poquito los tests para que compilasen pero fue cuestin o de un minuto. Ahora ejecuto todos los tests y falla uno que hab quedado por ah pena diente: ParserWorksWithLexer5 . El motivo es que se estn haciendo varias a llamadas al lexer y el mock estricto dice que slo le hab avisado de una. o an Para corregir el test podr partir en dos, ya que hay dos llamadas. Una la sia mular tipo stub y la otra tipo mock y viceversa. Pero es demasiado trabajo. a A estas alturas el test no merece la pena porque toda la lgica de negocio de o parser utiliza a lexer. Se ha convertido en una depedencia dif de eludir, con cil lo cual, un test que comprueba que se usa, es poco importante. Simplemente lo elimino. Ya tenemos todos los tests en verde. Repasemos el diagrama de clases: Qu sensacin da el diagrama y el cdigo? Personalmente me gusta e o o como ha quedado el diagrama pero hay cdigo que da un poco de mal olor. o Se trata de ResolveSimpleExpression ya que est violando el Principio de a 6 . Generalmente los typecasts son un indicador de algo Sustitucin de Liskov o que no se est haciendo del todo bien. Recordemos cmo qued el mtodo: a o o e

1 2 3 4 5 5 6

9.51: Resolver

public int R e s o l v e S i m p l e E x p r e s s i o n ( string expression ) { if (! MathRegex . IsExpressionValid ( expression )) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( expression );

pgina 148 a Ver Cap tulo 7 en la pgina 111 a

219

Cap tulo 9

6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

List < MathToken > mathExp = _lexer . GetTokens ( expression ); while ( mathExp . Count > 1) { MathToken token = _precedence . GetMaxPrecedence ( mathExp ); MathOperator op = ( MathOperator ) token ; int firstNumber , secondNumber ; firstNumber = (( MathNumber ) mathExp [ op . Index - 1]). IntValue ; secondNumber = (( MathNumber ) mathExp [ op . Index + 1]). IntValue ; int result = op . Resolve ( firstNumber , secondNumber , _calcProxy ); r e p l a c e T o k e n s W i t h R e s u l t ( mathExp , op . Index , result ); } return (( MathNumber ) mathExp [0]). IntValue ; }

Si refactorizo de manera que Resolve queda en la clase base (Pull up method), podr tratar a todos los tokens por igual: a

1 2 3 4 5 6 7 8

9.52: Resolver

public int R e s o l v e S i m p l e E x p r e s s i o n ( string expression ) { if (! MathRegex . IsExpressionValid ( expression )) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( expression ); List < MathToken > mathExp = _lexer . GetTokens ( expression ); while ( mathExp . Count > 1) {

220

Cap tulo 9
MathOperator op = _precedence . GetMaxPrecedence ( mathExp ); int firstNumber = mathExp [ op . Index - 1]. Resolve (); int secondNumber = mathExp [ op . Index + 1]. Resolve (); op . CalcProxy = _calcProxy ; int result = op . Resolve ( firstNumber , secondNumber ); r e p l a c e T o k e n s W i t h R e s u l t ( mathExp , op . Index , result ); } return mathExp [0]. Resolve (); }

9 10 11 12 13 14 15 16 17


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

public abstract class MathToken { protected int _precedence = 0; protected string _token = String . Empty ; protected int _index = -1; public MathToken ( string token ) { _token = token ; } public MathToken ( int precedence ) { _precedence = precedence ; } public int Index {...} public string Token {...} public int Precedence {...} public abstract int Resolve (); } public class MathNumber : MathToken { public MathNumber () : base (0) {} public MathNumber ( string token ) : base ( token ) {} public int IntValue { get { return Int32 . Parse ( _token ); } } public override int Resolve () { return IntValue ; } }

221

Cap tulo 9

49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98

public abstract class MathOperator : MathToken { protected int _firstNumber ; protected int _secondNumber ; protected CalculatorProxy _calcProxy ; public MathOperator ( int precedence ) : base ( precedence ) { } public int FirstNumber { get { return _firstNumber ; } set { _firstNumber = value ; } } public int SecondNumber { get { return _secondNumber ; } set { _secondNumber = value ; } } public CalculatorProxy CalcProxy { get { return _calcProxy ; } set { _calcProxy = value ; } } public override int Resolve () { return Resolve ( _firstNumber , _secondNumber ); } public abstract int Resolve ( int a , int b ); } public interface TokenPrecedence { MathOperator GetMaxPrecedence ( List < MathToken > tokens ); } public class Precedence : TokenPrecedence { public MathOperator GetMaxPrecedence ( List < MathToken > t okens ) { ... return ( MathOperator ) maxPrecedenceToken ; } }

Ntese que CalProxy ha sido movido dentro del operador. Al convertir Matho Token en abstracta nos aseguramos que nadie crea instancias de esa clase. Al n y al cabo queremos trabajar con instancias concretas. No hemos respetado el principio de Liskov del todo. La clase que gestiona precedencia sigue necesitando una conversin expl o cita. Vamos a darle una 222

Cap tulo 9 vuelta de tuerca ms: a

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

9.53: Resolver

public int R e s o l v e S i m p l e E x p r e s s i o n ( string expression ) { if (! MathRegex . IsExpressionValid ( expression )) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( expression ); List < MathToken > mathExp = _lexer . GetTokens ( expression ); while ( mathExp . Count > 1) { MathToken op = _precedence . GetMaxPrecedence ( mathExp ); op . PreviousToken = mathExp [ op . Index - 1]; op . NextToken = mathExp [ op . Index + 1]; int result = op . Resolve (); r e p l a c e T o k e n s W i t h R e s u l t ( mathExp , op . Index , result ); } return mathExp [0]. Resolve (); }


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

public abstract class MathToken { protected int _precedence = 0; protected string _token = String . Empty ; protected int _index = -1; protected MathToken _previousToken , _nextToken ;

public MathToken ( string token ) { _token = token ; } public MathToken ( int precedence ) { _precedence = precedence ; } public MathToken PreviousToken { get { return _previousToken ; } set { _previousToken = value ; } } public MathToken NextToken { get { return _nextToken ; } set { _nextToken = value ; } } public int Index { get { return _index ; } set { _index = value ; } } public string Token

223

Cap tulo 9
{ get { return _token ; } } public int Precedence { get { return _precedence ; } } public abstract int Resolve (); } public abstract class MathOperator : MathToken { protected CalculatorProxy _calcProxy ; public MathOperator ( int precedence ) : base ( precedence ) { _calcProxy = new CalcProxy ( new Validator ( -100 , 100) , new Calculator ()); } public CalculatorProxy CalcProxy { get { return _calcProxy ; } set { _calcProxy = value ; } } public override int Resolve () { return Resolve ( _previousToken . Resolve () , _nextToken . R esolve ()); } public abstract int Resolve ( int a , int b ); }

38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73

Resolver ya no necesita ningn CalculatorProxy. El constructor de Matu hOperator crea el proxy. Llegado este punto no dejar que el constructor a crease la instancia del colaborador porque sino, perdemos la inversin del o control. Es el momento perfecto para introducir un contenedor de inyeccin o de dependencias tipo Castle.Windsor. En ese caso dentro del constructor se har una llamada al contenedor para pedirle una instancia del colaborador a de manera que ste la inyecte. e

1 2 3 4 5 6 7 8

9.54: Supuesto MathOperator

public MathOperator ( int precedence ) : base ( precedence ) { WindsorContainer container = new WindsorContainer ( new XmlInterpreter ()); _calProxy = container . Resolve < CalculatorProxy >( " simpleProxy " ); }

224

Cap tulo 9 Los contenedores de inyeccin de dependencias se conguran a travs de un o e chero o bien mediante cdigo, de tal manera que si existen varias clases o que implementan una interfaz, es decir, varias candidatas a ser inyectadas como dependencias, slo tenemos que modicar la conguracin para reemo o plazar una dependencia por otra, no tenemos que tocar el cdigo fuente. Por o tanto el mtodo Resolve del contenedor buscar qu clase concreta hemos e a e congurado para ser instanciada cuando usamos como parmetro la interfaz a CalculatorProxy. Hemos podido eliminar la conversin de tipos en la funcin que obtiene o o la mxima precedencia y ahora el cdigo tiene mucha mejor pinta. a o Lo ultimo que me gustar refactorizar es la funcin recursiva getEx a o pressions de MathLexer. Tiene demasiados parmetros. Una funcin no a o deber tener ms de uno o dos parmetros, a lo sumo tres. Voy a utilizar un a a a objeto para agrupar datos y funcionalidad:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

9.55: MathLexer

public List < MathExpression > GetExpressions ( string expr ession ) { int openedParenthesis = 0; ExpressionBuilder expBuilder = ExpressionBuilder . Create (); expBuilder . InputText = expression ; getExpressions ( expBuilder , ref openedParenthesis ); if ( openedParenthesis != 0) throw new I n v a l i d O p e r a t i o n E x c e p t i o n ( " Parenthesis do not match " ); _fixer . FixExpressions ( expBuilder . AllExpressions ); expBuilder . AllExpressions . Sort (); return expBuilder . AllExpressions ; } private void getExpressions ( ExpressionBuilder expBuild er , ref int openedParanthesis ) { while ( expBuilder . ThereAreMoreChars ()) { char currentChar = expBuilder . GetCurrentChar (); if ( currentChar == OPEN_SUBEXPRESSION ) { openedParanthesis ++; getExpressions ( expBuilder . P r o c e s s N e w S u b E x p r e s s i o n () , ref openedParanthesis ); } else if ( currentChar == CLOSE_SUBEXPRESSION ) { expBuilder . S u b E x p r e s s i o n E n d F o u n d (); openedParanthesis - -; return ; } else expBuilder . A d d S u b E x p r e s s i o n C h a r (); } expBuilder . S u b E x p r e s s i o n E n d F o u n d ();

225

Cap tulo 9
}

38


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56

public class ExpressionBuilder { private static string _inputText ; private static int _currentIndex = 0; private static List < MathExpression > _allExpressions ; private MathExpression _subExpression ; private ExpressionBuilder () { } public static ExpressionBuilder Create () { ExpressionBuilder builder = new ExpressionBuilder (); builder . AllExpressions = new List < MathExpression >(); builder . CurrentIndex = 0; builder . InputText = String . Empty ; builder . SubExpression = new MathExpression ( String . Empt y ); return builder ; } public ExpressionBuilder P r o c e s s N e w S u b E x p r e s s i o n () { ExpressionBuilder builder = new ExpressionBuilder (); builder . InputText = _inputText ; builder . SubExpression = new MathExpression ( String . Empt y ); updateIndex (); return builder ; } public bool ThereAreMoreChars () { return _currentIndex < MaxLength ; } public void A d d S u b E x p r e s s i o n C h a r () { _subExpression . Expression += _inputText [ _currentIndex ]. ToString (); if ( _subExpression . Order == -1) _subExpression . Order = _currentIndex ; updateIndex (); } public void S u b E x p r e s s i o n E n d F o u n d () { _allExpressions . Add ( _subExpression ); updateIndex (); } public string InputText { get { return _inputText ; } set { _inputText = value ; } } public char GetCurrentChar () {

226

Cap tulo 9
return _inputText [ _currentIndex ]; } public int MaxLength { get { return _inputText . Length ; } } public int CurrentIndex { get { return _currentIndex ; } set { _currentIndex = value ; } } public List < MathExpression > AllExpressions { get { return _allExpressions ; } set { _allExpressions = value ; } } public MathExpression SubExpression { get { return _subExpression ; } set { _subExpression = value ; } } private void updateIndex () { _currentIndex ++; } }

57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87

Refactorizacin hecha, cdigo ms claro y casi todos los tests pasando. o o a El test ParserWorksWithCalcProxy se ha roto ya que el parser no recibe ningn proxy. Es momento de eliminarlo. Nos ha prestado buen servicio hasta u aqu pero ya termin su misin. Tenemos toda la bater de test en verde y o o a la refactorizacin terminada por el momento. o Mientras resolv los ultimos tests me vino a la mente un ejemplo ms a a rebuscado que apunt en la libreta y que ahora expreso de forma ejecutable: e

1 2 3 4 5 6 7 8

9.56: LexerTests

[ Test ] public void G e t C o m p l e x N e s t e d E x p r e s s i o n s () { List < MathExpression > expressions = _lexer . GetExpressions ( " ((2 + 2) + 1) * (3 + 1) " ); failIfOtherSubExpressionThan ( expressions , " 3 + 1 " , " 2 + 2 " , " + " , " * " , " 1 " ); }

1 2

Luz verde sin tocar el SUT. Estupendo!. Ser parser capaz de operar esa a lista de subexpresiones?
[ Test ] public void P r o c e s s C o m p l e x N e s t e d E x p r e s s i o n s ()

227

Cap tulo 9
{ Assert . AreEqual (20 , _parser . ProcessExpression ( " ((2 + 2) + 1) * (3 + 1) " )); }

3 4 5 6 7

Pues no, no puede. Se nos est perdiendo la precedencia de las operaciones a con parntesis anidados. MathParser tiene que colaborar ms con MathLee a xer y tal vez ir resolviendo subexpresiones al tiempo que se van encontrando. Hay que anar ms MathParser. Sin embargo, no lo voy a hacer yo. Lo dejo a abierto como ejercicio para el lector, junto con los dems ejemplos pendientes a de nuestra libreta:
# Aceptacin - "((2 + 2) + 1) * (3 + 1)", devuelve 20 o # Aceptacin - "3 / 2", produce ERROR o # Aceptacin - "2 + -2", devuelve 0 o

Hemos llegado al nal del segundo cap tulo prctico. A lo largo de este a cap tulo hemos resuelto un problema clsico de la teor de compiladores pero a a de manera emergente, haciendo TDD. Ciertamente, para un problema que lleva tantos aos resuelto como es el de los analizadores de cdigo, hubiera n o buscado algn libro de Alfred V. Aho o algn colega suyo y habr utilizado u u a directamente los algoritmos que ya estn ms que inventados. Pero el n de a a este libro es docente; es mostrar con ejemplos como se hace un desarrollo dirigido por ejemplos, valga la redundancia. Como habr observado el cdigo va mutando, va adaptndose a los rea o a quisitos de una manera orgnica, incremental. A menudo en mis cursos de a TDD, cuando llegamos al nal de la implementacin del ejemplo alguien o dice... si hubiera sabido todos esos detalles lo hubiese implementado de otra manera. A esa persona le parece que el diagrama de clases que resulta no se corresponde con el modelado mental de conceptos que tiene en la cabeza y le cuesta admitir que las clases no necesariamente tienen una correspondencia con esos conceptos que manejamos los humanos. Para esa frase hay dos respuestas. La primera es que si conocisemos absolutamente el 100 % de e los detalles de un programa y adems fuesen inamovibles, entonces har a amos programas igual que hacemos edicios: es una situacin utpica. La segunda o o es que aun conociendo todos los ejemplos posibles, nadie tiene capacidad para escribir del tirn el cdigo perfecto que los resuelve todos. Es ms humano o o a y por tanto ms productivo progresar paso a paso. a Tambin me comentan a veces que se modica mucho cdigo con las ree o factorizaciones y que eso da sensacin de prdida de tiempo. Hacer software o e de calidad es perder el tiempo? Realmente no lleva tanto tiempo refactorizar; t picamente es cuestin de minutos. Luego se ganan horas, d y hasta meses o as 228

Cap tulo 9 cuando hay que mantener el software. Al disponer de una bater de tests de a tanta calidad como la que surge de hacer TDD bien, modicar el cdigo es o una actividad con la que se disfruta. Todo lo contrario a la estresante tarea de hacer modicaciones sobre cdigo sin tests. o Lo ideal es refactorizar un poquito a cada vez, respetar con conciencia el ultimo paso del algoritmo TDD que dice que debemos eliminar el cdigo o duplicado y si es posible, mejorar el cdigo tambin. o e El cdigo que tenemos hasta ahora no es perfecto y mi objetivo no es o que lo sea. A pesar de que no tiene mal aspecto es posible hacer mejoras todav Lo que pretendo mostrar es que el diseo emergente genera cdigo a. n o que est preparado para el cambio. Cdigo fcil de mantener, con una cierta a o a calidad. No es decisivo que en la etapa de refactorizacin lleguemos a un o cdigo impoluto, nadie tiene tiempo de entretenerse eternamente a retocar o un bloque de cdigo pero desde luego hay que cumplir unos m o nimos. Para m esos m nimos son los principios S.O.L.I.D, aunque no siempre es necesario que se cumplan completamente cada vez que se refactoriza: si la experiencia nos avisa de que tendremos que hacer cambios en el futuro cercano (en los prximos tests) que nos darn oportunidad de seguir evolucionando el cdigo, o a o podemos esperar a terminar de refactorizar. La artesan sigue jugando un a papel muy importante en esta tcnica de desarrollo. e Debo reconocer que el hecho de que MathRegex sea un conjunto de mtodos tipo static es una mala solucin. Lo mejor ser dividir en clases e o a cuando estn claras las distintas responsabilidades que tiene y as arreglar e el problema de que tantas otras clases dependiesen de la misma entidad. Generalmente los mtodos estticos rompen con la orientacin a objetos y e a o nos hacen volver a la programacin funcional. Se deben de evitar a toda o costa porque se nos hace imposible probar cuestiones como la interaccin o entre objetos, puesto que un conjunto de mtodos estticos no constituyen e a un objeto. Sin embargo en el punto en que estamos no he visto con claridad cmo refactorizar y he decidido preservar esa solucin, aunque en la libreta o o me apunto que debo cambiarla tan pronto como sepa hacerlo. En el prximo cap o tulo afrontaremos ejemplos que nos exigirn dobles de a prueba y terminaremos integrando nuestro cdigo con el resto del sistema pao ra visitar todos los escenarios que se presentan en el desarrollo de aplicaciones empresariales.

229

Cap tulo 9

230

Captulo

10

Fin del proyecto - Test de Integracin o


Hasta ahora nos hemos movido por la lgica de negocio mediante ejemo plos en forma de test unitario. Hemos respetado las propiedades de rpidez, a inocuidad, claridad y atomicidad, aunque la granularidad de algunos tests no fuese m nima en algn caso. En todo momento hemos trabajado con variau bles en memoria, no ha habido acceso al sistema de cheros ni al sistema de gestin de base de datos. Hemos trabajado en el escenario ideal para o comprender de qu trata el diseo emergente. Si ha seguido los cap e n tulos anteriores escribiendo cdigo frente a la pantalla, a la par que le el libro, o a ya habr comprendido de qu va TDD. a e La utilidad mas comn de una aplicacin es la manipulacin de datos, u o o algo que todav no sabemos hacer de manera emergente. Por ello vamos a a dar un giro a la implementacin de nuestro problema tratando criterios de o aceptacin que requieren que nuestros objetos se integren con otros objetos o externos, que s tienen acceso a una base de datos. Atendiendo a los requisitos que se exponen al comienzo del cap tulo 8, no necesitamos acceder a ningn sistema de cheros para desarrollar nuestra u solucin sino slo a una base de datos. No obstante, para mostrar en detalle o o como podemos hacer integracin a la TDD, imaginemos que el usuario nos o ha pedido que la lista de alumnos del juego se guarde en un chero en disco. Ms adelante trabajaremos con la base de datos. Nuestra libreta contiene lo a siguiente:

231

Cap tulo 10

# Aceptacin: o Ruta y nombre del fichero de datos son especificados por el usuario Fichero datos = C:datos.txt Fichero datos = /home/jmb/datos.txt # Aceptacin: Un usuario tiene nick y clave de acceso: o nick: adri, clave: pantera # Aceptacin: Crear, modificar y borrar usuarios o # Aceptacin: La clave de acceso est encriptada o a clave plana: pantera, hash MD5: 2d58b0ac72f929ca9ad3238ade9eab69 # Aceptacin: Si los usuarios que existen son Fran y Yeray o El listado de usuarios del sistema devuelve [(0, Fran), (1, Yeray)] Si usuarios del sistema son Esteban y Eladio, fichero contendr: a # Begin 0:Esteban:2d58b0ac72f929ca9ad3238ade9eab69 1:Eladio:58e53d1324eef6265fdb97b08ed9aadf # End Si a~adimos el usuario Alberto al fichero anterior: n # Begin 0:Esteban:2d58b0ac72f929ca9ad3238ade9eab69 1:Eladio:58e53d1324eef6265fdb97b08ed9aadf 2:Alberto:5ad4a96c5dc0eae3d613e507f3c9ab01 # End Las expresiones introducidas por el usuario 1 se guardan junto con su resultado "2 + 2", 4, Usuario(1, Alberto) "2 + 1", 3, Usuario(1, Alberto) # Aceptacin: o Obtener todas las expresiones introducidas por el usuario Alberto ([Usuario(1, Alberto), "2 + 2", 4], [Usuario(1, Alberto), "2 + 1", 3])

El criterio de aceptacin de que los usuarios se pueden modicar y borrar o no est bien escrito, no llega a haber ejemplos. Sabemos que al crearlo sus a atributos se guardan en el chero de datos porque se tiene que listar el nick y el identicador de usuario pero de momento, el cliente no nos ha contado cmo se modican y se borran usuarios. Lo dejaremos as para empezar o cuanto antes con la implementacin. Como podr observar, en la libreta o a vuelve a haber tests de aceptacin y tests de desarrollo. Los tests de desarrollo o son una propuesta que responde a cmo queremos implementar los tests de o aceptacin, los cuales no llegan a adentrarse en cmo sino en qu. o o e Empieza a oler a integracin porque la denicin de los ejemplos se ayuda o o 232

Cap tulo 10

10.1. La frontera entre tests unitarios y tests de integracin o

de muchos datos de contexto (xtures), en este caso de un chero y tambin e de una base de datos. Por dnde empezamos a trabajar? Lo primero es o delimitar la frontera entre nuestros objetos y los objetos de terceros.

10.1.

La frontera entre tests unitarios y tests de integracin o

Como hemos visto, trabajar en el mbito de los tests unitarios es rpido a a y productivo. Cuanta ms lgica de negocio podamos mantener controlada a o bajo nuestra bater de tests unitarios, ms fcil ser detectar y corregir a a a a problemas, adems de ampliar funcionalidad. Sin embargo, llega un punto a en que inevitablemente tenemos que romper las reglas de los tests unitarios y modicar el estado del sistema mediante una escritura en disco. La mejor forma de hacerlo es estableciendo una frontera a modo de contrato entre las dos partes: la que se limita a trabajar con datos en memoria y la que modica el estado del sistema. Grcamente pienso en ello como la parte a a la izquierda y la parte a la derecha.

Tal frontera se delimita mediante interfaces en lenguajes como Java y C#. Una interfaz es un contrato al n y al cabo. En el caso de lenguajes interpretados no se necesitan interfaces, simplemente habr clases. a Para que la parte izquierda contenga el mayor volumen de lgica de negoo cio posible, tenemos que pensar que la parte derecha ya est implementada. a Tenemos que disear la frontera y pensar que ya hay alguna clase que implen menta ese contrato y que es capaz de recibir y enviar datos al sistema. Es decir, nos mantenemos en el mbito de los tests unitarios considerando que a las clases que modican el estado del sistema ya existen, utilizando mocks y stubs para hablar con ellas, sin que realmente toquen el sistema. 233

10.1. La frontera entre tests unitarios y tests de integracin o

Cap tulo 10

Una vez llegamos ah vamos a por los tests de integracin, que probarn , o a que las clases efectivamente se integran bien con el sistema externo, escribiendo y leyendo datos. Para ejecutar los tests de integracin necesitaremos un o entorno de preproduccin que se pueda montar y desmontar antes y despus o e de la ejecucin de la bater de tests de integracin, ya que dichos tests o a o podr dejar el sistema en un estado inconsistente. an Aunque la plataforma sobre la que trabajemos nos ofrezca ya interfaces que nos puedan parecer vlidas para establecer la frontera, se recomienda dea nir nuestra propia interfaz independiente. Luego, la clase que implemente la interfaz frontera podr hacer uso de todas las herramientas que la plataforma a le ofrezca. Vamos a verlo con ejemplos mediante la implementacin de nuestra soluo cin. Hay un test de aceptacin que dice que crearemos el chero de datos de o o usuario en la ruta especicada por alguien. En los tests de desarrollo se aprecia que el chero de usuarios contiene una l nea por cada usuario, con unos delimitadores de comienzo y n de chero y unos delimitadores de campos para los atributos de los usuarios. Por un lado veo la necesidad de crear un chero y por otra la de leerlo e interpretarlo. Empiezo a resolver el problema por la parte izquierda:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

[ TestFixture ] public class UserManagementTests { [ Test ] public void ConfigUsersFile () { string filePath = " / home / carlosble / data . txt " ; FileHandler handlerMock = MockRepository . GenerateMock < FileHandler >(); handlerMock . Expect ( x = > x . CreateFile ( filePath )). Return ( new UserFile ()); UsersStorageManager manager = new UsersStorageManager ( handlerMock ); manager . SetUsersFile ( filePath ); handlerMock . V e r i f y A l l E x p e c t a t i o n s (); } }

Mi planteamiento ha sido el siguiente: Quiero una clase para gestin de datos o de usuarios que tenga un mtodo para denir cul es el chero donde se e a guardarn los datos. Existir una interfaz FileHandler con un mtodo para a a e crear el chero, que devolver un objeto de tipo UserFile. Estoy obligando a a que mi SUT colabore con el manejador de cheros. Nada ms terminar a de escribir el test me he preguntado qu pasar si hay algn problema con e a u la creacin del chero (ej: insuciencia de permisos) y lo he apuntado en la o 234

Cap tulo 10

10.1. La frontera entre tests unitarios y tests de integracin o

libreta. El subconjunto de prximas tareas de la libreta es el siguiente: o


Crear un fichero en la ruta especificada Resolver casos en que no se puede crear Permisos insuficientes Ruta no existe Leer un fichero de texto lnea a lnea

Voy a ocuparme de que el test pase lo antes posible, como siempre:

1 2 3 4 5 6 7 8 9 10 11 12 13 14

public class UsersStorageManager { FileHandler _handler ; public UsersStorageManager ( FileHandler handler ) { _handler = handler ; } public void SetUsersFile ( string path ) { _handler . CreateFile ( path ); } }

Luz verde. Acabamos de denir una frontera mediante la interfaz FileHandler. Ahora podemos ir desarrollando todos los casos que nos vayan haciendo falta, como por ejemplo el de que no haya permisos para crear el chero. Simularemos tal caso dicindole a Rhino.Mocks que lance una excepcin por e o falta de permisos y nos ocuparemos de que nuestra clase gestora responda adecuadamente. El juego consiste en utilizar los dobles1 para que produzcan un determinado comportamiento y nosotros podamos programar nuestra lgica de negocio acorde a ese comportamiento. o El benecio que obtenemos es doblemente bueno. Por un lado estamos trabajando con tests unitarios y por otro, resulta que ante posibles defectos en las clases que implementen FileHandler, nuestro test no se romper. El a SUT est bien aislado. a Cmo implementamos los tests de integracin para la clase que cumple o o con el contrato FileHandler? Para esta funcionalidad me parece bien seguir usando NUnit. Lo primero que se me viene a la cabeza antes de empezar es que tendremos que cuidarnos de saber si estamos en un entorno MS Windows, POSIX o algn otro para evitar problemas con las rutas. Lo apunto en u
1

Mocks, Stubs, etc

235

10.1. La frontera entre tests unitarios y tests de integracin o

Cap tulo 10

la libreta. Voy a crear un nuevo proyecto dentro de la solucin para los tests o de integracin (otra DLL) de modo que no se ejecuten cada vez que lance o los tests unitarios. La libreta dice:
Crear un fichero en la ruta especificada Resolver casos en que no se puede crear Permisos insuficientes Ruta no existe Leer un fichero de texto lnea a lnea Integracin: o Crear el fichero en Windows Crear el fichero en Ubuntu Crear el fichero en MacOS

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

Voy con el primero de ellos:

namespace IntegrationTests { [ TestFixture ] public class FileHandlerTests { [ Test ] public void C r e a t e F i l e W i t h W i n d o w s P a t h () { string path = @ " c :\ data . txt " ; UserFileHandler handler = new UserFileHandler (); handler . CreateFile ( path ); if (! File . Exists ( path )) { Assert . Fail ( " File was not created " ); } } } }

He creado una clase UserFileHandler que implementa la interfaz FileHandler. Llamo a su mtodo de crear chero y luego compruebo que e el chero existe mediante la API que me ofrece .Net. Hay que cuidarse de no hacer la comprobacin mediante el mismo SUT cuando se trata de integrao cin. Es decir, supongamos que el SUT tiene un metodo tipo IsFileOpened. o Ser absurdo invocarlo para hacer la comprobacin de que el chero se ha a o creado ya que eso no garantiza que efectivamente el sistema de cheros se ha modicado. Por este motivo a veces la etapa de armacin de un test o de integracin se puede llegar a complicar bastante. Vamos a hacer que el o primer test pase: 236

Cap tulo 10

1 2 3 4 5 6 7 8

10.1. La frontera entre tests unitarios y tests de integracin o

public class UserFileHandler : FileHandler { public DataFile CreateFile ( string path ) { File . Create ( path ); return null ; } }

Luz verde. Me llama la atencin que no estamos haciendo nada con el cheo ro. No estamos creando ningn DataFile. Voy a aumentar un poco ms la u a libreta:
Crear un fichero en la ruta especificada El que implemente DataFile contiene un FileStream con acceso al fichero creado Resolver casos en que no se puede crear Permisos insuficientes Ruta no existe Leer un fichero de texto lnea a lnea Integracin: o Crear el fichero en Windows Crear el fichero en Ubuntu Crear el fichero en MacOS

El mtodo File.Create devuelve un FileStream. Todo eso es parte de e la API de .Net. FileStream tiene un campo Handle que es una referencia al chero en disco. En base a esto voy a modicar mi test de integracin: o

1 2 3 4 5 6 7 8 9 10 11 12 13 14

[ Test ] public void C r e a t e F i l e W i t h W i n d o w s P a t h () { string path = @ " c :\ data . txt " ; UserFileHandler handler = new UserFileHandler (); DataFile dataFile = handler . CreateFile ( path ); if (! File . Exists ( path )) { Assert . Fail ( " File was not created " ); } Assert . IsNotNull ( dataFile . Stream ); }

Vamos a buscar la luz verde:

1 2 3

public class UserFileHandler : FileHandler < UserFile > { public UserFile CreateFile ( string path )

237

10.1. La frontera entre tests unitarios y tests de integracin o

Cap tulo 10

4 5 6 7 8 9 10

{ FileStream stream = File . Create ( path ); UserFile userFile = new UserFile (); userFile . Stream = stream ; return userFile ; } }

El test ya pasa. He decidido introducir genricos en el diseo lo cual me ha e n llevado a hacer algunas modicaciones:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

public interface FileHandler <T > where T : DataFile { T CreateFile ( string path ); } public interface DataFile { FileStream Stream { get ; set ; } } public class UserFile : DataFile { FileStream _stream = null ; public FileStream Stream { get { return _stream ; } set { _stream = value ; } } }


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

namespace UnitTests { [ TestFixture ] public class UserManagementTests { [ Test ] public void ConfigUsersFile () { string filePath = " / home / carlosble / data . txt " ; FileHandler < UserFile > handlerMock = MockRepository . GenerateMock < FileHandler < UserFile > >(); handlerMock . Expect ( x = > x . CreateFile ( filePath )). Return ( new UserFile ()); UsersStorageManager manager = new UsersStorageManager ( handlerMock ); manager . SetUsersFile ( filePath ); handlerMock . V e r i f y A l l E x p e c t a t i o n s (); } } }

namespace IntegrationTests

238

Cap tulo 10
{

10.1. La frontera entre tests unitarios y tests de integracin o

2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

[ TestFixture ] public class FileHandlerTests { [ Test ] public void C r e a t e F i l e W i t h W i n d o w s P a t h () { string path = @ " c :\ data . txt " ; UserFileHandler handler = new UserFileHandler (); DataFile dataFile = handler . CreateFile ( path ); if (! File . Exists ( path )) { Assert . Fail ( " File was not created " ); } Assert . IsNotNull ( dataFile . Stream ); } } }

1 2 3 4 5 6 7 8 9 10

Los genricos funcionan prcticamente igual en C# que en Java, aunque su e a sintaxis me parece ms clara en C#. No es importante que sea experto en a genricos si comprende el cdigo que he escrito. Si quiere leer ms sobre e o a genricos en .Net, hace tiempo escrib un art e culo en espaol sobre ello2 . n Bien, volvemos a tener los dos tests pasando, el unitario y el de integracin. Si nos paramos a pensarlo bien, quizs deber o a amos de separar los tests de integracin en distintas DLL. Una DLL deber contener los tests o a que son para sistemas MS Windows, otra los que son para Linux y as con cada sistema. De eso trata la integracin. Me plantear utilizar mquinas o a a virtuales para lanzar cada bater de tests. Se lo hab planteado? a a Afortunadamente .Net se basa en un estndar que implementa tambin el a e framework Mono3 , por lo que no tenemos que preocuparnos de que el sistema se comporte distinto en Windows que en Linux. Tan solo tenemos que mirar la API de File para ver, qu posibles excepciones se pueden producir y escribir e tests que se ejecuten con el mismo resultado en cualquier plataforma. Vamos a arreglar nuestro test de integracin para que funcione tambin en Linux y o e MacOS.
namespace IntegrationTests { [ TestFixture ] public class FileHandlerTests { private string getPlatformPath () { System . OperatingSystem osInfo = System . Environment . OSVersion ; string path = String . Empty ;
2 3

http://www.carlosble.com/?p=257 http://www.mono-project.com

239

10.1. La frontera entre tests unitarios y tests de integracin o

Cap tulo 10

11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

switch ( osInfo . Platform ) { case System . PlatformID . Unix : { path = " / tmp / data . txt " ; break ; } case System . PlatformID . MacOSX : { path = " / tmp / data . txt " ; break ; } default : { path = @ " C :\ data . txt " ; break ; } } return path ; } [ Test ] public void C r e a t e F i l e M u l t i P l a t f o r m () { string path = getPlatformPath (); UserFileHandler handler = new UserFileHandler (); DataFile dataFile = handler . CreateFile ( path ); if (! File . Exists ( path )) { Assert . Fail ( " File was not created " ); } Assert . IsNotNull ( dataFile . Stream ); } } }

Perfecto, ya tenemos todas las plataformas que nos interesan por ahora cubiertas. Ya no importa que algunos de los desarrolladores del equipo trabajen con Ubuntu y otros con Windows 7. Todo esto es igual de aplicable a Java. Por dnde vamos? o
Crear un fichero en la ruta especificada Resolver casos en que no se puede crear Permisos insuficientes Ruta no existe Leer un fichero de texto lnea a lnea

Avancemos un poco ms en los casos de posibles excepciones creando a el chero. Primero siempre preero abordar el test unitario antes que el de 240

Cap tulo 10 integracin: o

1 2 3 4 5 6 7 8 9 10 11 12 13 14

10.1. La frontera entre tests unitarios y tests de integracin o

10.1: UnitTests

[ Test ] [ ExpectedException ( typeof ( D i r e c t o r y N o t F o u n d E x c e p t i o n ))] public void T r y C r e a t e F i l e W h e n D i r e c t o r y N o t F o u n d () { FileHandler < UserFile > handlerMock = MockRepository . GenerateStub < FileHandler < UserFile > >(); handlerMock . Expect ( x = > x . CreateFile ( " " )). Throw ( new D i r e c t o r y N o t F o u n d E x c e p t i o n ()); UsersStorageManager manager = new UsersStorageManager ( handlerMock ); manager . SetUsersFile ( " " ); }

Le he dicho a Rhino.Mocks que lance una excepcin cuando se invoque a o CreateFile. El comportamiento que estoy expresando es que UserStorageManager no va a capturar la excepcin sino a dejarla pasar. Ntese o o que estoy utilizando un stub y no un mock, puesto que la vericacin de la o llamada al colaborador ya est hecha en el test anterior. No olvide que cada a test se centra en una unica caracter stica del SUT. Vamos a la parte de integracin: o

1 2 3 4 5 6 7 8 9 10

10.2: IntegrationTests

[ Test ] [ ExpectedException ( typeof ( D i r e c t o r y N o t F o u n d E x c e p t i o n ))] public void C r e a t e F i l e D i r e c t o r y N o t F o u n d () { string path = new NotFoundPath (). GetPlatformPath (); UserFileHandler handler = new UserFileHandler (); DataFile dataFile = handler . CreateFile ( path ); }

En realidad he copiado el test despes de su refactorizacin. Estamos en u o verde sin tocar el SUT. El cdigo tras la refactorizacin que he aplicado, es o o el siguiente:

1 2 3 4 5 6 7 8 9

10.3: IntegrationTests

public abstract class MultiPlatform { public abstract string GetPOSIXpath (); public abstract string GetWindowsPath (); public string GetPlatformPath () { System . OperatingSystem osInfo =

241

10.1. La frontera entre tests unitarios y tests de integracin o

Cap tulo 10

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68

System . Environment . OSVersion ; string path = String . Empty ; switch ( osInfo . Platform ) { case System . PlatformID . Unix : { path = GetPOSIXpath (); break ; } case System . PlatformID . MacOSX : { path = GetPOSIXpath (); break ; } default : { path = GetWindowsPath (); break ; } } return path ; } } public class NotFoundPath : MultiPlatform { public override string GetPOSIXpath () { return " / asdflwiejawseras / data . txt " ; } public override string GetWindowsPath () { return @ " C :\ a s d f a l s d f k w j e r a s d f a s \ data . txt " ; } } public class EasyPath : MultiPlatform { public override string GetPOSIXpath () { return " / tmp / data . txt " ; } public override string GetWindowsPath () { return @ " C :\ data . txt " ; } } [ TestFixture ] public class FileHandlerTests { [ Test ] public void C r e a t e F i l e M u l t i P l a t f o r m () { string path = new EasyPath (). GetPlatformPath ();

242

Cap tulo 10
UserFileHandler handler = new UserFileHandler ();

10.2. Diseo emergente con un ORM n

69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90

DataFile dataFile = handler . CreateFile ( path ); if (! File . Exists ( path )) { Assert . Fail ( " File was not created " ); } Assert . IsNotNull ( dataFile . Stream ); } [ Test ] [ ExpectedException ( typeof ( D i r e c t o r y N o t F o u n d E x c e p t i o n ))] public void C r e a t e F i l e D i r e c t o r y N o t F o u n d () { string path = new NotFoundPath (). GetPlatformPath (); UserFileHandler handler = new UserFileHandler (); DataFile dataFile = handler . CreateFile ( path ); } }

Para las dems excepciones que lance el sistema, tales como Unauthoa rizedAccessException o ArgumentNullException, no voy a escribir ms a tests puesto que s que el comportamiento de mis clases es el mismo, al no e capturar las excepciones. Si decido ms adelante utilizar UserStorageMaa nager desde otra clase y capturar las posibles excepciones para construir un mensaje amigable para el usuario, entonces escribir los tests correspondiene tes en su momento y lugar. Nos queda trabajar en la lectura del chero l nea a l nea. Qu es lo e primero que haremos? Escribir un test unitario que utilice un colaborador que cumple una interfaz (contrato), la cual contiene las llamadas a los mtodos e que nos devuelven datos. Al trabajar primero en el test unitario nos cuidamos mucho de que la interfaz que denimos como contrato para hablar con el exterior, nos resulte lo ms cmoda posible. Es todo lo contrario que en el a o desarrollo clsico, cuando diseamos la interfaz y luego al usarla tenemos a n que hacer malabares para adaptarnos a ella. Esta parte queda como ejercicio pendiente para el lector.

10.2.

Dise o emergente con un ORM n

Vamos a irnos directamente al acceso a base de datos. Para ello asumiremos que estamos trabajando con algn ORM4 como Hibernate, ActiveReu cord, Django ORM o similar.
4

http://es.wikipedia.org/wiki/Mapeo objeto-relacional

243

10.2. Diseo emergente con un ORM n

Cap tulo 10

Las dos formas ms extendidas de manejar datos mediante un ORM son a las siguientes: Un DAO con mtodos de persistencia + modelos anmicos e e Un modelo con atributos para datos y mtodos de persistencia e Ambas tienen sus ventajas e inconvenientes. La primera opcin se como pone de un DAO5 que tiene mtodos como save, create y delete, cuyos e parmetros son modelos. Estos modelos son clases sin mtodos funcionales a e pero con atributos designados para alojar datos, tales como Name, Address, Phone, etc. Martin Fowler les llam modelos anmicos por la ausencia de o e lgica de negocio en ellos. o La segunda opcin es la mezcla de los dos objetos anteriores en uno solo o que unica lgica de persistencia y campos de datos. o Por revisar los distintos casos que se nos pueden plantear a la hora de aplicar TDD, vamos a abandonar el ejemplo que ven amos desarrollando desde el cap tulo 8 y a enfocarnos sobre ejemplos puntuales. Siento decirle que no vamos a terminar de implementar todo lo que el cliente nos hab a pedido en el cap tulo 8. Toda esa larga historia que el cliente nos cont, era o slo para que el lector no pensase que nuestra aplicacin de ejemplo no es o o sucientemente empresarial. En Hibernate y NHibernate el patrn que se aplica viene siendo el del o DAO ms los modelos. El DAO es un objeto que cumple la interfaz Session a en el caso de Hibernate e ISession en el caso de NHibernate. Con JPA6 los modelos tienen anotaciones para expresar relaciones entre ellos o tipos de datos. Exactamente lo mismo que ocurre en .Net con Castle.ActiveRecord 7 . En lo sucesivo hablar de Hibernate para referirme a la versin Java y a la e o versin .Net indistintamente. o Si nos casamos con Hibernate como ORM, entonces la frontera que delimita nuestros tests unitarios y nuestros tests de integracin puede ser la o propia interfaz Session. Ya sabemos que mediante un framework de mocks podemos simular que este DAO invoca a sus mtodos de persistencia, con lo e cual utilizamos modelos en los tests unitarios sin ningn problema. Veamos u un pseudo-cdigo de un test unitario: o

1 2 3 4 5

[ Test ] public void U s e r M a n a g e r C r e a t e s N e w U s e r () { UserModel user = new UserModel (); ISession sessionMock =
5 6

Data Access Object Java Persistence API 7 Proyecto CastleProject

244

Cap tulo 10

10.2. Diseo emergente con un ORM n

6 7 8 9 10 11 12 13 14

MockRepository . GenerateMock < ISession >(); sessionMock . Expect ( x = > x . Save ( user )). IgnoreArguments (); UserManager manager = new UserManager ( sessionMock ); manager . SaveNewUser ( Angel , clave1234 ); sessionMock . V e r i f y A l l E x p e c t a t i o n s (); }

Estamos diciendo que nuestro UserManager es capaz de recibir un nick y una clave y guardar un registro en la base de datos. He especicado que se ignoren los argumentos porque unicamente me preocupa que se salve el registro, nada ms. Para continuar ahora probando la parte de integracin, podr tomar a o a el modelo UserModel, grabarlo en base de datos y a continuacin pedirle al o ORM que lo lea desde la base de datos para armar que se guard bien. o Si se ja bien ver que el formato es idntico al del acceso a cheros del a e principio del cap tulo. Si lo comprende le valdr para todo tipo de tests que a se aproximan a la frontera de nuestras clases con el sistema externo. Cmo abordamos los tests unitarios con ORMs que unican modelo o y DAO? Fcil. Nos inventamos el DAO nosotros mismos a travs de una a e interfaz que hace de frontera. En nuestra lgica de negocio, en lugar de llamar o directamente a los mtodos de persistencia del modelo, invocamos al DAO e pasando el modelo. Como el modelo incluye toda la lgica de persistencia, o el cdigo del DAO es super sencillo, se limita a invocar al modelo. Pseudoo cdigo: o

1 2 3 4 5 6 7

public class DAO () { public void Save ( Model model ) { model . Save (); } }

Habr casos en los que quizs podamos trabajar directamente con el modelo a a si hablamos de operaciones que no acceden a la base de datos. No se trata de adoptar el patrn del DAO para todos los casos de manera general. Donde o sea que quiera establecer un l mite entre lgica de negocio diseada con tests o n unitarios, lo utilizar y donde no, aprovechar la potencia de un modelo que e e incorpora lgica de persistencia. o Para los tests de integracin conviene trabajar con una base de datos o diferente a la de produccin porque si su ejecucin falla, la base de datos o o puede quedar inconsistente. Muchos frameworks ofrecen la posibilidad de crear una base de datos al vuelo cuando lanzamos los tests de integracin y o destruirla al terminar. Adems entre un test y otro se encargan de vaciar las a tablas para obligar a que los tests sean independientes entre s . 245

10.2. Diseo emergente con un ORM n

Cap tulo 10

Hay que tener cuidado de no abusar de estos frameworks, evitando que se creen y se destruyan bases de datos cuando tan slo estamos escribiendo tests o unitarios. Esto ocurre con el framework de tests de Django8 por ejemplo, que siempre crea la base de datos al vuelo incluso aunque no accedamos a ella. Mi recomendacin en este caso es utilizar el mecanismo de tests de Django o para los tests de integracin y utilizar pyunit para los tests unitarios. La razn o o es que los tests unitarios deben ser rpidos y crear la base de datos consume a ms de 5 segundos. Demasiado tiempo. a

10.2.1.

Dise ando relaciones entre modelos n

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

T picamente cuando llega el momento de disear modelos y empezar n a trazar relaciones entre unos y otros9 , me gusta tambin hacerlo con un e test primero. Supongamos que mi modelo User tiene una relacin de uno o a muchos con mi modelo Group, es decir, que un grupo tiene uno o ms a usuarios. Si mi objetivo es trabajar con modelos, entonces me ahorro la parte de tests unitarios y salto directamente a tests de integracin. Probar una o relacin tan sencilla como esta mediante un test unitario no vale la pena, o por lo menos en este momento. En lugar de escribir ambas clases a priori, diseo su API desde un test de integracin para asegurarme que es as como n o los quiero utilizar:
[ Test ] public void U s e r B e l o n g s I n M a n y G r o u p s () { Group group = Group (); group . Name = " Curris " ; group . Save (); UserModel user = new UserModel (); user . name = " curri1 " ; user . Group = group ; user . Save (); UserModel user = new UserModel (); user . name = " curri2 " ; user . Group = group ; users . Save (); Group reloaded = Group . FindAll (); Assert . AreEqual (2 , reloaded . Count ); ... }

Adems de que el test me ayuda a encontrar la API que busco, me puede a servir para probar que estoy trabajando bien con el framework ORM en caso de que est utilizando mecanismos complejos tipo cach. e e
8 9

Desarrollo web con Python (http://www.djangoproject.com/) OneToMany, ManyToMany

246

Cap tulo 10

10.3. La unicacin de las piezas del sistema o

Es importante tener cuidado de no hacer tests para probar que el framework que estamos usando funciona. Es decir, si escojo Hibernate no me pongo a escribir tests para comprobar que funciona. Igual que tampoco pruebo que .Net funciona. Cuando nos iniciamos en el desarrollo de tests de integracin o es comn caer en la trampa de lanzarse a escribir tests para todas las heu rramientas que estamos utilizando. Tenemos que partir de la base de que el cdigo de terceros, que utilizamos es robusto. Hay que conar en ello y o asumir que funciona tal cual diga su documentacin. La prxima vez que o o escoja una herramienta de terceros asegrese de que dispone de una buena u bater de tests que al menos cumple un m a nimo de calidad. El diseo emergente para los modelos no es algo que haga siempre. Si n se trata de unos modelos que ya he diseado mil veces como por ejemplo n usuarios y grupos, no hago un test primero. Sin embargo en muchas otras ocasiones, s que me ayuda a claricar lo que necesito. Hago TDD cuando por ejemplo tengo que leer datos de un chero de entrada y guardarlos en base de datos a travs de modelos o simplemente cuando no entiendo las e relaciones entre entidades sin ver (y escribir) un ejemplo primero.

10.3.

La unicacin de las piezas del sistema o

Ya casi tenemos todas las piezas del sistema diseadas pero en lo que a n tests se reere todav nos queda un rea que cubrir. Son los tests de sistema. a a Es decir, tests de integracin que van de extremo a extremo de la aplicacin. o o En el UserManager de antes, tenemos tests que cubren su lgica de negocio o y tests que cubren la integracin de nuestra clase que accede a datos con el o sistema exterior. Sin embargo no hay ningn bloque de cdigo que se ocupe u o de inyectar el objeto ISession en UserManager para operar en produccin. o Para esto escribimos un nuevo test de integracin que cubre todo el rea. o a Para este ejemplo concreto escribir el test antes que el SUT al estilo TDD a porque todav es un conjunto de objetos reducido. Sin embargo, cuando a el rea a cubrir es mayor, puede ser un antipatrn seguir haciendo TDD. A a o veces conviene escribir tests a posteriori. Encuentro que es as cuando se trata de integrar la interfaz de usuario con el resto del sistema. Intento por todos los medios dejar la interfaz grca para el nal del todo, cuando lo dems a a est terminado y slo hay que colocar la carcasa. Es una buena manera de a o evitar que el diseo grco contamine la lgica de negocio y al mismo tiempo n a o produce cdigo menos acoplado y ms fcil de mantener. En general, TDD o a a para integrar interfaces de usuario no es una prctica comn. a u Los tests de sistema para aplicaciones web se pueden llevar a cabo con

247

10.3. La unicacin de las piezas del sistema o

Cap tulo 10

herramientas como Selenium10 . Soy de la opinin de que no se debe abusar o de los tests que atacan a la interfaz de usuario porque son extremdamente a frgiles. Ante cualquier cambio se rompen y cuesta mucho mantenerlos. Si a nuestra bater de tests unitarios contempla la mayor parte de la lgica de a o negocio y nuestros tests de integracin de bajo nivel (como los que hemos o hecho en este cap tulo) contienen las partes clave, me limitar a escribir unos a cuantos tests de sistema para comprobar, grosso modo, que todas las piezas encajan pero no intentar hacer tests para toda la funcionalidad del sistema a atacando a la interfaz de usuario. Cuando queremos refactorizar un cdigo legado (que no tiene tests auo tomticos) entonces es buena idea blindar la aplicacin con tests de sistema a o antes de tocar cdigo, porque as seremos avisados de los destrozos que hao gamos. Por desgracia escribir todos esos tests es un trabajo de usar y tirar. Una vez refactorizado el cdigo y creados sus correspondientes tests unitarios o y de integracin, habr cambios en los niveles superiores que rompern los o a a tests de sistema y dejar de valer la pena mantenerlos. Cuando sea posible a ser mejor opcin escribir tests unitarios si el cdigo lo permite. Michael a o o Feathers lo describe en su libro[6].

10

http://seleniumhq.org/

248

Captulo

11

La solucin en versin Python o o


Al igual que en los cap tulos anteriores, vamos a escribir la Supercalculadora (al menos una versin reducida) siguiendo el desarrollo orientado o por pruebas pero, esta vez, en Python. Para la escritura del cdigo vamos a o seguir el estndar de estilo PEP8 1 , una gu de estilo muy conocida en el a a mundillo Python. Adems, usaremos el clsico y estndar pyunit para las a a a pruebas unitarias aunque tambin se podr utilizar el sencillo doctests o e a el potente nosetests2 (el cual recomiendo) que es un poderoso framework para test unitarios que nos har la vida mucho ms fcil. a a a El objetivo de este cap tulo no es ms que mostrar al lector que hacer a TDD con un lenguaje interpretado como Python es igual que con un lenguaje compilado o h brido. Otro punto a destacar es la eleccin de los tests unitarios o y cmo sto puede cambiar el diseo de la solucin. Esto no es ms que la o e n o a visualizacin del llamado diseo emergente el cual sale de los tests, por o n tanto, si la eleccin de pruebas es distinta en distintos puntos (o el orden) el o diseo puede variar (aunque el resultado debe permanecer igual). n Una aclaracin ms antes de empezar. Es posible que le sorprenda despus o a e de leer los cap tulos anteriores que el cdigo est escrito en espaol en lugar o e n de ingls. Despus de varias discusiones sobre el tema, el bueno de Carlos ha e e aceptado a que as fuera. Al contrario que l, creo que el cdigo no tiene por e o qu estar escrito en ingls. Estar escrito en esa lengua por todo aquel que e e a se sienta cmodo escribiendo en ingls pero me parece contraproducente el o e hacerlo por deber cuando se tiene otra lengua con la cual se sienta uno ms a gusto. Por supuesto, huelga decir, que el contexto inuye y que si a estamos en una organizacin internacional, en un trabajo donde hay otros o desarrolladores que slo hablan ingls, lo lgico (y debido puesto que el o e o
1 2

http://www.python.org/dev/peps/pep-0008/ http://code.google.com/p/python-nose/

249

Cap tulo 11 idioma internacional de facto) es hacerlo en ingls ya que el cdigo debe e o ser entendido por todo el mundo (por cierto, que sirva como nota que en este mundillo, todo el mundo deber ser capaz de manejarse sin problema a en ingls). En este caso, ya que el libro est enfocado a lectores hispanos y e a para evitar que alguien pueda no entender correctamente el cdigo en ingls, o e hemos decidido dar una nota de color en este cap tulo escribiendo el cdigo o en espaol. n Sin ms dilacin, vamos a ponernos a ello resumiendo en una frase qu es a o e lo que el cliente nos ped (para obtener la informacin completa, ver Cap a o tulo 8 en la pgina 121): a
 

Una calculadora de aritmtica bsica de nmeros enteros e a u

Para seguir la misma losof que el cap a tulos anteriores, vamos a usar los mismos tests de aceptacin que en el caso de la Supercalculadora en C# o aunque, como veremos ms tarde, quiz tomemos decisiones distintas a lo a a hora de elegir los test unitarios o el orden en que los implementaremos. Para no duplicar, no nos vamos a meter a analizar los tests de aceptacin en este o cap tulo. En caso de que quiera revisar los motivos de por qu estos tests e y no otros, por favor, vuelva a releer los cap tulos anteriores. Para refrescar la memoria, a continuacin se muestran los tests de aceptacin elegidos al o o inicio del proyecto:
"2 + 2", devuelve 4 "5 + 4 * 2 / 2", devuelve 9 "3 / 2", produce el mensaje ERROR "* * 4 - 2": produce el mensaje ERROR "* 4 5 - 2": produce el mensaje ERROR "* 4 5 - 2 -": produce el mensaje ERROR "*45-2-": produce el mensaje ERROR

Recuerda cual era nuestra forma de trabajo en cap tulos anteriores?, s , una muy sencilla: un editor de textos por un lado para llevar las notas (al que llambamos libreta) y un IDE por otro para escribir el cdigo. De nuevo a o vamos a seguir esta forma de trabajar que aunque sencilla, es muy poderosa. Sin embargo, vamos a simplicar an ms ya que en lugar de un IDE vamos a u a 3 con el modo python-mode 4 (vaya, ahora usar otro editor de textos, Emacs podemos usar el mismo programa como libreta, como editor de cdigo y o A X, que no se nos olvide). tambin como editor de LTE e
3 4

Emacs 23.1 - ftp://ftp.gnu.org/gnu/emacs/windows/emacs-23.1-bin-i386.zip http://www.rwdev.eu/articles/emacspyeng

250

Cap tulo 11 A por ello, entonces. Lo primero, creamos un directorio donde vamos a trabajar, por ejemplo supercalculadora python. Y dentro de este directorio creamos nuestro primer chero que, por supuesto, ser de pruebas: a ut supercalculadora.py. Vamos a empezar con los mismos test unitarios procedentes del primer test de aceptacin. Estos son los siguientes: o
# Aceptacin - "2 + 2", devuelve 4 o Sumar 2 al nmero 2, devuelve 4 u La cadena "2 + 2", tiene dos nmeros y un operador: 2, 2 y + u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce el mensaje ERROR o # Aceptacin - "* * 4 - 2": produce el mensaje ERROR o # Aceptacin -"* 4 5 - 2": produce el mensaje ERROR o # Aceptacin -"* 4 5 - 2 : produce el mensaje ERROR o # Aceptacin -"*45-2-": produce el mensaje ERROR o

1 2 3 4 5 6 7 8 9 10

Vamos a por el primer test de desarrollo:

import unittest import supercalculadora class T e s t s S u p e r c a l c u l a d o r a ( unittest . TestCase ): def test_sumar_2_y_2 ( self ): calc = supercalculadora . Supercalculadora () self . failUnlessEqual (4 , calc . sumar (2 , 2)) if __name__ == " __main__ " : unittest . main ()

El cdigo no compila porque todav no hemos creado ni el chero supercalo a culadora ni la clase Supercalculadora. Sin embargo ya hemos diseado n algo: el nombre de la clase, su constructor y su primer mtodo (nombre, e parmetros que recibe y el resultado que devuelve). El diseo, como vemos, a n emerge de la prueba. A continuacin escribimos el m o nimo cdigo posible o para que el test pase al igual que en cap tulos anteriores. Devolver 4, que es el resultado correcto, es la implementacin m o nima en este caso.

1 2 3

11.1: supercalculadora.py

class Supercalculadora : def sumar ( self , a , b ): return 4

1 2

Muy bien, ahora habr que refactorizar pero estamos tan al principio a que todav no hay mucho que hacer as que vamos a seguir con la siguiente a prueba unitaria sobre el mismo tema.
def test_sumar_5_y_7 ( self ): self . failUnlessEqual (12 , self . calc . sumar (5 , 7))

251

Cap tulo 11 Ejecutamos (ctrl+c ctrl+c) en Emacs y observamos que falla (en una ventana distinta dentro del editor):

1 2 3 4 5 6 7 8 9 10 11 12

.F =================================================================== FAIL : test_sumar_5_y_7 ( __main__ . T e s t s S u p e r c a l c u l a d o r a ) ------------------------------------------------------------------Traceback ( most recent call last ): File " < stdin > " , line 11 , in test_sumar_5_y_7 AssertionError : 12 != 4 ------------------------------------------------------------------Ran 2 tests in 0.000 s FAILED ( failures =1)

Vamos entonces a cambiar la implementacin que ser m o a nima para este test:

1 2 3

class Supercalculadora : def sumar ( self , a , b ): return 12

Sin embargo, aunque este es el cdigo m o nimo que hace pasar la segunda prueba, hace fallar la primera (es un fallo que podr amos haber obviado pero por motivos didcticos lo incluimos como primer ejemplo ya que en ejemplos a ms complejos no es fcil ver el fallo de pruebas anteriores) por lo tanto a a debemos buscar la implementacin m o nima que hace pasar todas las pruebas. En este caso, ser a:

1 2 3

class Supercalculadora : def sumar ( self , a , b ): return a + b

Ahora ya tenemos la luz verde (Ran 2 tests in 0.000s OK en nuestro caso) as que pasemos al paso de refactorizacin. Esta vez, s que hay o cosas que hacer ya que como vemos en las pruebas, la l nea calc = supercalculadora.Supercalculadora() est duplicada. Subsanmoslo creando a e un mtodo setUp (las maysculas y minsculas son importantes) donde moe u u vemos la duplicidad de las pruebas (por consistencia, aadimos tambin el n e mtodo tearDown aunque no har nada ya que es el contrario a setUp) e a

1 2 3 4 5 6 7 8 9 10 11

class T e s t s S u p e r c a l c u l a d o r a ( unittest . TestCase ): def setUp ( self ): self . calc = supercalculadora . Supercalculadora () def tearDown ( self ): pass def test_sumar_2_y_2 ( self ): self . failUnlessEqual (4 , self . calc . sumar (2 , 2)) def test_sumar_5_y_7 ( self ):

252

Cap tulo 11
self . failUnlessEqual (12 , self . calc . sumar (5 , 7))

12

Voy a aadir un ultimo test que pruebe que el orden en los sumandos n no altera el resultado (propiedad conmutativa), adems de aadirlo en la a n libreta ya que puede ser de importancia para futuras operaciones. Tambin, e como puede apreciar, marcaremos con HECHAS las cuestiones resueltas de la libreta por claridad.
# Aceptacin - "2 + 2", devuelve 4 o Sumar 2 al nmero 2, devuelve 4 - HECHO! u La propiedad conmutativa se cumple La cadena "2 + 2", tiene dos nmeros y un operador: 2, 2 y + u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce el mensaje ERROR o # Aceptacin - "* * 4 - 2": produce el mensaje ERROR o # Aceptacin - "* 4 5 - 2": produce el mensaje ERROR o # Aceptacin - "* 4 5 - 2 : produce el mensaje ERROR o # Aceptacin - "*45-2-": produce el mensaje ERROR o

Vamos con ello con un tests:

1 2 3

def t e s t _ s u m a r _ p r o p i e d a d _ c o n m u t a t i v a ( self ): self . failUnlessEqual ( self . calc . sumar (5 , 7) , self . calc . sumar (7 , 5))

La prueba pasa sin necesidad de tocar nada. Genial!, la propiedad conmutativa se cumple. Hasta el momento y para recapitular, tenemos los siguientes tests:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

class T e s t s S u p e r c a l c u l a d o r a ( unittest . TestCase ): def setUp ( self ): self . calc = supercalculadora . Supercalculadora () def tearDown ( self ): pass def test_sumar_2_y_2 ( self ): self . failUnlessEqual (4 , self . calc . sumar (2 , 2)) def test_sumar_5_y_7 ( self ): self . failUnlessEqual (12 , self . calc . sumar (5 , 7)) def t e s t _ s u m a r _ p r o p i e d a d _ c o n m u t a t i v a ( self ): self . failUnlessEqual ( self . calc . sumar (5 , 7) , self . calc . sumar (7 , 5))

Todo bien hasta el momento. Fenomenal!

253

Cap tulo 11

# Aceptacin - "2 + 2", devuelve 4 o Sumar 2 al nmero 2, devuelve 4 - HECHO! u La propiedad conmutativa se cumple - HECHO! La cadena "2 + 2", tiene dos nmeros y un operador: 2, 2 y + u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce el mensaje ERROR o # Aceptacin - "* * 4 - 2": produce el mensaje ERROR o # Aceptacin - "* 4 5 - 2": produce el mensaje ERROR o # Aceptacin - "* 4 5 - 2 : produce el mensaje ERROR o # Aceptacin - "*45-2-": produce el mensaje ERROR o

Hemos actualizado la libreta con el ultimo resultado. Recordad que es muy importante que la libreta est en todo momento actualizada y reeje el e estado actual de nuestro desarrollo. En este punto, paremos un momento y miremos si hay algo que refactorizar. Vemos que el cdigo es bastante limpio as que reexionamos sobre o cmo seguir. o Hemos hecho la suma pero, sin embargo, no tenemos ningn test de acepu tacin sobre la resta que parece algo distinta. En el caso anterior deber o amos vericar el orden ya que ser importante (no se cumple la propiedad conmua tativa). Adems, en ciertos casos el nmero devuelto podr ser negativo y a u a eso no lo hab amos contemplado. Debe ser un error o un nmero negatiu vo es correcto?, obviamente este caso es trivial y queremos que los valores negativos sean aceptados (un nmero negativo es un entero y eso es lo que u hab pedido el cliente, no?) pero, por si acaso, preguntmosle al cliente!. a e Es mejor estar seguros de que quiere trabajar con negativos y que cuando dijo entero realmente quer decir entero y no natural. a Aadamos en la libreta un test de aceptacin y unas cuantas pruebas n o unitarias para este nuevo caso.

254

Cap tulo 11

# Aceptacin - "2 + 2", devuelve 4 o Sumar 2 al nmero 2, devuelve 4 - HECHO! u La propiedad conmutativa se cumple - HECHO! La cadena "2 + 2", tiene dos nmeros y un operador: 2, 2 y + u # Aceptacin - "5 - 3", devuelve 2 o Restar 5 al nmero 3, devuelve 2 u Restar 2 al nmero 3, devuelve -1 u La propiedad conmutativa no se cumple # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce el mensaje ERROR o # Aceptacin - "* * 4 - 2": produce el mensaje ERROR o # Aceptacin - "* 4 5 - 2": produce el mensaje ERROR o # Aceptacin - "* 4 5 - 2 : produce el mensaje ERROR o # Aceptacin - "*45-2-": produce el mensaje ERROR o Operaciones con nmeros negativos u

Podr amos seguir con el ultimo test unitario que tenemos en la suma, el de los operadores y operandos. Sin embargo, parece algo distinto y sabemos que requiere algo ms que cambios en Supercalculadora, por eso a decidimos posponerlo un poco y pasarnos a la resta. Escribimos el primer test para la resta:

1 2 3

... def test_restar_5_y_3 ( self ): self . failUnlessEqual (2 , self . calc . restar (5 , 3))

1 2 3 4

Por supuesto, falla (la operacin resta no existe todav as que vamos o a), a ponernos con la implementacin como ya hemos hecho anteriormente. o
class Supercalculadora : ... def restar ( self , a , b ): return 2

1 2 3

Pasa de nuevo!. Refactorizamos?, parece que todav el cdigo (tanto a o de las pruebas como el resto) es limpio, as que vamos a seguir con otra prueba.
... def test_restar_2_y_3 ( self ): self . failUnlessEqual ( -1 , self . calc . restar (2 , 3))

Luz roja de nuevo. Vamos a arreglarlo de tal modo que tanto esta como la prueba anterior (y por supuesto todas las dems que formen o no parte de a la resta) pasen. 255

Cap tulo 11 11.2: supercalculadora.py

1 2 3 4 5 6

class Supercalculadora : def sumar ( self , a , b ): return a + b def restar ( self , a , b ): return a - b

Otra vez en verde. Y seguimos con la siguiente prueba ya que al igual que antes no vemos necesidad de refactorizar (pero lo mantenemos en mente!)

1 2 3 4

... def t e s t _ r e s t a r _ n o _ p r o p i e d a d _ c o n m u t a t i v a ( self ): self . failIfEqual ( self . calc . restar (5 , 3) , self . calc . restar (3 , 5))

Sigue funcionando sin tocar nada... pero, al escribir esto se nos vienen a la cabeza otra serie de preguntas (actualizamos, a la vez, la libreta).
# Aceptacin - "2 + 2", devuelve 4 o Sumar 2 al nmero 2, devuelve 4 - HECHO! u La propiedad conmutativa se cumple - HECHO! La cadena "2 + 2", tiene dos nmeros y un operador: 2, 2 y + u # Aceptacin - "5 - 3", devuelve 2 o Restar 5 al nmero 3, devuelve 2 - HECHO! u Restar 2 al nmero 3, devuelve -1 - HECHO! u La propiedad conmutativa no se cumple - HECHO! # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", produce el mensaje ERROR o # Aceptacin - "* * 4 - 2": produce el mensaje ERROR o # Aceptacin - "* 4 5 - 2": produce el mensaje ERROR o # Aceptacin - "* 4 5 - 2 : produce el mensaje ERROR o # Aceptacin - "*45-2-": produce el mensaje ERROR o Qu nmero es el ms grande que se puede manejar?, y el ms e u a a peque~o? n

Las nuevas cuestiones sobre l mites numricos ataen a todas las operae n ciones de la calculadora y no sabemos qu debemos hacer en esta disyuntiva. e Lo primero es claricar qu comportamiento debe tener el software pregune tando al cliente. De ah podemos sacar los nuevos test de aceptacin para o las dudas que tenemos. No vamos a ir por ese camino como hemos hecho en los cap tulos anteriores. Recuerde que pod amos recorrer el rbol de tests en profundidad a o en amplitud. Anteriormente lo hemos hecho en amplitud as que esta vez vamos a coger el otro camino y hacerlo en profundidad. Vamos a ir ms lejos a 256

Cap tulo 11 en el camino ya andado y no movindonos a otras partes del rbol (los nuee a vos tests que aparecer sobre el nmero mximo y m an u a nimo, por ejemplo). Vamos a seguir con el test de aceptacin que ya o bamos manejando. Ahora bien, parece que vamos a cambiar de tercio porque el test unitario que hab amos denido al principio en la suma es ms un test de un parser a que de la calculadora en s Nos ponemos con l en el mismo sitio donde . e estabamos escribiendo los test unitarios anteriormente con algunas decisiones de diseo ya en la cabeza, por ejemplo, que la expresin ser una cadena de n o a caracteres. Por claridad, ponemos todas la pruebas hasta ahora:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

11.3: ut supercalculadora.py

import unittest import supercalculadora import expr_aritmetica class T e s t s S u p e r c a l c u l a d o r a ( unittest . TestCase ): def setUp ( self ): self . calc = calculadora . Calculadora () def tearDown ( self ): pass def test_sumar_2_y_2 ( self ): self . failUnlessEqual (4 , self . calc . sumar (2 , 2)) def test_sumar_5_y_7 ( self ): self . failUnlessEqual (12 , self . calc . sumar (5 , 7)) def t e s t _ s u m a r _ p r o p i e d a d _ c o n m u t a t i v a ( self ): self . failUnlessEqual ( self . calc . sumar (5 , 7) , self . calc . sumar (7 , 5)) def test_restar_5_y_3 ( self ): self . failUnlessEqual (2 , self . calc . restar (5 , 3)) def test_restar_2_y_3 ( self ): self . failUnlessEqual ( -1 , self . calc . restar (2 , 3)) def t e s t _ r e s t a r _ n o _ p r o p i e d a d _ c o n m u t a t i v a ( self ): self . failIfEqual ( self . calc . restar (5 , 3) , self . calc . restar (3 , 5)) def t e s t _ e x t r a e r _ o p e r a n d o s _ y _ o p e r a d o r e s _ e n _ 2 _ m a s _ 2 ( self ): expresion = expr_aritmetica . ExprAritmetica () self . failUnless ({ Operandos : [2 , 2] , Operadores : [ + ]} , expresion . parse ( " 2 + 2 " ))

Estamos en rojo porque el archivo expr aritmetica.py ni siquiera existe. Lo corregimos pero tambin falla porque la clase ExprAritmetica tampoco e existe. Lo corregimos igualmente y ahora falta el mtodo parse que creamos e con la m nima implementacin posible. Ser algo as o a :

257

Cap tulo 11 11.4: expr aritmetica.py

1 2 3

class ExprAritmetica : def parse ( self , exp ): return { Operandos : [2 , 2] , Operadores : [ + ]}

Por n pasa las pruebas y, como es habitual, pensamos en el siguiente paso, la refactorizacin. o Ummm, hay muchas cosas que refactorizar. Empecemos por el nombre de la clase Supercalculadora (y su chero). Parece ser ahora que tenemos dos mdulos que son parte de un programa que ser la supercalculadora, o a la clase supercalculadora deber ser, en realidad, calculadora puesto que es a la clase encargada de calcular (supercalculadora ser la aplicacin en s o, a o como mucho, la clase principal). Cambiemos el nombre del chero (por calculadora.py) y de la clase. Sigamos por los tests. Tenemos los tests de la clase Calculadora y de la clase ExprAritmetica en la misma clase de tests llamada TestsSupercalculadora. Aunque ambos son tests de la aplicacin Supercalculadora creo o que es necesario tenerlos separados en tests para Calculadora y tests para ExprAritmetica para que todo quede ms claro. Para alcanzar mayor claridad a an, vamos a separarlos tambin en dos cheros de tests, uno para cada u e mdulo al igual que la implementacin. En el archivo principal creamos o o una suite donde recogemos todos las pruebas de los distintos mdulos. o Tenemos entonces:

1 2 3 4 5 6 7 8 9 10 11

11.5: ut supercalculadora.py

import unittest import ut_calculadora import ut_expr_aritmetica if __name__ == " __main__ " : suite = unittest . TestSuite () suite . addTest ( unittest . makeSuite ( ut_calculadora . TestsCalculadora )) suite . addTest ( unittest . makeSuite ( ut_expr_aritmetica . TestsExprAritmetica )) unittest . TextTestRunner ( verbosity =3). run ( suite )


1 2 3 4 5 6 7 8 9 10

11.6: ut calculadora.py

import unittest import calculadora class TestsCalculadora ( unittest . TestCase ): def setUp ( self ): self . calc = calculadora . Calculadora () def tearDown ( self ): pass

258

Cap tulo 11
def test_sumar_2_y_2 ( self ): self . failUnlessEqual (4 , self . calc . sumar (2 , 2)) def test_sumar_5_y_7 ( self ): self . failUnlessEqual (12 , self . calc . sumar (5 , 7)) def t e s t _ s u m a r _ p r o p i e d a d _ c o n m u t a t i v a ( self ): self . failUnlessEqual ( self . calc . sumar (5 , 7) , self . calc . sumar (7 , 5)) def test_restar_5_y_3 ( self ): self . failUnlessEqual (2 , self . calc . restar (5 , 3)) def test_restar_2_y_3 ( self ): self . failUnlessEqual ( -1 , self . calc . restar (2 , 3)) def t e s t _ r e s t a r _ n o _ p r o p i e d a d _ c o n m u t a t i v a ( self ): self . failIfEqual ( self . calc . restar (5 , 3) , self . calc . restar (3 , 5))

11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29


1 2 3 4 5 6 7 8 9

11.7: ut expr aritmetica.py

import unittest import expr_aritmetica class TestsExprAritmetica ( unittest . TestCase ): def t e s t _ e x t r a e r _ o p e r a n d o s _ y _ o p e r a d o r e s _ e n _ 2 _ m a s _ 2 ( self ): expresion = expr_aritmetica . ExprAritmetica () self . failUnlessEqual ({ Operandos : [2 , 2] , Operadores : [ + ]} , expresion . parse ( " 2 + 2 " ))

Sabemos que para ExprAritmetica tenemos que implementar mucho ms pero por ahora lo vamos a dejar hasta que salga ms funcionalidad a a en las otras pruebas de aceptacin. En este punto, actualicemos la libreta o y sigamos con la siguiente prueba de aceptacin. Esta es un poco compleja o tocando muchos puntos a la vez as que mejor la aplazamos y vamos a pensar qu podemos hacer ahora. e

259

Cap tulo 11

# Aceptacin - "2 + 2", devuelve 4 o Sumar 2 al nmero 2, devuelve 4 - HECHO! u La propiedad conmutativa se cumple - HECHO! La cadena "2 + 2", tiene dos nmeros y un operador: 2, 2 y + u HECHO! # Aceptacin - "5 - 3", devuelve 2 o Restar 5 al nmero 3, devuelve 2 - HECHO! u Restar 2 al nmero 3, devuelve -1 - HECHO! u La propiedad conmutativa no se cumple - HECHO! # Aceptacin - "2 + -2", devuelve 0 o Sumar 2 al nmero -2, devuelve 0 u Restar 2 al nmero -5, devuelve -7 u Restar -2 al nmero -7, devuelve -5 u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", devuelve un error o # Aceptacin - "* * 4 - 2": devuelve un error o # Aceptacin - "* 4 5 - 2": devuelve un error o # Aceptacin - "* 4 5 - 2 : devuelve un error o # Aceptacin - "*45-2-": devuelve un error o Qu nmero es el ms grande que se puede manejar?, y el ms e u a a peque~o? n

1 2 3 4 5 6 7

Nos ponemos con los nmeros negativos puesto que parece ms relacionau a do con el mismo tema, las operaciones. Como siempre, escribimos la prueba primero (para abreviar, pongo todas las pruebas juntas aunque primero se escribe una, luego el cdigo m o nimo para que pase, luego se refactoriza, luego otra prueba...). En esta ocasin, hemos escrito las pruebas en dos tests (para o suma y para resta puesto que son dos comportamientos distintos en nuestra aplicacin) dando nombres que reejen con claridad la intencin de los tests. o o
... def t e s t _ s u m a r _ n u m e r o s _ n e g a t i v o s ( self ): self . failUnlessEqual (0 , self . calc . sumar (2 , -2)) def t e s t _ r e s t a r _ n u m e r o s _ n e g a t i v o s ( self ): self . failUnlessEqual ( -7 , self . calc . restar ( -5 , 2)) self . failUnlessEqual ( -5 , self . calc . restar ( -7 , -2))

Esto ha sido fcil, la implementacin que ten a o amos ya soportaba los nmeu ros negativos por los que el test pasa sin necesidad de hacer ninguna modicacin en el cdigo. Esto es bueno ya que tenemos ms tests que verican o o a el funcionamiento de la implementacin pero malo a la vez porque no hemos o avanzado con nuestro software. Debemos seguir escribiendo pruebas unitarias que nos hagan implementar ms la funcionalidad del software que estamos a 260

Cap tulo 11 escribiendo. Actualicemos la libreta, escojamos por dnde seguir y pensemos en nuevos o test de aceptacin y/o unitarios si hiciera falta. o
# Aceptacin - "2 + 2", devuelve 4 o Sumar 2 al nmero 2, devuelve 4 - HECHO! u La propiedad conmutativa se cumple - HECHO! La cadena "2 + 2", tiene dos nmeros y un operador: 2, 2 y + u HECHO! # Aceptacin - "5 - 3", devuelve 2 o Restar 5 al nmero 3, devuelve 2 - HECHO! u Restar 2 al nmero 3, devuelve -1 - HECHO! u La propiedad conmutativa no se cumple - HECHO! # Aceptacin - "2 + -2", devuelve 0 o Sumar 2 al nmero -2, devuelve 0 - HECHO! u Restar 2 al nmero -5, devuelve -7 - HECHO! u Restar -2 al nmero -7, devuelve -5 - HECHO! u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", devuelve un error o Dividir 2 entre 2 da 1 Dividir 10 entre 5 da 2 Dividir 10 entre -5 da -2 Dividir -10 entre -5 da 2 Dividir 3 entre 2 lanza una excepcin o Dividir 3 entre 0 lanza una excepcin o La cadena "10 / -5", tiene dos nmeros y un operador: 10, -5 y u / # Aceptacin - "* * 4 - 2": devuelve un error o # Aceptacin - "* 4 5 - 2": devuelve un error o # Aceptacin - "* 4 5 - 2 : devuelve un error o # Aceptacin - "*45-2-": devuelve un error o Qu nmero es el ms grande que se puede manejar?, y el ms e u a a peque~o? n

Vemos que hemos aadido algunos test unitarios al caso de la divisin. n o Vamos a seguir con esta prueba de aceptacin ya que parece una buena forma o de seguir despus de la suma y la resta. e Empezamos con las pruebas:

1 2 3 4 5

... def t e s t _ d i v i s i o n _ e x a c t a ( self ): self . failUnlessEqual (1 , self . calc . dividir (2 , 2)) self . failUnlessEqual (2 , self . calc . dividir (10 , 5))

261

Cap tulo 11
def t e s t _ d i v i s i o n _ e x a c t a _ n e g a t i v a ( self ): self . failUnlessEqual ( -2 , self . calc . dividir (10 , -5)) self . failUnlessEqual (2 , self . calc . dividir ( -10 , -5))

6 7 8

Recuerde que es un test a la vez, esto es slo para abreviar. Es decir, las o pruebas anteriores se har en tres pasos de prueba - cdigo - refactorian o zacin. El cdigo despus de estas pruebas, ser (recuerde que la primera o o e a aproximacin ser nada ms que return 1) o a a

1 2 3

... def dividir ( self , a , b ): return a / b

Seguimos con el test de divisin no entera: o

1 2 3

... def t e s t _ d i v i s i o n _ n o _ e n t e r a _ d a _ e x c e p c i o n ( self ): self . failUnlessRaises ( ValueError , self . calc . dividir , 3 , 2)

Falla, as que escribimos la m mima implementacin para hacer pasar las o pruebas:

1 2 3 4 5 6

... def dividir ( self , a , b ): if a % b != 0: raise ValueError else : return a / b

Parace que no hace falta refactorizar, as que seguimos con el siguiente.

1 2 3 4

... def test_division_por_0 ( self ): self . failUnlessRaises ( ZeroDivisionError , self . calc . dividir , 3 , 0)

Pasa sin necesidad de tocar el cdigo ya que a / b lanza una excepcin o o directamente. Hace falta refactorizar?, parece que todav no. Ahora pasaa mos a un nuevo test unitario sobre las expresiones aritmticas que promete e ser un poco ms complejo de resolver. Vamos primero con el test y luego con a la implementacin, como siempre. o

1 2 3 4 5 6 7 8

... class TestsExprAritmetica ( unittest . TestCase ): ... def t e s t _ e x t r a e r _ o p e r a n d o s _ y _ o p e r a d o r e s _ e n _ 1 0 _ e n t r e _ m e n o s _ 5 ( self ): expresion = expr_aritmetica . ExprAritmetica () self . failUnlessEqual ({ Operandos : [10 , -5] , Operadores : [ / ]} , expresion . parse ( " 10 / -5 " ))

Y el cdigo ms simple que se me ha ocurrido para pasar ambas pruebas o a es el siguiente: 262

Cap tulo 11

1 2 3 4 5 6 7 8 9 10 11 12 13

import string class ExprAritmetica : def parse ( self , exp ): operandos = [] operadores = [] tokens = string . split ( exp ) for token in tokens : try : operandos . append ( string . atoi ( token )) except ValueError : operadores . append ( token ) return { operandos : operandos , operadores : operadore s }

Un ultimo paso antes de actualizar la libreta. La refactorizacin. El cdigo o o parece limpio pero aunque es simple, las excepciones se usan fuera del ujo normal de comportamiento (hay lgica en el except) y esto, en general, no o es bueno (aunque en un caso tan simple no importe mucho). Lo mejor es que lo arreglemos junto con los tests de la clase ExprAritmetica donde vemos que hay algo de duplicidad.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

11.8: expr aritmetica.py


def __es_numero__ ( self , cadena ): try : string . atoi ( cadena ) return True except ValueError : return False def parse ( self , exp ): operandos = [] operadores = [] tokens = exp . split () for token in tokens : if self . __es_numero__ ( token ): operandos . append ( string . atoi ( token )) else : operadores . append ( token ) return { operandos : operandos , operadores : operadore s }

Una vez ms y como en los casos anteriores, movemos la duplicidad al a mtodo setUp y creamos su contrario tearDown vac e o.

1 2 3 4 5 6 7 8 9

11.9: ut expr aritmetica.py

import unittest import expr_aritmetica class TestsExprAritmetica ( unittest . TestCase ): def setUp ( self ): self . expresion = expr_aritmetica . ExprAritmetica () def tearDown ( self ): pass

263

Cap tulo 11

10 11 12 13 14 15 16 17 18 19

def t e s t _ e x t r a e r _ o p e r a n d o s _ y _ o p e r a d o r e s _ e n _ 2 _ m a s _ 2 ( self ): self . failUnlessEqual ({ operandos : [2 , 2] , operadores : [ + ]} , self . expresion . parse ( " 2 + 2 " )) def t e s t _ e x t r a e r _ o p e r a n d o s _ y _ o p e r a d o r e s _ e n _ 1 0 _ e n t r e _ m e n o s _ 5 ( self ): self . failUnlessEqual ({ operandos : [10 , -5] , operadores : [ / ]} , self . expresion . parse ( " 10 / -5 " ))

Despus de todos estos cambios y de las nuevas pruebas de aceptacin e o que hemos creado, debemos actualizar la libreta:
# Aceptacin - "2 + 2", devuelve 4 o Sumar 2 al nmero 2, devuelve 4 - HECHO! u La propiedad conmutativa se cumple - HECHO! La cadena "2 + 2", tiene dos nmeros y un operador: 2, 2 y + u - HECHO! # Aceptacin - "5 - 3", devuelve 2 o Restar 5 al nmero 3, devuelve 2 - HECHO! u Restar 2 al nmero 3, devuelve -1 - HECHO! u La propiedad conmutativa no se cumple - HECHO! # Aceptacin - "2 + -2", devuelve 0 o Sumar 2 al nmero -2, devuelve 0 - HECHO! u Restar 2 al nmero -5, devuelve -7 - HECHO! u Restar -2 al nmero -7, devuelve -5 - HECHO! u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o # Aceptacin - "3 / 2", devuelve un error o Dividir 2 entre 2 da 1 - HECHO! Dividir 10 entre 5 da 2 - HECHO! Dividir 10 entre -5 da -2 - HECHO! Dividir -10 entre -5 da 2 - HECHO! Dividir 3 entre 2 lanza una excepcin - HECHO! o Dividir 3 entre 0 lanza una excepcin - HECHO! o La cadena "10 / -5", tiene dos nmeros y un operador: 10, -5 y u / - HECHO! # Aceptacin - "* * 4 - 2": devuelve un error o # Aceptacin - "* 4 5 - 2": devuelve un error o # Aceptacin - "* 4 5 - 2 : devuelve un error o # Aceptacin - "*45-2-": devuelve un error o Qu nmero es el ms grande que se puede manejar?, y el ms e u a a peque~o? n

Vamos a movernos un poco ms en la ExprAritmetica. Hagmos algua a 264

Cap tulo 11 nas pruebas ms. a


def t e s t _ e x t r a e r _ o p e r a n d o s _ y _ o p e r a d o r e s _ e x p r _ s i n _ p t e s i s ( self ): self . failUnlessEqual ({ operandos : [5 , 4 , 2 , 2] , operadores : [ + , * , / ]} , self . expresion . parse ( " 5 + 4 * 2 / 2 " ))

1 2 3 4

Vaya, qu sorpresa!. Nuestro parser funciona para expresiones ms come a plejas sin parntesis. Pongamos al d la libreta y pensemos en cmo seguir e a o adelante.
# Aceptacin - "2 + 2", devuelve 4 o Sumar 2 al nmero 2, devuelve 4 - HECHO! u La propiedad conmutativa se cumple - HECHO! La cadena "2 + 2", tiene dos nmeros y un operador: 2, 2 y + u - HECHO! # Aceptacin - "5 - 3", devuelve 2 o Restar 5 al nmero 3, devuelve 2 - HECHO! u Restar 2 al nmero 3, devuelve -1 - HECHO! u La propiedad conmutativa no se cumple - HECHO! # Aceptacin - "2 + -2", devuelve 0 o Sumar 2 al nmero -2, devuelve 0 - HECHO! u Restar 2 al nmero -5, devuelve -7 - HECHO! u Restar -2 al nmero -7, devuelve -5 - HECHO! u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o "5 + 4 * 2 / 2", devuelve 9 Operandos son 5, 4, 2 y 2 y operadores +, *, / HECHO! # Aceptacin - "3 / 2", devuelve un error o Dividir 2 entre 2 da 1 - HECHO! Dividir 10 entre 5 da 2 - HECHO! Dividir 10 entre -5 da -2 - HECHO! Dividir -10 entre -5 da 2 - HECHO! Dividir 3 entre 2 lanza una excepcin - HECHO! o Dividir 3 entre 0 lanza una excepcin - HECHO! o La cadena "10 / -5", tiene dos nmeros y un operador: 10, -5 y u / - HECHO! # Aceptacin - "* * 4 - 2": devuelve un error o # Aceptacin - "* 4 5 - 2": devuelve un error o # Aceptacin - "* 4 5 - 2 : devuelve un error o # Aceptacin - "*45-2-": devuelve un error o Qu nmero es el ms grande que se puede manejar?, y el ms e u a a peque~o? n

En este momento tenemos un poco de la calculadora y un poco de la 265

Cap tulo 11 ExprAritmetica. Vamos a dar un giro y en vez de seguir, vamos a integrar estas dos partes. La clase principal ser Supercalculadora que usar Cala a culadora para calcular el resultado de las operaciones y ExprAritmetica para evaluar las expresiones. Decidimos de antemano que a la calculadora le vamos a pasar expresiones aritmticas en forma de cadena de caracteres para e que las calcule. Vamos a ver qu diseo sacamos siguiendo TDD... e n Renombramos el chero actual ut supercalculadora.py a ut main.py y creamos de nuevo el chero ut supercalculadora.py. Comenzamos con las pruebas. La primera ser la ms bsica de la suma. En este punto decia a a dimos que ExprAritmetica ser pasada como parmetro en el constructor a a de Supercalculadora ya que tiene un comportamiento muy denido.

1 2 3 4 5 6 7 8 9

11.10: ut supercalculadora.py

import supercalculadora import ut_calculadora import ut_expr_aritmetica class T e s t s S u p e r c a l c u l a d o r a ( unittest . TestCase ): def test_sumar ( self ): sc = supercalculadora . Supercalculadora ( expr_aritmetica . ExprAritmetica ()) self . failUnlessEqual ( " 4 " , sc . calcular ( " 2 + 2 " ))

El test falla. El chero supercalculadora.py no existe. Una vez creado sigue en rojo porque la clase Supercalculadora no existe y, posteriormente, el mtodo calcular tampoco. Corrigiendo paso por paso llegamos a una e implementacin nal que ser la siguiente: o a

1 2 3 4 5 6 7 8 9 10 11 12 13 14

11.11: supercalculadora.py

import expr_aritmetica import calculadora class Supercalculadora : def __init__ ( self , parser ): self . calc = calculadora . Calculadora () self . parser = parser def calcular ( self , expresion ): expr_descompuesta = self . parser . parse ( expresion ) if expr_descompuesta [ operadores ][0] == + : return str ( self . calc . sumar ( expr_descompuesta [ operandos ][0] , expr_descompuesta [ operandos ][1]))

Ahora tenemos la luz verde de nuevo pero nos empezamos a dar cuenta de que parse va a ser dif de utilizar si queremos operar correctamente cil con la precedencia de operadores. De todas formas, vayamos paso a paso y creemos ms pruebas. a

266

Cap tulo 11

# Aceptacin - "2 + 2", devuelve 4 o Sumar 2 al nmero 2, devuelve 4 - HECHO! u La propiedad conmutativa se cumple - HECHO! La cadena "2 + 2", tiene dos nmeros y un operador: 2, 2 y + u - HECHO! # Aceptacin - "5 - 3", devuelve 2 o Restar 5 al nmero 3, devuelve 2 - HECHO! u Restar 2 al nmero 3, devuelve -1 - HECHO! u La propiedad conmutativa no se cumple - HECHO! # Aceptacin - "2 + -2", devuelve 0 o Sumar 2 al nmero -2, devuelve 0 - HECHO! u Restar 2 al nmero -5, devuelve -7 - HECHO! u Restar -2 al nmero -7, devuelve -5 - HECHO! u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o "5 + 4 * 2 / 2", devuelve 9 "5 + 4 - 3", devuelve 6 "5 + 4 / 2 - 4", devuelve 3 Operandos son 5, 4, 2 y 2 y operadores +, *, / HECHO! # Aceptacin - "3 / 2", devuelve un error o Dividir 2 entre 2 da 1 - HECHO! Dividir 10 entre 5 da 2 - HECHO! Dividir 10 entre -5 da -2 - HECHO! Dividir -10 entre -5 da 2 - HECHO! Dividir 3 entre 2 lanza una excepcin - HECHO! o Dividir 3 entre 0 lanza una excepcin - HECHO! o La cadena "10 / -5", tiene dos nmeros y un operador: 10, -5 y u / - HECHO! # Aceptacin - "* * 4 - 2": devuelve un error o # Aceptacin - "* 4 5 - 2": devuelve un error o # Aceptacin - "* 4 5 - 2 : devuelve un error o # Aceptacin - "*45-2-": devuelve un error o Qu nmero es el ms grande que se puede manejar?, y el ms e u a a peque~o? n

Vayamos con el test ms sencillo 5 + 4 - 3 pero antes, creemos otros a incluso ms sencillos an: a u

1 2 3 4 5

... def test_restar ( self ): sc = supercalculadora . Supercalculadora ( expr_aritmetica . ExprAritmetica ()) self . failUnlessEqual ( " 0 " , sc . calcular ( " 2 - 2 " ))

267

Cap tulo 11 Falla como era de esperar as que lo arreglamos con la implementacin o m nima.
... def calcular ( self , expresion ): expr_descompuesta = self . parser . parse ( expresion ) if expr_descompuesta [ operadores ][0] == + : return str ( self . calc . sumar ( expr_descompuesta [ operandos ][0] , expr_descompuesta [ operandos ][1])) elif expr_descompuesta [ operadores ][0] == - : return str ( self . calc . restar ( expr_descompuesta [ operandos ][0] , expr_descompuesta [ operandos ][1]))

1 2 3 4 5 6 7 8 9 10 11


1 2 3 4 5

Y ahora si que nos ponemos con el test que hab amos identicado antes:
def t e s t _ e x p r e s i o n _ c o m p l e j a _ s i n _ p a r e n t e s i s _ s i n _ p r e c e d e n c i a ( self ): sc = supercalculadora . Supercalculadora ( expr_aritmetica . ExprAritmetica ()) self . failUnlessEqual ( " 6 " , sc . calcular ( " 5 + 4 - 3 " ))

...


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

Falla, as que nos ponemos con la implementacin. o


def calcular ( self , expresion ): expr_descompuesta = self . parser . parse ( expresion ) res = 0 for i in range ( len ( expr_descompuesta [ operadores ])): if i == 0: res = expr_descompuesta [ operandos ][0] if expr_descompuesta [ operadores ][ i ] == + : res = self . calc . sumar ( res , expr_descompuesta [ operandos ][ i + 1]) if expr_descompuesta [ operadores ][ i ] == - : res = self . calc . restar ( res , expr_descompuesta [ operandos ][ i + 1]) return str ( res )

...

1 2 3 4 5

Otra vez funciona pero sigo preocupado por la precedencia de operadores. Creemos la prueba para esto y veamos como funciona.
... def t e s t _ e x p r e s i o n _ c o m p l e j a _ s i n _ p a r e n t e s i s _ c o n _ p r e c e d e n c i a ( self ): sc = supercalculadora . Supercalculadora ( expr_aritmetica . ExprAritmetica ()) self . failUnlessEqual ( " 3 " , sc . calcular ( " 5 + 4 / 2 - 4 " ))

Falla, lo que nos tem amos. Ahora tenemos que pensar cmo solucionao mos este problema. Una primera idea es buscar los operadores ms prioritarios a y hacer la operacin y as ir poco a poco simplicando la expresin. Lo hao o cemos pasar de la manera ms sencilla (y fea... realmente fea) que se me ha a ocurrido: 268

Cap tulo 11

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

def calcular ( self , expresion ): expr_descompuesta = self . parser . parse ( expresion ) try : i = expr_descompuesta [ operadores ]. index ( / ) res_intermedio = self . calc . dividir ( expr_descompuesta [ operandos ][ i ] , expr_descompuesta [ operandos ][ i + 1]) expr_descompuesta = { operandos : [ expr_descompuesta [ operandos ][0] , res_intermedio , expr_descompuesta [ operandos ][3]] , operadores : [ expr_descompuesta [ operadores ][0] , expr_descompuesta [ operadores ][2]]} except ValueError : pass res = 0 for i in range ( len ( expr_descompuesta [ operadores ])): if i == 0: res = expr_descompuesta [ operandos ][0] if expr_descompuesta [ operadores ][ i ] == + : res = self . calc . sumar ( res , expr_descompuesta [ operandos ][ i + 1]) if expr_descompuesta [ operadores ][ i ] == - : res = self . calc . restar ( res , expr_descompuesta [ operandos ][ i + 1]) return str ( res )

Da realmente miedo... Vamos a refactorizar primero y luego a aadir n ms pruebas para este caso ya que sabemos que hay muchos ms casos a a que hay que probar. Movamos la simplicacin de la expresin (evaluacin o o o intermedia) a otro mtodo llamado simplicar. No es que sea mucho mejor e pero es algo ms legible. Con los nuevos tests, el cdigo ir tomando mejor a o a forma... o eso espero. Empezamos con los tests, luego con el cdigo. o

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

11.12: ut supercalculadora.py

class T e s t s S u p e r c a l c u l a d o r a ( unittest . TestCase ): def setUp ( self ): self . sc = supercalculadora . Supercalculadora ( expr_aritmetica . ExprAritmetica ()) def tearDown ( self ): pass def test_sumar ( self ): self . failUnlessEqual ( " 4 " , self . sc . calcular ( " 2 + 2 " )) def test_restar ( self ): self . failUnlessEqual ( " 0 " , self . sc . calcular ( " 2 - 2 " )) def t e s t _ e x p r e s i o n _ c o m p l e j a _ s i n _ p a r e n t e s i s _ s i n _ p r e c e d e n c i a ( self ): self . failUnlessEqual ( " 6 " , self . sc . calcular ( " 5 + 4 - 3 " ))

269

Cap tulo 11
def t e s t _ e x p r e s i o n _ c o m p l e j a _ s i n _ p a r e n t e s i s _ c o n _ p r e c e d e n c i a ( self ): self . failUnlessEqual ( " 3 " , self . sc . calcular ( " 5 + 4 / 2 - 4 " ))

18 19


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

11.13: supercalculadora.py

def simplificar ( self , expr_descompuesta ): expr_simplificada = {} try : i = expr_descompuesta [ operadores ]. index ( / ) res_intermedio = self . calc . dividir ( expr_descompuesta [ operandos ][ i ] , expr_descompuesta [ operandos ][ i + 1]) expr_simplificada = { operandos : [ expr_descompuesta [ operandos ][0] , res_intermedio , expr_descompuesta [ operandos ][3]] , operadores : [ expr_descompuesta [ operadores ][0] , expr_descompuesta [ operadores ][2]]} except ValueError : expr_simplificada = expr_descompuesta return expr_simplificada def calcular ( self , expresion ): expr_simplificada = self . simplificar ( self . parser . parse ( expresion )) res = 0 for i in range ( len ( expr_simplificada [ operadores ])): if i == 0: res = expr_simplificada [ operandos ][0] if expr_simplificada [ operadores ][ i ] == + : res = self . calc . sumar ( res , expr_simplificada [ operandos ][ i + 1]) if expr_simplificada [ operadores ][ i ] == - : res = self . calc . restar ( res , expr_simplificada [ operandos ][ i + 1]) return str ( res )

Seguimos con un nuevo test algo ms complejo en el mismo rea para a a mejorar nuestra implementacin. o

1 2 3 4

... def t e s t _ e x p r e s i o n _ c o m p l e j a _ s i n _ p a r e n t e s i s _ c o n _ p r e c e d e n c i a ( self ): self . failUnlessEqual ( " 3 " , self . sc . calcular ( " 5 + 4 / 2 - 4 " )) self . failUnlessEqual ( " -1 " , self . sc . calcular ( " 4 / 2 - 3 " ))

La implementacin para cumplir ambos tests ha quedado como sigue: o

1 2 3 4

def simplificar ( self , expr_descompuesta ): expr_simplificada = {} try :

270

Cap tulo 11
i = expr_descompuesta [ operadores ]. index ( / ) res_intermedio = self . calc . dividir ( expr_descompuesta [ operandos ][ i ] , expr_descompuesta [ operandos ][ i + 1]) expr_simplificada = expr_descompuesta expr_simplificada [ operadores ]. pop ( i ) expr_simplificada [ operandos ]. pop ( i ) expr_simplificada [ operandos ]. pop ( i ) expr_simplificada [ operandos ]. insert (i , res_interme dio ) except ValueError : expr_simplificada = expr_descompuesta return expr_simplificada def calcular ( self , expresion ): expr_simplificada = self . simplificar ( self . parser . parse ( expresion )) res = 0 for i in range ( len ( expr_simplificada [ operadores ])): if i == 0: res = expr_simplificada [ operandos ][0] if expr_simplificada [ operadores ][ i ] == + : res = self . calc . sumar ( res , expr_simplificada [ operandos ][ i + 1]) if expr_simplificada [ operadores ][ i ] == - : res = self . calc . restar ( res , expr_simplificada [ operandos ][ i + 1]) return str ( res )

5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

Y en el paso de refactorizacin podemos mejorar mucho las cosas movieno do la lgica a simplificar mientras resolvemos la expresin de una manera o o recursiva. Quiz, incluso, creemos algn otro mtodo donde podamos moa u e ver alguna lgica dentro del algoritmo que estamos creando. Como tenemos o pruebas unitarias ser fcil hacer cambios ms grandes en la refactorizacin a a a o sin que rompamos el funcionamiento del programa. En caso de que lo hagamos sin darnos cuenta, las pruebas unitarias nos alertarn de los errores a cometidos y as los corregiremos fcilmente. a Aprovechamos la ocasin tambin para repasar los nombres de todos los o e mtodos y mejorarlos para que todos muestren claramente la intencin de la e o funcionalidad y de las pruebas. 11.14: supercalculadora.py

1 2 3 4 5 6 7

def __operar__ ( self , expr_descompuesta ): i = None res_intermedio = 0 if / in expr_descompuesta [ operadores ]: i = expr_descompuesta [ operadores ]. index ( / ) res_intermedio = self . calc . dividir (

271

Cap tulo 11
expr_descompuesta [ operandos ][ i ] , expr_descompuesta [ operandos ][ i + 1]) elif - in expr_descompuesta [ operadores ]: i = expr_descompuesta [ operadores ]. index ( - ) res_intermedio = self . calc . restar ( expr_descompuesta [ operandos ][ i ] , expr_descompuesta [ operandos ][ i + 1]) elif + in expr_descompuesta [ operadores ]: i = expr_descompuesta [ operadores ]. index ( + ) res_intermedio = self . calc . sumar ( expr_descompuesta [ operandos ][ i ] , expr_descompuesta [ operandos ][ i + 1]) else : # Es un error , tenemos que decidir que hacer en los test # siguientes # Forzamos el error para que no haya problemas luego assert False return (i , res_intermedio ) def __simplificar__ ( self , expr_descompuesta ): if expr_descompuesta [ operadores ] == []: return expr_descompuesta (i , res_intermedio ) = self . __operar__ ( expr_descompuest a ) expr_simplificada = expr_descompuesta expr_simplificada [ operadores ]. pop ( i ) expr_simplificada [ operandos ]. pop ( i ) expr_simplificada [ operandos ]. pop ( i ) expr_simplificada [ operandos ]. insert (i , res_interme dio ) return self . __simplificar__ ( expr_simplificada ) def calcular ( self , expresion ): return str ( self . __simplificar__ ( self . parser . parse ( expresion ))[ operandos ][0]

8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43

Creamos un par de test unitarios ms para estar seguros de que la ima plementacin funciona para casos ms complejos o, en el peor de los casos, o a para cambiarla hasta que todas las pruebas pasen.

1 2 3 4 5 6 7 8 9

11.15: ut supercalculadora.py
def t e s t _ e x p r e s i o n _ c o m p l e j a _ s i n _ p a r e n t e s i s _ c o n _ p r e c e d e n c i a ( self ): self . failUnlessEqual ( " 3 " , self . sc . calcular ( " 5 + 4 / 2 - 4 " )) self . failUnlessEqual ( " -1 " , self . sc . calcular ( " 4 / 2 - 3 " )) self . failUnlessEqual ( " 1 " , self . sc . calcular ( " 4 / 2 - 3 + 1 + 6 / 3 - 1 " )) self . failUnlessEqual ( " -8 " , self . sc . calcular ( " 4 / -2 + 3 + -1 + -6 / -3 - 10 " ))

...

Qu suerte!, luz verde de nuevo sin que hayamos tenido que cambiar e nada! Eso nos da pie para seguir con otras nuevas pruebas para seguir incrementando la funcionalidad poco a poco. Pero, antes de eso, es el momento de actualizar la libreta otra vez ms. a 272

Cap tulo 11

# Aceptacin - "2 + 2", devuelve 4 o Sumar 2 al nmero 2, devuelve 4 - HECHO! u La propiedad conmutativa se cumple - HECHO! La cadena "2 + 2", tiene dos nmeros y un operador: 2, 2 y + u - HECHO! # Aceptacin - "5 - 3", devuelve 2 o Restar 5 al nmero 3, devuelve 2 - HECHO! u Restar 2 al nmero 3, devuelve -1 - HECHO! u La propiedad conmutativa no se cumple - HECHO! # Aceptacin - "2 + -2", devuelve 0 o Sumar 2 al nmero -2, devuelve 0 - HECHO! u Restar 2 al nmero -5, devuelve -7 - HECHO! u Restar -2 al nmero -7, devuelve -5 - HECHO! u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o "5 + 4 * 2 / 2", devuelve 9 "2 - 2", devuelve 0 - HECHO! "5 + 4 - 3", devuelve 6 - HECHO! "5 + 4 / 2 - 4", devuelve 3 - HECHO! "4 / 2 - 3 + 1 + 6 / 3 - 1", devuelve 1 - HECHO! "4 / -2 + 3 + -1 + -6 / -3 - 10", devuelve -8 - HECHO! Operandos son 5, 4, 2 y 2 y operadores +, *, / HECHO! # Aceptacin - "3 / 2", devuelve un error o Dividir 2 entre 2 da 1 - HECHO! Dividir 10 entre 5 da 2 - HECHO! Dividir 10 entre -5 da -2 - HECHO! Dividir -10 entre -5 da 2 - HECHO! Dividir 3 entre 2 lanza una excepcin - HECHO! o Dividir 3 entre 0 lanza una excepcin - HECHO! o La cadena "10 / -5", tiene dos nmeros y un operador: 10, -5 y u / - HECHO! # Aceptacin - "* * 4 - 2": devuelve un error o # Aceptacin - "* 4 5 - 2": devuelve un error o # Aceptacin - "* 4 5 - 2 : devuelve un error o # Aceptacin - "*45-2-": devuelve un error o Qu nmero es el ms grande que se puede manejar?, y el ms e u a a peque~o? n

Ya slo nos queda una operacin, la multiplicacin. Vamos ir con ella o o o poniendo nuestra atencin en el test de aceptacin 5 + 4 * 2 / 2. Antes o o de ponernos con esta expresin, vamos a ir a por algo ms sencillo como 4 o a 273

Cap tulo 11 * 2, -4 * 2, 4 * -2 y -4 * -2. Como siempre, seguimos la forma de trabajar TDD empezando por un test, haciendo la implementacin, refactorizando, despus tomando otro test, o e etc., repitiendo el algoritmo TDD paso a paso.

1 2 3 4 5 6 7 8

11.16: ut calculadora.py
def t e s t _ m u l t i p l i c a r _ s i m p l e ( self ): self . failUnlessEqual (8 , self . calc . multiplicar (4 , 2)) def t e s t _ m u l t i p l i c a r _ n e g a t i v a ( self ): self . failUnlessEqual ( -8 , self . calc . multiplicar ( -4 , 2)) self . failUnlessEqual ( -8 , self . calc . multiplicar (4 , -2)) self . failUnlessEqual (8 , self . calc . multiplicar ( -4 , -2))

...


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

11.17: calculadora.py

class Calculadora : def sumar ( self , a , b ): return a + b def restar ( self , a , b ): return a - b def multiplicar ( self , a , b ): return a * b def dividir ( self , a , b ): if a % b != 0: raise ValueError else : return a / b

Actualizamos la libreta antes de centrarnos en la expresin ms compleja o a con suma, multiplicacin y divisin. o o

274

Cap tulo 11

# Aceptacin - "2 + 2", devuelve 4 o Sumar 2 al nmero 2, devuelve 4 - HECHO! u La propiedad conmutativa se cumple - HECHO! La cadena "2 + 2", tiene dos nmeros y un operador: 2, 2 y + u - HECHO! # Aceptacin - "5 - 3", devuelve 2 o Restar 5 al nmero 3, devuelve 2 - HECHO! u Restar 2 al nmero 3, devuelve -1 - HECHO! u La propiedad conmutativa no se cumple - HECHO! # Aceptacin - "2 + -2", devuelve 0 o Sumar 2 al nmero -2, devuelve 0 - HECHO! u Restar 2 al nmero -5, devuelve -7 - HECHO! u Restar -2 al nmero -7, devuelve -5 - HECHO! u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o "5 + 4 * 2 / 2", devuelve 9 "4 * 2", devuelve 8 - HECHO! 4 * 2", devuelve -8 - HECHO! "4 * -2", devuelve -8 - HECHO! 4 * -2", devuelve 8 - HECHO! "2 - 2", devuelve 0 - HECHO! "5 + 4 - 3", devuelve 6 - HECHO! "5 + 4 / 2 - 4", devuelve 3 - HECHO! "4 / 2 - 3 + 1 + 6 / 3 - 1", devuelve 1 - HECHO! "4 / -2 + 3 + -1 + -6 / -3 - 10", devuelve -8 - HECHO! Operandos son 5, 4, 2 y 2 y operadores +, *, / HECHO! # Aceptacin - "3 / 2", devuelve un error o Dividir 2 entre 2 da 1 - HECHO! Dividir 10 entre 5 da 2 - HECHO! Dividir 10 entre -5 da -2 - HECHO! Dividir -10 entre -5 da 2 - HECHO! Dividir 3 entre 2 lanza una excepcin - HECHO! o Dividir 3 entre 0 lanza una excepcin - HECHO! o La cadena "10 / -5", tiene dos nmeros y un operador: 10, -5 y u / - HECHO! # Aceptacin - "* * 4 - 2": devuelve un error o # Aceptacin - "* 4 5 - 2": devuelve un error o # Aceptacin - "* 4 5 - 2 : devuelve un error o # Aceptacin - "*45-2-": devuelve un error o Qu nmero es el ms grande que se puede manejar?, y el ms e u a a peque~o? n

275

Cap tulo 11 Ahora nos ponemos con la expresin pendiente como parte de la supero calculadora. Vamos con ello con el test que ser parte de expresin compleja a o con precedencia sin parntesis. e

1 2 3

11.18: ut supercalculadora.py
def t e s t _ e x p r e s i o n _ c o m p l e j a _ s i n _ p a r e n t e s i s _ c o n _ p r e c e d e n c i a ( self ): ... self . failUnlessEqual ( " 9 " , self . sc . calcular ( " 5 + 4 * 2 / 2 " ))

Falla puesto que se va por la rama que no est implementada en operar . a Nos ponemos con ello.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

11.19: supercalculadora.py

def __operar__ ( self , expr_descompuesta ): i = None res_intermedio = 0 if / in expr_descompuesta [ operadores ]: i = expr_descompuesta [ operadores ]. index ( / ) res_intermedio = self . calc . dividir ( expr_descompuesta [ operandos ][ i ] , expr_descompuesta [ operandos ][ i + 1]) elif * in expr_descompuesta [ operadores ]: i = expr_descompuesta [ operadores ]. index ( * ) res_intermedio = self . calc . multiplicar ( expr_descompuesta [ operandos ][ i ] , expr_descompuesta [ operandos ][ i + 1]) elif - in expr_descompuesta [ operadores ]: i = expr_descompuesta [ operadores ]. index ( - ) res_intermedio = self . calc . restar ( expr_descompuesta [ operandos ][ i ] , expr_descompuesta [ operandos ][ i + 1]) elif + in expr_descompuesta [ operadores ]: i = expr_descompuesta [ operadores ]. index ( + ) res_intermedio = self . calc . sumar ( expr_descompuesta [ operandos ][ i ] , expr_descompuesta [ operandos ][ i + 1]) else : # Es un error , tenemos que decidir que hacer en los test # siguientes # Forzamos el error para que no haya problemas luego assert False return (i , res_intermedio )

Bien, ahora pasa y no hay mucho que refactorizar. Sin embargo no me siento muy seguro de que esta implementacin sea correcta. Al n y al cabo, o tenemos precedencia y la divisin puede lanzar fcilmente una excepcin si o a o el resultado no es exacto... Voy a creear una prueba ms que me de ms a a conanza. Por ejemplo, 4 - -3 * 2 / 3 + 5

11.20: ut supercalculadora.py

276

Cap tulo 11
... def t e s t _ e x p r e s i o n _ c o m p l e j a _ s i n _ p a r e n t e s i s _ c o n _ p r e c e d e n c i a ( self ): ... self . failUnlessEqual ( " 11 " , self . sc . calcular ( " 4 - -3 * 2 / 3 + 5 " ))

1 2 3 4 5

Vaya, falla!. La divisin lanza una excepcin, lo que nos tem o o amos. Si lo pensamos, tiene sentido ya que si damos prioridad a la divisin antes que a o la multiplicacin la divisin podr ser no natural cuando lo ser si multiplio o a a camos primero. Vamos a cambiar la implementacin y ver si hacemos pasar o todos los tests. 11.21: supercalculadora.py

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

... class Supercalculadora : def __init__ ( self , parser ): self . calc = calculadora . Calculadora () self . parser = parser def __operar__ ( self , expr_descompuesta ): i = None res_intermedio = 0 if * in expr_descompuesta [ operadores ]: i = expr_descompuesta [ operadores ]. index ( * ) res_intermedio = self . calc . multiplicar ( expr_descompuesta [ operandos ][ i ] , expr_descompuesta [ operandos ][ i + 1]) elif / in expr_descompuesta [ operadores ]: i = expr_descompuesta [ operadores ]. index ( / ) res_intermedio = self . calc . dividir ( expr_descompuesta [ operandos ][ i ] , expr_descompuesta [ operandos ][ i + 1]) elif - in expr_descompuesta [ operadores ]: i = expr_descompuesta [ operadores ]. index ( - ) res_intermedio = self . calc . restar ( expr_descompuesta [ operandos ][ i ] , expr_descompuesta [ operandos ][ i + 1]) elif + in expr_descompuesta [ operadores ]: i = expr_descompuesta [ operadores ]. index ( + ) res_intermedio = self . calc . sumar ( expr_descompuesta [ operandos ][ i ] , expr_descompuesta [ operandos ][ i + 1]) else : # Es un error , tenemos que decidir que hacer en los test # siguientes # Forzamos el error para que no haya problemas luego assert False return (i , res_intermedio ) ...

Ahora pasa!. Es el turno de la refactorizacin. El cdigo no ha cambiado o o mucho y es aceptable, sin embargo, tengo mis dudas con respecto a los tests. 277

Cap tulo 11 Tenemos un test con muchas pruebas dentro y quiz es tiempo de revisarlo a y ver si podemos separarlo un poco.

1 2 3 4 5 6 7 8 9 10 11 12 13

11.22: ut supercalculadora.py

def t e s t _ e x p r e s i o n _ c o m p l e j a _ s i n _ p a r e n t e s i s _ c o n _ p r e c e d e n c i a ( self ): self . failUnlessEqual ( " 3 " , self . sc . calcular ( " 5 + 4 / 2 - 4 " )) self . failUnlessEqual ( " -1 " , self . sc . calcular ( " 4 / 2 - 3 " )) self . failUnlessEqual ( " 1 " , self . sc . calcular ( " 4 / 2 - 3 + 1 + 6 / 3 - 1 " )) self . failUnlessEqual ( " -8 " , self . sc . calcular ( " 4 / -2 + 3 + -1 + -6 / -3 - 10 " )) self . failUnlessEqual ( " 9 " , self . sc . calcular ( " 5 + 4 * 2 / 2 " )) def t e s t _ e x p r _ c o m p l e j a _ t o d a s _ o p e r a c i o n e s _ s i n _ p a r e n t e s i s ( self ): self . failUnlessEqual ( " 11 " , self . sc . calcular ( " 4 - -3 * 2 / 3 + 5 " ))

Hemos sacado las expresiones (en este caso slo una, por desgracia) que o utilizan todas las operaciones sin parntesis y son ms propensas a dar errores e a en otra test que prueba expec camente este caso. Actualizamos la libreta una ultima vez:

278

Cap tulo 11

# Aceptacin - "2 + 2", devuelve 4 o Sumar 2 al nmero 2, devuelve 4 - HECHO! u La propiedad conmutativa se cumple - HECHO! La cadena "2 + 2", tiene dos nmeros y un operador: 2, 2 y + u - HECHO! # Aceptacin - "5 - 3", devuelve 2 o Restar 5 al nmero 3, devuelve 2 - HECHO! u Restar 2 al nmero 3, devuelve -1 - HECHO! u La propiedad conmutativa no se cumple - HECHO! # Aceptacin - "2 + -2", devuelve 0 o Sumar 2 al nmero -2, devuelve 0 - HECHO! u Restar 2 al nmero -5, devuelve -7 - HECHO! u Restar -2 al nmero -7, devuelve -5 - HECHO! u # Aceptacin - "5 + 4 * 2 / 2", devuelve 9 o "5 + 4 * 2 / 2", devuelve 9 - HECHO! "4 - -3 * 2 / 3 + 5", devuelve 11 - HECHO! "4 * 2", devuelve 8 - HECHO! 4 * 2", devuelve -8 - HECHO! "4 * -2", devuelve -8 - HECHO! 4 * -2", devuelve 8 - HECHO! "2 - 2", devuelve 0 - HECHO! "5 + 4 - 3", devuelve 6 - HECHO! "5 + 4 / 2 - 4", devuelve 3 - HECHO! "4 / 2 - 3 + 1 + 6 / 3 - 1", devuelve 1 - HECHO! "4 / -2 + 3 + -1 + -6 / -3 - 10", devuelve -8 - HECHO! Operandos son 5, 4, 2 y 2 y operadores +, *, / HECHO! # Aceptacin - "3 / 2", devuelve un error o Dividir 2 entre 2 da 1 - HECHO! Dividir 10 entre 5 da 2 - HECHO! Dividir 10 entre -5 da -2 - HECHO! Dividir -10 entre -5 da 2 - HECHO! Dividir 3 entre 2 lanza una excepcin - HECHO! o Dividir 3 entre 0 lanza una excepcin - HECHO! o La cadena "10 / -5", tiene dos nmeros y un operador: 10, -5 y u / - HECHO! # Aceptacin - "* * 4 - 2": devuelve un error o # Aceptacin - "* 4 5 - 2": devuelve un error o # Aceptacin - "* 4 5 - 2 : devuelve un error o # Aceptacin - "*45-2-": devuelve un error o Qu nmero es el ms grande permitido?, y el ms peque~o? e u a a n

279

Cap tulo 11 Para terminar, vamos a crear una ultima prueba con un stub y vamos a cambiar nuestro diseo en base a esto sin necesidad de implementar nada (o n casi nada). No voy a explicar el uso de mocks y stubs ya que se ha explicado5 y ver que es igual nada ms que cambiando la sintaxis del lenguaje. a a Simplemente quiero mostrar una posible solucin en Python usando pymox 6 o para que vea que en este lenguaje se puede lograr lo mismo que en .NET y Java. Pensemos ahora en las expresiones aritmticas. Por ahora suponemos e que estn bien y no tenemos ningn validador que detecte errores. En algn a u u momento lo necesitaremos pero todav es pronto para ponernos a ello. Sin a embargo, queremos ver cmo se comportar nuestro cdigo si tuviramos o a o e ese validador y que ste nos dijese que una expresin es invalida. Esto lo e o podemos hacer con un stub. Vamos a crear un test usando pymox que simule la respuesta de validar cada vez que calculamos una expresin aritmtica. Para esto, obviamente, o e necesitamos tomar ciertas decisiones en el diseo, como por ejemplo que la n clase Supercalculadora recibir un parmetro ms que ser el validador. a a a a Suponemos tambin que el validador va a responder con un booleano si la e expresin es vlida o no. En este punto, vamos a probar slo que la expresin o a o o es invlida pero, como ya hemos dicho, tenemos que comprobar que el mtodo a e es llamado. El test quedar como sigue: a

1 2 3 4 5 6 7 8 9 10 11 12 13 14

11.23: ut supercalculadora.py
def t e s t _ v a l i d a d o r _ e x p r e s i o n _ i n v a l i d a _ s t u b ( self ): validador_stub = v a l i d a d o r _ e x p r _ a r i t m e t i c a . V a l i d a d o r E x p r A r i t m e t i c a () validar_mock = mox . Mox () validar_mock . StubOutWithMock ( validador_stub , valida r ) validador_stub . validar ( " 2 ^ 3 " ). AndReturn ( False ) validar_mock . ReplayAll () sc = supercalculadora . Supercalculadora ( exp_aritmetica . ExprAritmetica () , validador_stub ) self . failUnlessRaises ( SyntaxError , sc . calcular , " 2 ^ 3 " ) validar_mock . UnsetStubs () validar_mock . VerifyAll ()

...

Esto falla puesto que el Supercalculadora slo recibe un parmetro y o a no dos en su constructor y tambin porque no tenemos ni siquiera un esquee leto de la clase Validador. Pongmonos con las implementacin corrigiendo a o poco a poco los errores.
5 6

Ver Cap tulo 6 en la pgina 95 a http://code.google.com/p/pymox/

280

Cap tulo 11 11.24: supercalculadora.py

1 2 3 4 5 6 7

... class Supercalculadora : def __init__ ( self , parser , validador ): self . calc = calculadora . Calculadora () self . parser = parser self . validador = validador ...


1 2 3

11.25: validador expr aritmetica.py

class V a l i d a d o r E x p r A r i t m e t i c a : def validar ( self , expresion ): True

Por ultimo, en ut supercalculadora.py tenemos que importar el nuevo chero de validacin. o En este punto vemos que la prueba an falla porque la excepcin por u o validacin no se produce. Adems, vemos que las dems pruebas se han roto o a a ya que hemos cambiado el constructor (hemos aadido el validador como un n nuevo argumento). Corrijamos primero el error de la ultima prueba y despus e pasemos a corregir (mejor dicho a actualizar) todas las dems. a

1 2 3 4 5 6 7

11.26: supercalculadora.py
def calcular ( self , expresion ): if not self . validador . validar ( expresion ): raise SyntaxError ( " La expresion no es valida " ) return str ( self . __simplificar__ ( self . parser . parse ( expresion ))[ operandos ][0])

...

La prueba pasa ahora as que pongmonos a corregir las otras pruebas en a el paso de refactorizacin. Como la llamada al constructor est en el setUp o a y este es llamado por todas las pruebas, solamente tenemos que cambiar el constructor aqu y todas las pruebas pasarn ya que vamos a utilizar la a implementacin real que siempre devuelve True. o

1 2 3 4 5 6 7 8 9 10 11 12

11.27: ut supercalculadora.py
mox unittest v a l i d a d o r _ e x p r _ a r i t m e t i c a as validador exp_aritmetica supercalculadora

import import import import import

class T e s t s S u p e r c a l c u l a d o r a ( unittest . TestCase ): def setUp ( self ): self . sc = supercalculadora . Supercalculadora ( exp_aritmetica . ExprAritmetica () , validador . V a l i d a d o r E x p r A r i t m e t i c a ())

281

Cap tulo 11
def tearDown ( self ): pass def test_sumar ( self ): self . failUnlessEqual ( " 4 " , self . sc . calcular ( " 2 + 2 " )) def test_restar ( self ): self . failUnlessEqual ( " 0 " , self . sc . calcular ( " 2 - 2 " )) def t e s t _ e x p r e s i o n _ c o m p l e j a _ s i n _ p a r e n t e s i s _ s i n _ p r e c e d e n c i a ( self ): self . failUnlessEqual ( " 6 " , self . sc . calcular ( " 5 + 4 - 3 " )) def t e s t _ e x p r e s i o n _ c o m p l e j a _ s i n _ p a r e n t e s i s _ c o n _ p r e c e d e n c i a ( self ): self . failUnlessEqual ( " 3 " , self . sc . calcular ( " 5 + 4 / 2 - 4 " )) self . failUnlessEqual ( " -1 " , self . sc . calcular ( " 4 / 2 - 3 " )) self . failUnlessEqual ( " 1 " , self . sc . calcular ( " 4 / 2 - 3 + 1 + 6 / 3 - 1 " )) self . failUnlessEqual ( " -8 " , self . sc . calcular ( " 4 / -2 + 3 + -1 + -6 / -3 - 10 " )) self . failUnlessEqual ( " 9 " , self . sc . calcular ( " 5 + 4 * 2 / 2 " )) def t e s t _ e x p r _ c o m p l e j a _ t o d a s _ o p e r a c i o n e s _ s i n _ p a r e n t e s i s ( self ): self . failUnlessEqual ( " 11 " , self . sc . calcular ( " 4 - -3 * 2 / 3 + 5 " ))

13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

Ahora es el momento de actualizar la libreta y hacer un nuevo commit al repositorio. S digo nuevo porque durante este ejercicio deber , amos haber subido al repositorio el cdigo (y las pruebas, que no olvidemos son parte o del cdigo) frecuentemente, por ejemplo cada vez que ten o amos una nueva funcionalidad en verde despus de unas cuantas pruebas. e Para claricar todo el cdigo, una vez ms, aqu estn todos los cheros o a a que hemos creado (menos las ya mencionados ut supercalculadora.py y validador expr aritmetica.py).

1 2 3 4 5 6 7 8 9 10 11 12 13 14

11.28: ut main.py
unittest ut_calculadora ut_supercalculadora ut_expr_aritmetica

import import import import

if __name__ == " __main__ " : suite = unittest . TestSuite () suite . addTest ( unittest . makeSuite ( ut_calculadora . TestsCalculadora )) suite . addTest ( unittest . makeSuite ( ut_expr_aritmetica . TestsExprAritmetica )) suite . addTest ( unittest . makeSuite ( ut_supercalculadora . T e s t s S u p e r c a l c u l a d o r a )) unittest . TextTestRunner ( verbosity =3). run ( suite )

11.29: ut calculadora.py

import unittest

282

Cap tulo 11
import calculadora class TestsCalculadora ( unittest . TestCase ): def setUp ( self ): self . calc = calculadora . Calculadora () def tearDown ( self ): pass def t e s t _ s u m a r _ n u m e r o s _ i g u a l e s ( self ): self . failUnlessEqual (4 , self . calc . sumar (2 , 2)) def t e s t _ s u m a r _ n u m e r o s _ d i s t i n t o s ( self ): self . failUnlessEqual (12 , self . calc . sumar (5 , 7)) def t e s t _ s u m a r _ p r o p i e d a d _ c o n m u t a t i v a ( self ): self . failUnlessEqual ( self . calc . sumar (5 , 7) , self . calc . sumar (7 , 5)) def t e s t _ s u m a r _ n u m e r o s _ n e g a t i v o s ( self ): self . failUnlessEqual (0 , self . calc . sumar (2 , -2)) def t e s t _ r e s t a _ p o s i t i v a _ n u m e r o s _ d i s t i n t o s ( self ): self . failUnlessEqual (2 , self . calc . restar (5 , 3)) def t e s t _ r e s t a _ n e g a t i v a _ n u m e r o s _ d i s t i n t o s ( self ): self . failUnlessEqual ( -1 , self . calc . restar (2 , 3)) def t e s t _ r e s t a r _ n u m e r o s _ n e g a t i v o s ( self ): self . failUnlessEqual ( -7 , self . calc . restar ( -5 , 2)) self . failUnlessEqual ( -5 , self . calc . restar ( -7 , -2)) def t e s t _ r e s t a r _ n o _ p r o p i e d a d _ c o n m u t a t i v a ( self ): self . failIfEqual ( self . calc . restar (5 , 3) , self . calc . restar (3 , 5)) def t e s t _ d i v i s i o n _ e x a c t a ( self ): self . failUnlessEqual (1 , self . calc . dividir (2 , 2)) self . failUnlessEqual (2 , self . calc . dividir (10 , 5)) def t e s t _ d i v i s i o n _ e x a c t a _ n u m e r o s _ n e g a t i v o s ( self ): self . failUnlessEqual ( -2 , self . calc . dividir (10 , -5)) self . failUnlessEqual (2 , self . calc . dividir ( -10 , -5)) def t e s t _ d i v i s i o n _ n o _ e n t e r a _ d a _ e x c e p c i o n ( self ): self . failUnlessRaises ( ValueError , self . calc . dividir , 3 , 2) def test_division_por_0 ( self ): self . failUnlessRaises ( ZeroDivisionError , self . calc . dividir , 3 , 0) def t e s t _ m u l t i p l i c a r _ s i m p l e ( self ): self . failUnlessEqual (8 , self . calc . multiplicar (4 , 2)) def t e s t _ m u l t i p l i c a r _ n e g a t i v a ( self ): self . failUnlessEqual ( -8 , self . calc . multiplicar ( -4 , 2)) self . failUnlessEqual ( -8 , self . calc . multiplicar (4 , -2)) self . failUnlessEqual (8 , self . calc . multiplicar ( -4 , -2))

2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60

283

Cap tulo 11


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

11.30: ut expr aritmetica.py

import unittest import expr_aritmetica class TestsExprAritmetica ( unittest . TestCase ): def setUp ( self ): self . expresion = expr_aritmetica . ExprAritmetica () def tearDown ( self ): pass def t e s t _ e x t r a e r _ o p e r a n d o s _ y _ o p e r a d o r e s _ e n _ 2 _ m a s _ 2 ( self ): self . failUnlessEqual ({ operandos : [2 , 2] , operadores : [ + ]} , self . expresion . parse ( " 2 + 2 " )) def t e s t _ e x t r a e r _ o p e r a n d o s _ y _ o p e r a d o r e s _ e x p r _ s i n _ p t e s i s ( self ): self . failUnlessEqual ({ operandos : [5 , 4 , 2 , 2] , operadores : [ + , * , / ]} , self . expresion . parse ( " 5 + 4 * 2 / 2 " ))


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

11.31: supercalculadora.py

import exp_aritmetica import calculadora class Supercalculadora : def __init__ ( self , parser , validador ): self . calc = calculadora . Calculadora () self . parser = parser self . validador = validador def __operar__ ( self , expr_descompuesta ): i = None res_intermedio = 0 if * in expr_descompuesta [ operadores ]: i = expr_descompuesta [ operadores ]. index ( * ) res_intermedio = self . calc . multiplicar ( expr_descompuesta [ operandos ][ i ] , expr_descompuesta [ operandos ][ i + elif / in expr_descompuesta [ operadores ]: i = expr_descompuesta [ operadores ]. index ( / ) res_intermedio = self . calc . dividir ( expr_descompuesta [ operandos ][ i ] , expr_descompuesta [ operandos ][ i + elif - in expr_descompuesta [ operadores ]: i = expr_descompuesta [ operadores ]. index ( - ) res_intermedio = self . calc . restar ( expr_descompuesta [ operandos ][ i ] , expr_descompuesta [ operandos ][ i + elif + in expr_descompuesta [ operadores ]: i = expr_descompuesta [ operadores ]. index ( + ) res_intermedio = self . calc . sumar ( expr_descompuesta [ operandos ][ i ] , expr_descompuesta [ operandos ][ i +

1])

1])

1])

1])

284

Cap tulo 11
else : # Es un error , tenemos que decidir que hacer en los test # siguientes # Forzamos el error para que no haya problemas luego assert False return (i , res_intermedio )

33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61

def __simplificar__ ( self , expr_descompuesta ): if expr_descompuesta [ operadores ] == []: return expr_descompuesta (i , res_intermedio ) = self . __operar__ ( expr_descompuest a ) expr_simplificada = expr_descompuesta expr_simplificada [ operadores ]. pop ( i ) expr_simplificada [ operandos ]. pop ( i ) expr_simplificada [ operandos ]. pop ( i ) expr_simplificada [ operandos ]. insert (i , res_interme dio ) return self . __simplificar__ ( expr_simplificada ) def calcular ( self , expresion ): if not self . validador . validar ( expresion ): raise SyntaxError ( " La expresion no es valida " ) return str ( self . __simplificar__ ( self . parser . parse ( expresion ))[ operandos ][0])


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

11.32: calculadora.py

class Calculadora : def sumar ( self , a , b ): return a + b def restar ( self , a , b ): return a - b def multiplicar ( self , a , b ): return a * b def dividir ( self , a , b ): if a % b != 0: raise ValueError else : return a / b


1 2 3 4 5 6 7

11.33: expr aritmetica.py

import string class ExprAritmetica : def __es_numero__ ( self , cadena ): try : string . atoi ( cadena ) return True

285

Cap tulo 11
except ValueError : return False def parse ( self , exp ): operandos = [] operadores = [] tokens = exp . split () for token in tokens : if self . __es_numero__ ( token ): operandos . append ( string . atoi ( token )) else : operadores . append ( token ) return { operandos : operandos , operadores : operadore s }

8 9 10 11 12 13 14 15 16 17 18 19 20

Lo ultimo antes de acabar este cap tulo, la salida despus de correr todos e los tests:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

test_division_exacta ( ut_calculadora . TestsCalculadora ) ... ok test_division_exacta_numeros_negativos ( ut_calculadora . TestsCalculadora ) ... ok test_division_no_entera_da_excepcion ( ut_calculadora . TestsCalculadora ) ... ok test_division_por_0 ( ut_calculadora . TestsCalculadora ) ... ok test_multiplicar_negativa ( ut_calculadora . TestsCalculadora ) ... ok test_multiplicar_simple ( ut_calculadora . TestsCalculadora ) ... ok test_resta_negativa_numeros_distintos ( ut_calculadora . TestsCalculadora ) ... ok test_resta_positiva_numeros_distintos ( ut_calculadora . TestsCalculadora ) ... ok test_restar_no_propiedad_conmutativa ( ut_calculadora . TestsCalculadora ) ... ok test_restar_numeros_negativos ( ut_calculadora . TestsCalculadora ) ... ok test_sumar_numeros_distintos ( ut_calculadora . TestsCalculadora ) ... ok test_sumar_numeros_iguales ( ut_calculadora . TestsCalculadora ) ... ok test_sumar_numeros_negativos ( ut_calculadora . TestsCalculadora ) ... ok test_sumar_propiedad_conmutativa ( ut_calculadora . TestsCalculadora ) ... ok test_extraer_operandos_y_operadores_en_2_mas_2 ( ut_exp_aritmetica . TestsExpAritmetica ) ... ok test_extraer_operandos_y_operadores_expr_sin_ptesis ( ut_exp_aritmetica . TestsExpAritmetica ) ... ok test_expresion_compleja_sin_parentesis_con_precedencia ( ut_supercalculadora . T e s t s S u p e r c a l c u l a d o r a ) ... ok test_expresion_compleja_sin_parentesis_sin_precedencia ( ut_supercalculadora . T e s t s S u p e r c a l c u l a d o r a ) ... ok test_expr_compleja_todas_operaciones_sin_parentesis ( ut_supercalculadora . T e s t s S u p e r c a l c u l a d o r a ) ... ok test_restar ( ut_supercalculadora . T e s t s S u p e r c a l c u l a d o r a ) ... ok test_sumar

286

Cap tulo 11
( ut_supercalculadora . T e s t s S u p e r c a l c u l a d o r a ) ... ok test_validador_expresion_invalida_mock ( ut_supercalculadora . T e s t s S u p e r c a l c u l a d o r a ) ... ok ------------------------------------------------------------------Ran 22 tests in 0.000 s OK

42 43 44 45 46 47 48 49

En este punto, tenemos una calculadora que hace todas las operaciones y lanza excepciones si hay error como hab amos diseado. Adems, tenemos n a una funcionalidad bsica para manejar expresiones, por ahora, sin parntesis a e y est preparada para seguir con el validador de expresiones. a Merece la pena mencionar que aunque la calculadora sea capaz de lanzar excepciones en caso de error y de que tengamos test unitarios para ello, no hemos hecho lo mismo a nivel de aplicacin (para Supercalculadora). Es o decir, ahora mismo la clase principal no sabe qu hacer (aunque deber mose a trar un error) si la Calculadora, por ejemplo, lanzase una excepcin. Esto o hay que tratarlo con nuevos tests unitarios a nivel de Supercalculadora y con el test de aceptacin que tenemos para este caso. o Todav hay mucho trabajo que hacer para completar la Supercalculaa dora, muchas ms pruebas y casos. El diseo cambiar cuando haya ms a n a a pruebas (podemos intuir que expresin aritmtica tendr que usar un parser o e a de verdad y quiz un tokenizer, etctera) y lo que tengamos ser, proa e a bablemente, bastante distinto a lo que hemos llegado ahora. Sin embargo, esperamos que este ejemplo haya mostrado como hacer TDD para crear la aplicacin y le haya ayudado a entender como usar TDD con Python. o

287

Cap tulo 11

288

Captulo

12

Antipatrones y Errores comunes


Hay una amplia gama de antipatrones en que podemos incurrir cuando estamos practicando TDD. Este cap tulo no pretende cubrirlos todos ni mucho menos, sino dar un pequeo repaso a los ms comunes. Antes de pasar n a a ver antipatrones, es interesante citar los errores t picos que cometemos cuando empezamos a practicar TDD por primera vez.

Errores del principiante


El nombre del test no es sucientemente descriptivo Recordemos que el nombre de un mtodo y de sus parmetros son su e a mejor documentacin. En el caso de un test, su nombre debe expresar con o total claridad la intencin del mismo. o No sabemos qu es lo que queremos que haga el SUT e Nos hemos lanzado a escribir un test pero no sabemos en realidad qu es e lo que el cdigo bajo prueba tiene que hacer. En algunas ocasiones, lo resolo vemos hablando con el dueo de producto y, en otras, hablando con otros n desarrolladores. Tenga en cuenta que est tomando decisiones de diseo al a n escribir el test y que la programacin por parejas o las revisiones de cdigo o o nos ayudan en la toma de decisiones. No sabemos quin es el SUT y quin es el colaborador e e En los tests de validacin de interaccin, pensamos que el colaborador, o o aquel que representamos mediante un doble, es el SUT. Antes de utilizar dobles de prueba tenemos que estar completamente seguros de quin es e 289

Cap tulo 12 el SUT y quin es el colaborador y qu es lo que queremos comprobar. En e e general, comprobamos que el SUT habla con el colaborador, o bien le decimos al colaborador que, si le hablan, responda algo que le decimos. Un mismo mtodo de test est haciendo m ltiples armaciones e a u Cuando practicamos TDD correctamente, apenas tenemos que usar el depurador. Cuando un test falla, lo encontramos directamente y lo corregimos en dos minutos. Para que esto sea as cada mtodo debe probar una unica , e funcionalidad del SUT. A veces utilizamos varias armaciones (asserts) en el mismo test, pero slo si giran en torno a la misma funcionalidad. Un mtodo o e de test raramente excede las 10 l neas de cdigo. o Los test unitarios no estn separados de los de integracin a o Los tests unitarios se ejecutan frecuentemente. De hecho, se ejecutan continuamente cuando practicamos TDD. As que tenemos que conseguir que se ejecuten en menos de un segundo. Esta es la razn fundamental para o tenerlos separados de los tests de integracin o Rpido, Inocuo, Atmico, Independiente a o Si rompe alguna de sus propiedades, entonces no es un test unitario. Pregntese si sus tests cumplen las reglas y, en caso de que no sea as piense u , si con un poco ms de esfuerzo puede conseguir que lo hagan. a Se nos olvida refactorizar No slo por tener una gran bater de tests, el cdigo ya es ms fcil o a o a a de mantener. Si el cdigo no est limpio, ser muy costoso modicarlo, y o a a tambin sus tests. No olvide buscar y corregir cdigo duplicado despus de e o e hacer pasar cada test. El cdigo de los tests debe estar tan limpio como el o cdigo de produccin. o o Confundir un mock con un stub Cuando queremos que un objeto falso devuelva una respuesta programada en caso de que se le llame, usamos un stub. Cuando queremos conrmar que efectivamente la llamada a un mtodo se produce, usamos un mock. Un e mock es ms restrictivo que un stub y puede aumentar la fragilidad del test. a No obstante, en muchas ocasiones la mejor solucin pasa por usar un mock1 . o
1

Revise el Cap tulo 6 en la pgina 95 para ms informacin a a o

290

Cap tulo 12 No eliminamos cdigo muerto o A veces, tras cambios en la lgica de negocio, queda cdigo en desuso. o o Puede ser cdigo de produccin o pueden ser tests. Puesto que disponemos o o de un sistema de control de versiones que nos permite volver atrs si alguna a vez volviese a hacer falta el cdigo, debemos eliminarlo de la versin en o o produccin. El cdigo muerto induce a errores antes o despus. Se suele o o e menospreciar cuando se trata de tests pero, como ha visto, el cdigo de los o tests es tan importante como el cdigo que prueban. o

Antipatrones
James Carr2 recopil una lista de antipatrones ayudado por la comunidad o TDD que traduje en mi blog3 y que ahora aado a esta seccin. Los nombres n o que les pusieron tienen un carcter cmico y no son en absoluto ociales pero a o su contenido dice mucho. Algunos de ellos ya estn recogidos en los errores a comentados arriba. El Mentiroso Un test completo que cumple todas sus armaciones (asserts) y parece ser vlido pero que cuando se inspecciona ms de cerca, muestra que realmente a a no est probando su cometido en absoluto. a Setup Excesivo Es un test que requiere un montn de trabajo para ser congurado. A o veces se usan varios cientos de l neas de cdigo para congurar el entorno de o dicho test, con varios objetos involucrados, lo cual nos impide saber qu es e lo que se est probando debido a tanto ruido. a El Gigante Aunque prueba correctamente el objeto en cuestin, puede contener miles o de l neas y probar much simos casos de uso. Esto puede ser un indicador de que el sistema que estamos probando es un Objeto Dios4
http://blog.james-carr.org La traduccin ha sido mejorada para el libro porque en el blog est bastante mal o a 4 http://en.wikipedia.org/wiki/God object
3 2

291

Cap tulo 12 El Imitador A veces, usar mocks puede estar bien y ser prctico pero otras, el desaa rrollador se puede perder imitando los objetos colaboradores. En este caso un test contiene tantos mocks, stubs y/o falsicaciones, que el SUT ni siquiera se est probando. En su lugar estamos probando lo que los mocks estn a a devolviendo. El Inspector Viola la encapsulacin en un intento de conseguir el 100 % de cobertura o de cdigo y por ello sabe tanto del objeto a prueba que, cualquier intento de o refactorizarlo, rompe el test. Sobras Abundantes Es el caso en que un test crea datos que se guardan en algn lugar y u otro test los reutiliza para sus propios nes. Si el generador de los datos se ejecuta despus, o no se llega a ejecutar, el test que usa esos datos falla e por completo. El Hroe Local e Depende del entorno de desarrollo espec co en que fue escrito para poder ejecutarse. El resultado es que el test pasa en dicho entorno pero falla en cualquier otro sitio. Un ejemplo t pico es poner rutas que son espec cas de una persona, como una referencia a un chero en su escritorio. El Cotilla Quisquilloso Compara la salida completa de la funcin que se prueba, cuando en realio dad slo est interesado en pequeas partes de ella. Esto se traduce en que el o a n test tiene que ser continuamente mantenido a pesar de que los cambios sean insignicantes. Este es endmico de los tests de aplicaciones web. Ejemplo, e comparar todo un html de salida cuando solo se necesita saber si el title es correcto. El Cazador Secreto A primera vista parece no estar haciendo ninguna prueba por falta de armaciones (asserts). El test est en verdad conando en que se lanzar una a a excepcin en caso de que ocurra algn accidente desafortunado y que el o u framework de tests la capturar reportando el fracaso. a 292

Cap tulo 12 El Escaqueado Un test que hace muchas pruebas sobre efectos colaterales (presumiblemente fciles de hacer) pero que nunca prueba el autntico comportamiento a e deseado. A veces puede encontrarse en tests de acceso a base de datos, donde el mtodo a prueba se llama, despus el test selecciona datos de la base e e de datos y hace armaciones sobre el resultado. En lugar de comprobar que el mtodo hace lo que debe, se est comprobando que dicho mtodo no e a e alter ciertos datos o, lo que es lo mismo, que no caus daos. o o n El Bocazas Un test o bater de tests que llenan la consola con mensajes de diagnstia o co, de log, de depuracin, y dems forraje, incluso cuando los tests pasan. A o a veces, durante la creacin de un test, es necesario mostrar salida por pantalla, o y lo que ocurre en este caso es que, cuando se termina, se deja ah aunque ya no haga falta, en lugar de limpiarlo. El Cazador Hambriento Captura excepciones y no tiene en cuenta sus trazas, a veces reemplazndolas con un mensaje menos informativo, pero otras incluso registrando a el suceso en un log y dejando el test pasar. El Secuenciador Un test unitario que depende de que aparezcan, en el mismo orden, elementos de una lista sin ordenar. Dependencia Oculta Un primo hermano del Hroe Local, un test que requiere que existan e ciertos datos en alguna parte antes de correr. Si los datos no se rellenaron, el test falla sin dejar apenas explicacin, forzando al desarrollador a indagar o por acres de cdigo para encontrar qu datos se supon que deb haber. o e a a El Enumerador Una bater de tests donde cada test es simplemente un nombre seguido a de un nmero, ej, test1, test2, test3. Esto supone que la misin del test no u o queda clara y la unica forma de averiguarlo es leer todo el test y rezar para que el cdigo sea claro. o

293

Cap tulo 12 El Extra o n Un test que ni siquiera pertenece a la clase de la cual es parte. Est en a realidad probando otro objeto (X), muy probablemente usado por el que se est probando en la clase actual (objeto Y), pero saltndose la interaccin a a o que hay entre ambos, donde el objecto X deb funcionar en base a la salida a de Y, y no directamente. Tambin conocido como La Distancia Relativa. e El Evangelista de los Sistemas Operativos Conf en que un sistema operativo espec a co se est usando para ejecua tarse. Un buen ejemplo ser un test que usa la secuencia de nueva l a nea de Windows en la armacin (assert), rompindose cuando corre bajo Linux. o e El que Siempre Funciona Se escribi para pasar en lugar de para fallar primero. Como desafortunado o efecto colateral, sucede que el test siempre funciona, aunque debiese fallar. El Libre Albedr o En lugar de escribir un nuevo test unitario para probar una nueva funcionalidad, se aade una nueva armacin (assert) dentro de un test existente. n o El Unico Una combinacin de varios antipatrones, particularmente El Libre Alo bedr y El Gigante. Es un slo test unitario que contiene el conjunto entero o o de pruebas de toda la funcionalidad que tiene un objeto. Una indicacin o comn de eso es que el test tiene el mismo nombre que su clase y contiene u mltiples l u neas de setup y armaciones. El Macho Chilln o Debido a recursos compartidos puede ver los datos resultantes de otro test y puede hacerlo fallar incluso aunque el sistema a prueba sea perfectamente vlido. Esto se ha visto comnmente en tnesse, donde el uso de a u variables de clase estticas, usadas para guardar colecciones, no se limpiaban a adecuadamente despus de la ejecucin, a menudo repercutiendo de manera e o inesperada en otros tests. Tambin conocido como El husped no invitado. e e

294

Cap tulo 12 El Escabador Lento Un test que se ejecuta de una forma incre blemente lenta. Cuando los desarrolladores lo lanzan, les da tiempo a ir al servicio, tomar caf, o peor, e dejarlo corriendo y marcharse a casa al terminar el d a. Notas nales Por ultimo yo aadir como antipatrn el hecho de ponerle comentarios a n a o un test. Para m si un test necesita comentarios, es que no est bien escrito. , a No se tome a pecho cada uno de los patrones ni los errores. Como en tantas otras reas, las reglas tienen sus excepciones. El objetivo es que le a sirvan para identicar malos olores en su prctica con TDD. a

295

Cap tulo 12

296

Apendice

Integracin Continua (CI) o


A.1. Introduccin o

Qu es la integracin continua? e o
En palabras de Martin Fowler, entendemos la integracin continua como: o Una prctica del desarrollo de software donde los miembros del equipo intea gran su trabajo con frecuencia: normalmente, cada persona integra de forma diaria, conduciendo a mltiples integraciones por d Cada integracin es u a. o comprobada por una construccin automtica (incluyendo las pruebas) para o a detectar errores de integracin tan rpido como sea posible. Muchos equipos o a encuentran que este enfoque conduce a la reduccin signicativa de probleo mas de integracin y permite a un equipo desarrollar software cohesivo ms o a rpidamente a Muchos asocian la integracin continua (utilizar IC para referirme al o e trmino en adelante) con el uso de herramientas como CruiseControl1 o e Hudson2 , sin embargo, la IC puede practicarse sin el uso de estas, aunque con una mayor disciplina. En otras palabras, IC es mucho ms que la utilizacin a o de una herramienta. En algunos proyectos, la integracin se lleva a cabo como un evento (cada o lunes integramos nuestro cdigo...), la prctica de la IC elimina esta forma o a de ver la integracin, ya que forma parte de nuestro trabajo diario. o La IC encaja muy bien con prcticas como TDD dado que se centran a en disponer de una buena bater de pruebas y en evolucionar el cdigo a o realizando pequeos cambios a cada vez. Aunque, en realidad, la metodolog n a de desarrollo no es determinante siempre y cuando se cumplan una serie de
1 2

http://cruisecontrol.sourceforge.net/ http://hudson-ci.org/

297

A.1. Introduccin o

Integracin Continua (CI) o

buenas prcticas. a La IC es independiente del tipo de metodolog de gestin (gil o prea o a dictiva), sin embargo, por los benecios que aporta, proporciona gran valor a las metodolog giles, ayudando a tener un producto funcional en todo as a momento. Los benecios de hacer IC son varios, sin embargo, se entendern y aprea ciarn mejor una vez que conozcamos las prcticas que conllevan. a a

Conceptos
Construccin (Build): una construccin implica algo ms que compilar, o o a podr consistir en compilar, ejecutar pruebas, usar herramientas de anlisis a a de cdigo3 , desplegar... entre otras cosas. Un build puede ser entendido como o el proceso de convertir el cdigo fuente en software que funcione. o Scripts de construccin (build script) : se trata de un conjunto de o scripts que son utilizados para compilar, testear, realizar anlisis del cdigo a o o desplegar software. Podemos tener scripts de construcciones sin tener que implementar IC, sin embargo, para practicar IC son vitales.

Empezando con IC
Ya se conoce de las bondades de un SCV (Sistema de Control de Versiones) para nuestro cdigo fuente. La verdad, es que es dif pensar en o cil proyectos que no utilicen alguna herramienta de este tipo. Para realizar IC tambin es vital disponer de un repositorio centralizado cuya localizacin sea e o conocida por los miembros del equipo. Ser nuestra base para realizar las a construcciones cada vez que un desarrollador suba sus cambios. El repositorio debe contener todo lo necesario para que nuestro proyecto pueda ser construido de forma automtica, ya sean scripts, librer de a as terceros, cheros de conguracin, etc. o

Cmo es la vida con integracin continua? o o


Imaginemos que decidimos agregar una pequea funcionalidad a nuestro n software. Comenzamos descargando el cdigo actual del repositorio a nueso tra mquina local, a esta copia la llamaremos copia local o working copy. En a nuestra copia local, aadimos o modicamos el cdigo necesario para rean o lizar la funcionalidad elegida (no nos olvidemos de las pruebas asociadas). A continuacin, debemos realizar una construccin de manera automtica, o o a esto podr consistir en compilar y ejecutar una bater de pruebas. Si hemos a a tenido xito, lo siguiente que pensaremos es que ya estamos listos para subir e
3

http://pmd.sourceforge.net/

298

Integracin Continua (CI) o

A.2. Prcticas de integracin continua a o

nuestros cambios al repositorio, sin embargo, otros desarrolladores han podido subir sus cambios mientras nosotros realizbamos nuestra tarea. Por lo a tanto debemos bajarnos los cambios del repositorio, resolver los conictos si los hubiera y lanzar de nuevo la construccin automtica para vericar que o a todo ha sido integrado correctamente. Finalmente, podemos subir nuestros cambios al repositorio. Pero nuestro trabajo no acaba aqu Debemos construir una ultima vez . pero, en este caso, en una mquina de integracin basndonos en el cdigo a o a o actual del repositorio (con nuestra nueva funcionalidad). Entre otras cosas, podr haber ocurrido que nos hayamos olvidado de subir un chero y el a repositorio no haya sido actualizado correctamente (este problema no es extrao). Si todo ha ido bien en la mquina de integracin, hemos acabado n a o nuestro trabajo. En caso contrario, debemos arreglar tan pronto como sea posible los problemas que hayamos podido ocasionar en el repositorio. De esta forma, disponemos de una base estable en el repositorio del cual cada desarrollador partir para realizar su trabajo diario. a Llegados a este punto, es posible que piense que es un proceso muy latoso. No obstante, para su tranquilidad, comentar que esta ultima parte en la e mquina de integracin podr ser ejecutada automticamente por un servidor a o a a de IC como CruiseControl, Hudson, etc, al detectar que ha habido cambios en el repositorio. No se desespere, la integracin continua promueve automatizar o todo el proceso lo mximo posible para aumentar nuestra productividad. Ms a a adelante entraremos en detalle, ahora he querido simplicar el proceso sin nuevos conceptos. Ya tenemos lo bsico para empezar a trabajar con IC. Nuestro proyecto en a un repositorio centralizado, toda la informacin necesaria en l para construir o e el proyecto y unas nociones sobre la manera de trabajar. Pero esto es slo la o punta del iceberg, a continuacin se pasar a detallar una serie de prcticas o a a para realizar IC de forma efectiva.

A.2.
A.2.1.

Prcticas de integracin continua a o


Automatizar la construccin o

Nuestro trabajo, en parte, consiste en automatizar procesos para nuestros queridos usuarios. Sin embargo, a veces nos olvidamos de que una parte de nuestras tareas podr ser automatizadas, concretamente, las tareas necean sarias para obtener nuestro software a partir de ese montn de cdigo fuente. o o Pero, cuando hablamos de construir nuestro software a partir de lo que existe en el repositorio, a que nos referimos?. Construir signica mucho ms a que compilar, nuestra construccin podr descargar las ultimas fuentes del o a 299

A.2. Prcticas de integracin continua a o

Integracin Continua (CI) o

trunk4 , compilar, ejecutar pruebas automticas, generar documentacin o un a o esquema de base de datos si interaccionamos con un SGBD5 , etc. Iniciar una construccin deber ser tan fcil para cualquier desarrollador como lanzar o a a un unico comando desde una consola: freyes@dev:/home/project$build now Existen herramientas de scripts de construccin libres que nos facilitan la o labor, muchas son usadas en diversos proyectos open-source. Algunas de las ms conocidas son Ant6 , NAnt7 , Maven8 , MSBuild9 , Rake10 , etc. a Buenas prcticas a Divide el script de construccin en diferentes comandos para que cualo quiera pueda lanzar una parte de forma aislada (por ejemplo, lanzar las pruebas), sin que pierda tiempo en realizar el proceso completamente. Normalmente, algunos desarrolladores usan un IDE para la construccin, ests herramientas son de gran utilidad para nuestra productivio a dad pero es esencial poder construir nuestro software sin IDE alguno. Nos deben facilitar la vida pero no debemos caer en la dependencia absoluta. El proceso de construccin deber ir tan rpido como se pueda. Nadie o a a lanzar el build para comprobar que la integracin ha ido correctamena o te si la construccin es lenta. Dedica algo de tiempo para que sea lo o ms eciente posible. Sin embargo, no todo es posible en el mundo a realT M ... En este caso, podr amos plantearnos dividir la construccin o en varias etapas. Una de las etapas, de cara al trabajo de cada desarrollador, podr compilar y ejecutar las pruebas unitarias. Las etapas que a llevaran ms tiempo podr ser lanzadas unicamente en la mquina a an a de integracin (algunas incluso de forma paralela). o

A.2.2.

Los test forman parte de la construccin o

Ya se ha hecho hincapi, aunque sea de manera indirecta, en que los tests e forman parte de la construccin. Sin embargo, no est de ms rearmar la o a a
http://svnbook.red-bean.com/nightly/en/svn-book.html#svn.branchmerge.maint.layout http://es.wikipedia.org/wiki/Sistema de gesti %C3 %B3n de bases de datos 6 http://ant.apache.org/ 7 http://nant.sourceforge.net/ 8 http://maven.apache.org/ 9 http://msdn.microsoft.com/en-us/library/0k6kkbsd.aspx 10 http://rake.rubyforge.org/
5 4

300

Integracin Continua (CI) o

A.2. Prcticas de integracin continua a o

importancia de estos en el proceso. Es muy dif tener una larga bater de cil a test que prueben todas las partes del proyecto (100 % de cobertura) o que todas estas sean perfectas. Pero, como bien dice Martin Fowler: Pruebas imperfectas, que corren frecuentemente, son mucho mejores que pruebas perfectas que nunca se han escrito. Aunque esto no supone que debamos dejar de mejorar nuestras habilidades para desarrollar pruebas de mejor calidad. Es necesario automatizar la ejecucin de las pruebas para que formen o parte de la construccin. Tambin es vital que el tiempo de ejecucin de o e o las pruebas sea corto (tanto en la mquina del desarrollador, como en la a mquina de integracin). Adems, si se produce una larga demora noticando a o a a las partes interesadas sobre lo que ha ocurrido en la construccin y los o desarrolladores se centran en otras actividades, se pierde uno de los principales benecios de la IC. Tener pruebas que se ejecutan rpidamente es lo preferible pero no siema pre podemos conseguirlo. Existen diferentes tipos de pruebas (unitarias, integracin, sistema...) y todas son importantes para nuestro software pero el o tiempo de ejecucin suele ser ms largo en unas que en otras (como podr o a a ser el caso de las pruebas de sistemas). Llegados a este punto, si nuestra construccin se demora bastante (XP recomienda 10 minutos) podr o amos ejecutar slo las pruebas ms rpidas (como suelen ser las unitarias) cada o a a vez que el repositorio sea modicado y lanzar las restantes a intervalos. Uno de los benecios de la IC es que reduce los riesgos cuando llevemos nuestro sistema al entorno de produccin pero, para ello, es necesario que o las pruebas se ejecuten en un entorno lo ms parecido al de produccin. a o Cada diferencia es un riesgo ms que no podremos vericar hasta la fase a de instalacin en produccin. El uso de mquinas virtuales11 para congurar o o a estos entornos es una opcin bastante acertada. o Buenas prcticas a Estructura el proyecto por cada tipo de test (test/unit <-> test/integration <-> test/system) as podrs ejecutar un grupo de manera a independiente sin muchas complicaciones u otros cheros de conguracin o Escribe tambin pruebas para los defectos/bugs que encuentres e Realiza una comprobacin (assert) por test. Adems de dejar claro el o a objetivo del test, reducimos el ciclo para que los test pasen. Recuerda que si un assert falla, el test termina en ese punto sin dar informacin o de las siguientes comprobaciones.
11

http://es.wikipedia.org/wiki/M %C3 %A1quina virtual

301

A.2. Prcticas de integracin continua a o

Integracin Continua (CI) o

A.2.3.

Subir los cambios de manera frecuente

Es una de las prcticas principales de la IC (impl a cita en la denicin). Uno o de los benecios de realizar cambios frecuentemente en el repositorio es que nos fuerza a dividir nuestra tarea en otras ms pequeas, lo que proporciona a n una sensacin de avance. Otro de los benecios es que es ms fcil detectar o a a errores, puesto que en ese tiempo no habremos podido escribir mucho cdigo, o slo habr unos cuntos lugares donde el problema estar escondido. o a a a En IC hay una mxima que debe ser cumplida por todo el equipo cuando a se plantea subir cambios al repositorio: Nunca se debe subir cdigo que no o funciona. Esto puede ser desde cdigo que no compila hasta cdigo que no o o pasa las pruebas. Para prevenir que ocurra esto, el desarrollador debe realizar una construccin en su entorno local. Puede ser que ms adelante encono a tremos problemas en la mquina de integracin pero, seguir esta mxima, a o a produce un repositorio ms estable. a Tampoco deber amos partir de cdigo del repositorio cuya construccin o o ha fallado. Esto podr llevarnos a duplicar esfuerzos (varios desarrolladores a solucionando el mismo problema) o a pensar que nuestros cambios han sido lo que han provocado el problema, lo que supone una gran prdida de tiempo e hasta que lo descubramos. Es responsabilidad de quien haya roto la construccin del repositorio, o arreglarlo lo ms rpidamente posible. Para ello, no deber a a amos usar trucos para llevar a cabo nuestro objetivo, como eliminar o comentar el cdigo de o las pruebas que no pasan. Si el problema no es fcil de resolver y puede llevar a tiempo, quizs debamos plantearnos revertir los cambios en el respositorio a y solucionarlo tranquilamente en nuestro entorno local, de manera que no interramos en el trabajo del resto del equipo.

A.2.4.

Construir en una mquina de integracin a o

Cuando utilizamos una mquina de integracin donde realizamos las consa o trucciones a partir del repositorio, reducimos las suposiciones sobre el entorno y la conguracin y ayudamos a prevenir los problemas del tipo En o mi mquina funciona!12 . Hay 2 maneras de realizar esta tarea: ejecutar la a construccin automtica de forma manual o utilizar un servidor de IC. o a La operacin de forma manual es muy simple, el desarrollador que sube o los cambios al repositorio, advierte al equipo para que no haya interferencias, se dirige a la mquina de integracin y all realiza la construccin a partir del a o o repositorio. Usando un servidor de IC, cada vez que alguien sube sus cambios al
12

http://www.codinghorror.com/blog/archives/000818.html

302

Integracin Continua (CI) o

A.2. Prcticas de integracin continua a o

repositorio, se realiza la construccin de manera automtica, noticando del o a resultado del proceso (por e-mail, jabber, etc). A priori puede parecer que el enfoque manual es una prdida de tiempo, e 13 en contra del uso de servidores de sin embargo, existen buenos argumentos IC.

A.2.5.

Todo el mundo puede ver lo que est pasando a

Uno de los benecios de la integracin continua es que aporta claridad en o el proyecto, cualquiera puede ver el estado del repositorio ojeando la mquina a de integracin. Pero lo correcto ser que cualquiera, desde el lugar donde se o a encuentre, pueda ver el estado del proyecto. Aqu es donde marca la diferencia el uso de un servidor de integracin continua con respecto a realizar la inteo gracin de forma manual. Los servidores de IC, como por ejemplo Hudson, o aportan bastante informacin respecto a la evolucin del proyecto (cuntos o o a builds se han fallado, cuntos han pasado, grcos sobre la evolucin, etc) a a o y de los problemas concretos que ha producido cada cambio en el sistema. La mayor de los servidores de IC proporcionan plugins o extensiones que a enriquecen la informacin de nuestros proyectos (cobertura de las pruebas, o generacin automtica de documentacin a partir del cdigo fuente, etc). o a o o Adems, los servidores de IC son capaces de comunicar por diferentes mea canismos lo que est ocurriendo en el servidor. Entre los mecanismos menos a ostentosos se encuentra el env de emails o sms al mvil de los interesados, o o los ms frikies pueden hacer uso de lamparas de lava14 , semforos15 , ama a bient orb16 , emisin de sonidos en un altavoz con el resultado de la ultima o construccin, Nabaztag17 , etc. o Tambin proporciona gran valor que cada persona, independientemente e de su cargo o labor en el proyecto, pueda obtener el ultimo ejecutable y ser capaz de arrancarlo sin muchos suplicios. Aunque a muchos les pueda parecer arriesgado este enfoque, entre los benecios aportados se encuentran: Aumento del feedback a lo largo del proyecto entre todos los integrantes implicados en el mismo. Las buenas ideas diferencian nuestro software y las cr ticas constructivas nos hacen mejorar. Menor nmero de interrupciones para el equipo de desarrollo cada vez u que alguien (ajeno al desarrollo diario) nos pida ver nuestro software funcionando. Ah est, prubalo t mismo! a e u
13 14

http://jamesshore.com/Blog/Continuous-Integration-is-an-Attitude.html http://www.pragmaticautomation.com/cgi-bin/pragauto.cgi/Monitor/Devices/BubbleBubbleBu 15 http://wiki.hudson-ci.org/pages/viewpage.action?pageId=38633731 16 http://weblogs.java.net/blog/kohsuke/archive/2006/11/diyorb my own e.html 17 http://wiki.hudson-ci.org/display/HUDSON/Nabaztag+Plugin

303

A.3. IC para reducir riesgos

Integracin Continua (CI) o

Incremento del conocimiento sobre lo que hace nuestro software, qu fune cionalidades cubre en cada momento y qu carencias tiene, a expensas e de angustiosos y largos documentos de nalisis a

A.2.6.

Automatizar el despliegue

Todas las prcticas anteriores reducen el riesgo al llegar a la fase de a despliegue (automatizar la construccin, automatizar las pruebas, tener un o entorno lo ms parecido al entorno de produccin, etc). Si somos capaces a o de desplegar software en cualquier momento, estaremos aportando un valor inmenso a nuestros usuarios. Algunas personas arman que, sin un despliegue exitoso, el software realmente no existe, pues nadie, sin ser el equipo de desarrollo, lo ha visto funcionando. El xito del despliegue pasa por eliminar e el mayor nmero de errores mediante la automatizacin. u o Tener software que funciona en cualquier momento es un benecio enorme, por no decir totalmente necesario, si usamos metodolog giles como as a Scrum, Crystal, etc, donde se producen pequeas entregas al cliente de man nera incremental a lo largo del proyecto. Buenas prcticas a Identica cada despliegue en el repositorio. En subversion son conocidos como TAGs. Todas las pruebas deben ejecutarse y pasar correctamente para realizar el despliegue. Si el despliegue no ha sido correcto, aade capacidades para realizar n una vuelta atrs (roll back) y dejar la ultima versin que funcion. a o o

A.3.

IC para reducir riesgos

La IC ayuda a identicar y reducir los riesgos existentes a lo largo del desarrollo de un proyecto. Pero, de qu tipo de riesgos estamos hablando?. e Podemos apreciar que la integracin continua intenta minimizar el riesgo en o los siguientes escenarios

Despliegues demasiado largos y costosos


No es dif encontrar proyectos que realizan la construccin del software cil o en determinados entornos slo cuando faltan unos pocos d para la entrega o as del mismo. Lo toman como algo eventual, una etapa que se realiza exclusivamente al nal. Esto se suele traducir casi siempre en entregas intensas y 304

Integracin Continua (CI) o

A.4. Conclusin o

dolorosas para los miembros del equipo y, sobre todo, en una fase dif de cil estimar. Algunos de los escenarios que hacen dura esta fase: No se ha podido vericar que toda la informacin para la construccin o o se encuentra disponible en el repositorio, y no en los equipos de los desarrolladores. La famosa frase En mi equipo funciona suele ser reejo de este tipo de problemas Ausencia de una construccin automtica o a

Descubrir defectos demasiado tarde


Denir pruebas para nuestro software garantiza algo de calidad pero tan importante como hacerlas es ejecutarlas a diario con el n de que saquen a relucir los defectos que podamos haber introducido.

Falta de visibilidad del proyecto


Cuntas pruebas pasan con xito?. Seguimos los estndares planteados a e a para el proyecto? Cundo se construyo el ultimo ejecutable que funcionaba a correctamente? Este tipo de preguntas suelen ser dif ciles de responder si son hechas en cualquier momento sin previo aviso.

Baja calidad del software


Desarrollar cdigo de baja calidad suele producir un elevado coste en el o tiempo, esta armacin puede parecer gratuita para muchas personas pero o el programador que haya regresado a modicar o leer esa parte del cdigo o que no huele bien18 , no la ver como tal. Disponer de herramientas que nos a ayuden a analizar el cdigo, pudiendo generar informes sobre asuntos tales o como cdigo duplicado, etc. suele ser de gran utilidad para identicar estos o problemas y solventarlos antes de que nuestro proyecto se convierta en un enorme pantano.

A.4.

Conclusin o

La integracin continua es una prctica que ayuda a los equipos a mejoo a rar la calidad en el ciclo de desarrollo. Reduce riesgos al integrar diariamente y genera software que puede ser desplegado en cualquier momento y lugar.
18

http://en.wikipedia.org/wiki/Code smell

305

A.4. Conclusin o

Integracin Continua (CI) o

Adems, aporta mayor visibilidad sobre el estado del proyecto. Dado los benea cios que proporciona, vale la pena sacricar algo de tiempo para que forme parte de nuestra caja de herramientas.

306

Bibliograf a

[1] Gojko Adzic. Test Driven .NET Development with FitNesse. Neuri Limited, 2008. [2] Gojko Adzic. Bridging the Communication Gap: Specication by Example and Agile Acceptance Testing. Neuri Limited, 2009. [3] Kent Beck. Test Driven Development: By Example. Addison-Wesley Professional, 2002. [4] Bennington. Ingenier del Software: Un enfoque prctico (3ra Edicin). a a o Roger S. Presuman, 1956. [5] Mike Cohn. User Stories Applied. Addison-Wesley Professional, 2004. [6] Michael C. Feathers. Working Eectively with Legacy Code. Prentice Hall, 2004. [7] Martin Fowler. Refactoring: Improving the Design of Existing Code. Addison-Wesley, 1999. [8] Henrik Kniberg. SCRUM y XP desde las Trincheras. InfoQ, 2006. [9] Lasse Koskela. Test Driven. Manning, 2007. [10] Robert C. Martin. Agile Software Development: Principles, Patterns, and Practices. Prentice Hall, 2002. [11] Robert C. Martin. Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall, 2008. [12] Gerard Meszaros. xUnit Test Patterns. Addison-Wesley, 2007.

307

BIBLIOGRAF IA

BIBLIOGRAF IA

[13] Roberto Canales Mora. Informtica Profesional. Las reglas no escritas a para triunfar en la empresa. Starbook Editorial, 2009. [14] Steve Freeman Nat Pryce. Growing Object-Oriented Software, Guided by Tests. Addison-Wesley Professional, 2009. [15] Juan Palacio. Flexibilidad con SCRUM. Lulu.com, 2007. [16] J. B. Rainsberg. JUnit Recipes: Practical Methods for Programmer Testing. Manning Publications, 2004.

308

You might also like