FANDOM


Capitulo 1. El Agilismo

Modelo en Cascada

Este es el más básico de todos los modelos y ha servido como bloque de construcción para los demás paradigmas de ciclo de vida. Está basado en el ciclo convencional de una ingeniería y su visión es muy simple: el desarrollo de software se debe realizar siguiendo una secuencia de fases. Cada etapa tiene un conjunto de metas bien definidas y las actividades dentro de cada una contribuyen a la satisfacción de metas de esa fase o quizás a una subsecuencia de metas de la misma. El arquetipo del ciclo de vida abarca las siguientes actividades:

  • Ingeniería y Análisis del Sistema.
  • Análisis de los requisitos del software.
  • Diseño.
  • Codificación.
  • Prueba.
  • Mantenimiento.

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 también a él y saber que:

  • Los proyectos reales raramente siguen el flujo secuencial que propone el modelo.
  • Al principio, es difícil para el cliente establecer todos los requisitos explícitamente.
  • El cliente debe tener paciencia. Hasta llegar a las etapas finales del proyecto no estará disponible

una versión operativa del programa. Un error importante que no pueda ser detectado hasta que el programa esté funcionando, puede ser desastroso.

Hablemos de Cifras

Para ello nos basaremos en los estudios realizados por un conjunto de profesionales de Massachussets que se unió en 1985 bajo el nombre de Standish Group. El objetivo de estos profesionales era obtener información de los proyectos fallidos en tecnologías de la información (IT) y así poder encontrar y combatir las causas de los fracasos.  

Durante los últimos diez años, la industria invirtió varios miles de millones de dólares en el desarrollo y perfeccionamiento de metodologías y tecnologías. En 2004 el Standish Group reveló los siguientes resultados:

  • Porcentaje de proyectos exitosos: crece hasta el 29%.
  • Porcentaje de proyectos fracasados: 71%.

Según el informe de Standish, las tres causas principales de los fracasos, por orden de importancia, son:

  1. Escasa participación de los usuarios.
  2. Requerimientos y especificaciones incompletas.
  3. Cambios frecuentes en los requerimientos y especificaciones.

El Manifiesto Ágil

En 2001, 17 representantes de nuevas metodologías y crí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 prácticas tradicionales. Estos profesionales, con una dilatada experiencia como aval, conocían perfectamente las desventajas del clásico modelo en cascada. El manifiesto ágil se compone de cuatro principios. Es pequeño pero bien cargado de significado:

Agilismo.png

Tras este manifiesto se encuentran 12 principios de vital importancia para entender su filosofía:

  1. Nuestra máxima prioridad es satisfacer al cliente a través de entregas tempranas y continuas de software valioso.
  2. Los requisitos cambiantes son bienvenidos, incluso en las etapas finales del desarrollo. Los procesos ágiles aprovechan al cambio para ofrecer una ventaja competitiva al cliente.
  3. Entregamos software que funciona frecuentemente, entre un par de semanas y un par de meses. De hecho es común entregar cada tres o cuatro semanas.
  4. Las personas del negocio y los desarrolladores deben trabajar juntos diariamente a lo largo de todo el proyecto.
  5. Construimos proyectos en torno a individuos motivados. Dándoles el lugar y el apoyo que necesitan y confiando en ellos para hacer el trabajo.
  6. El método más eficiente y efectivo de comunicar la información hacia y entre un equipo de desarrollo es la conversación cara a cara.
  7. La principal medida de avance es el software que funciona.
  8. Los procesos ágiles promueven el desarrollo sostenible. Los patrocinadores, desarrolladores y usuarios deben poder mantener un ritmo constante.
  9. La atención continua a la excelencia técnica y el buen diseño mejora la agilidad.
  10. La simplicidad es esencial.
  11. Las mejores arquitecturas, requisitos y diseños emergen de la auto-organización de los equipos.
  12. A intervalos regulares, el equipo reflexiona sobre cómo ser más eficaces, a continuación mejoran y ajustan

su comportamiento en consecuencia.

¿En qué Consiste el Agilismo?: Un Enfoque Práctico

El agilismo es una respuesta a los fracasos y las frustraciones del modelo en cascada. Sin embargo, agilismo no es perfeccionismo, es más, el agilista reconoce que el software 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. La esencia del agilismo es la habilidad para adaptarse a los cambios.

El viejo modelo en cascada se transforma en una noria que, a cada vuelta (iteración), se alimenta con nuevos requerimientos o aproximaciones más refinadas de los ya abordados en iteraciones anteriores, puliendo además los detalles técnicos. Al igual que en el modelo tradicional, existen fases de análisis, desarrollo y pruebas pero, en lugar de ser consecutivas, están solapadas.

Todo el equipo trabaja unido, formando una piña y el cliente es parte de ella, ya no se lo considera un oponente. La estrategia de juego ya no es el control sino la colaboración y la confianza. En lugar de trabajar por horas, trabajamos por objetivos y usamos el tiempo como un recurso más y no como un fin en sí mismo (lo cual no quiere decir que no existan fechas de entrega para cada iteración).

En cualquier método ágil, los equipos deben ser pequeños, típicamente menores de siete personas. Cuando los proyectos son muy grandes y hace falta más personal, se crean varios equipos.

Los planes se hacen frecuentemente y se reajustan si hace falta. Siempre son planes de corta duración, menores de seis meses, aunque la empresa pueda tener una planificación a muy alto nivel que cubra más tiempo.

El código pertenece a todo el equipo (propiedad colectiva) y cualquier desarrollador está en condiciones de modificar código escrito por otro.

Todo el equipo se reúne periódicamente, incluidos usuarios y analistas de negocio, a ser posible diariamente y si no, al menos una vez a la semana. Por norma general, se admite que sólo los desarrolladores se reúnan diariamente y que la reunión con el cliente/analista sea sólo una vez a la semana.

Los desarrolladores envían sus cambios al repositorio de código fuente al menos una vez al día (commit). Cada vez que se termina de desarrollar una nueva función, esta pasa al equipo de calidad para que la valide aunque el resto todavía no estén listas.

No podemos negar que las metodologías son a menudo disciplinas y que implantarlas no es sencillo, todo tiene su costo y tenemos que poner en la balanza las dificultades y los beneficios para determinar qué decisión tomamos frente a cada problema.

La Situación Actual

En el mundo tecnológico los meses parecen días y los años, meses. Las oportunidades aparecen y se desvanecen fugazmente y nos vemos obligados a tomar decisiones con presteza. Las decisiones tecnológicas han convertido en multimillonarias a personas en cuestión de meses y han hundido imperios exactamente con la misma rapidez. Ahora nos está comenzando a llegar la onda expansiva de un movimiento que pone en entredicho técnicas que teníamos por buenas pero que con el paso de los años se están revelando insostenibles. Si bien hace poco gustábamos de diseñar complejas arquitecturas antes de escribir una sola línea de código que atacase directamente al problema del cliente, ahora, con la escasez de recursos económicos y la mayor exigencia de los usuarios, la palabra agilidad va adquiriendo valores de eficacia, elegancia, simplicidad y sostenibilidad.

Ágil Parece, Plátano Es

Se está usando mucho la palabra ágil y, por desgracia, no siempre está bien empleada. Algunos aprovechan el término ágil para referirse a cowboy programming (programación a lo vaquero), es decir, hacer lo que les viene en gana, como quieren y cuando quieren. Incluso hay empresas que creen estar siguiendo métodos ágiles pero que en realidad no lo hacen (y no saben que no lo hacen). Existen mitos sobre el agilismo que dicen que no se documenta y que no se planifica o analiza. También se dice que no se necesitan arquitectos pero, no es cierto, lo que sucede es que las decisiones de arquitectura se toman en equipo.

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 significa personas distintas, sobre todo en equipos muy reducidos. Una persona puede adoptar más de un rol, puede ir adoptando distintos roles con el paso del tiempo, o rotar de rol a lo largo del día. Hagamos un repaso a los papeles más comunes en un proyecto software.

  • Dueño del producto: su misión es pedir lo que necesita y aceptar o pedir correcciones sobre lo que se le entrega.
  • Cliente: es el dueño del producto y el usuario final.
  • Analista de negocio: también es el dueño del producto porque trabaja codo a codo con el cliente y traduce los requisitos en tests de aceptación para que los desarrolladores los entiendan, es decir, les explica qué hay que hacer y resuelve sus dudas.
  • Desarrolladores: toman la información del analista de negocio y deciden cómo lo van a resolver además de implementar la solución.
  • Administradores de sistemas: se encargan de velar por los servidores y servicios que necesitan los desarrolladores.

¿Porqué nos Cuesta Comenzar a ser Ágiles?

Si el agilismo tiene tantas ventajas, ¿por qué no lo está practicando ya todo el mundo? La resistencia al cambio es uno de los motivos fundamentales. Todavía forma parte de nuestra cultura pensar que las cosas de toda la vida son las mejores. Si los ingenieros y los científicos pensásemos así, entonces tendríamos máquinas de escribir en lugar de computadoras (en el mejor de los casos).

Capítulo 2: ¿Qué es el Desarrollo Dirigido por Tests? (TDD)

El Desarrollo Dirigido por Tests (Test Driven Development), al cual me referiré como TDD, es una técnica de diseño e implementación de software incluida dentro de la metodología XP. TDD es una técnica para diseñar software que se centra en tres pilares fundamentales:

  • La implementación de las funciones justas que el cliente necesita y no más.
  • La minimización del número de defectos que llegan al software en fase de producción.
  • La producción de software modular, altamente reutilizable y preparado para el cambio.

Cuando empezamos a leer sobre TDD creemos que se trata de una buena técnica para que nuestro código tenga una cobertura de tests muy alta, algo que siempre es deseable, pero es realmente una herramienta de diseño. TDD es la respuesta a las grandes preguntas de: ¿Cómo lo hago?, ¿Por dónde empiezo?, ¿Cómo sé qué es lo que hay que implementar y lo que no?, ¿Cómo escribir un código que se pueda modificar sin romper funcionalidad existente?

No se trata de escribir pruebas a granel como locos, sino de diseñar adecuadamente según los requisitos.

Las primeras páginas del libro de Kent Beck (uno de los padres de la metodología XP) dan unos argumentos muy claros y directos sobre por qué es beneficioso convertir a TDD en nuestra herramienta de diseño principal. Estas son algunas de las razones que da Kent junto con otras destacadas figuras de la industria:

  • La calidad del software aumenta.
  • Conseguimos código altamente reutilizable.
  • El trabajo en equipo se hace más fácil, une a las personas.
  • Nos permite confiar en nuestros compañeros aunque tengan menos experiencia.
  • Multiplica la comunicación entre los miembros del equipo.
  • Las personas encargadas de la garantía de calidad adquieren un rol más inteligente e interesante.
  • Escribir el ejemplo (test) antes que el código nos obliga a escribir el mínimo de funcionalidad necesaria, evitando sobrediseñar.
  • Cuando revisamos un proyecto desarrollado mediante TDD, nos damos cuenta de que los tests son la mejor documentación técnica que podemos consultar a la hora de entender qué misión cumple cada pieza del puzzle.

Personalmente, añadiría lo siguiente:

  • Incrementa la productividad.
  • Nos hace descubrir y afrontar más casos de uso en tiempo de diseño.
  • La jornada se hace mucho más amena.
  • Uno se marcha a casa con la reconfortante sensación de que el trabajo está bien hecho.

El Algoritmo TDD

La esencia de TDD es sencilla pero ponerla en práctica correctamente es cuestión de entrenamiento, como tantas otras cosas. El algoritmo TDD sólo tiene tres pasos:

  1. Escribir la especificación del requisito (el ejemplo, el test).
  2. Implementar el código según dicho ejemplo.
  3. Refactorizar para eliminar duplicidad y hacer mejoras.

Escribir la especificación primero

Una vez que tenemos claro cuál es el requisito, lo expresamos en forma de código. Si estamos a nivel de aceptación o de historia, lo haremos con un framework tipo Fit, Fitnesse, Concordion o Cucumber. Esto es, ATDD. Si no, lo haremos con algún framework xUnit.

¿Cómo escribimos un test para un código que todavía no existe? Un test no es inicialmente un test sino un ejemplo o especificación. La palabra especificación podría tener la connotación de que es inamovible, algo preestablecido y fijo, pero no es así. Un test se puede modificar. El hecho de tener que usar una funcionalidad antes de haberla escrito le da un giro de 180 grados al código resultante. No vamos a empezar por fastidiarnos a nosotros mismos sino que nos cuidaremos de diseñar lo que nos sea más cómodo, más claro, siempre que cumpla con el requisito objetivo.

Implementar el código que hace funcionar el ejemplo

Teniendo el ejemplo escrito, codificamos lo mínimo necesario para que se cumpla, para que el test pase. Típicamente, el mínimo código es el de menor número de caracteres porque mínimo quiere decir el que menos tiempo nos llevó escribirlo. No importa que el código parezca feo o chapucero, eso lo vamos a enmendar en el siguiente paso y en las siguientes iteraciones. En este paso, la máxima es no implementar nada más que lo estrictamente obligatorio para cumplir la especificación actual. Y no se trata de hacerlo sin pensar, sino concentrados para ser eficientes. Parece fácil pero, al principio, no lo es; veremos que siempre escribimos más código del que hace falta.

Refactorizar

Refactorizar no significa reescribir el código; reescribir es más general que refactorizar. Refactorizar es modificar el diseño sin alterar su comportamiento. En este tercer paso del algoritmo TDD, rastreamos el código (también el del test) en busca de líneas duplicadas y las eliminamos refactorizando. Además, revisamos que el código cumpla con ciertos principios de diseño y refactorizamos para que así sea.

La clave de una buena refactorización es hacerlo en pasitos muy pequeños. Se hace un cambio, se ejecutan todos los tests y, si todo sigue funcionando, se hace otro pequeño cambio.

¿Y TDD sirve para proyectos grandes? Un proyecto grande no es sino la agrupación de pequeños subproyectos y es ahora cuando toca aplicar aquello de “divide y vencerás”. El tamaño del proyecto no guarda relación con la aplicabilidad de TDD. La clave está en saber dividir, en saber priorizar. De ahí la ayuda de Scrum para gestionar adecuadamente el backlog del producto. Por eso tanta gente combina XP y Scrum. Todavía no he encontrado ningún proyecto en el que se desaconseje aplicar TDD.

Consideraciones y Recomendaciones

Ventajas del desarrollador experto frente al junior

Existe la leyenda de que TDD únicamente es válido para personal altamente cualificado 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 cómo enfocan los tests, es decir, qué tests escriben; más allá del código que escriben. El experto en diseño orientado a objetos buscará un test que fuerce al SUT (Subject Under Test: es el objeto que nos ocupa, el que estamos diseñando a través de ejemplos) a tener una estructura que sabe que le dará buenos resultados en términos de legibilidad y reusabilidad. Un experto es capaz de anticipar futuros casos de uso y futuros problemas aplicando las buenas prácticas que conoce. El junior probablemente se siente a escribir lo que mejor le parece, sin saber que la solución que elige quizás le traiga quebraderos de cabeza más adelante.

TDD con una tecnología desconocida

La primera vez que usamos una determinada tecnología o incluso una nueva librería, es complicado que podamos escribir la especificación antes que el SUT, porque no sabemos las limitaciones y fortalezas que ofrece la nueva herramienta. En estos casos, XP habla de spikes. Un spike es un pequeño programa que se escribe para indagar en la herramienta, explorando su funcionalidad. Es hacerse alguna función o alguna aplicación pequeña que nos aporte el conocimiento que no tenemos. Hay que respetar el tiempo de aprendizaje con la herramienta y avanzar una vez que tengamos confianza con ella. Intentar practicar TDD en un entorno desconocido es, a mi parecer, un antipatrón poco documentado.

TDD en medio de un proyecto

¿No se puede aplicar TDD en un proyecto que ya está parcialmente implementado? Claro que se puede, aunque con más consideraciones en juego. Para los nuevos requisitos de la aplicación, es decir, aquello que todavía falta por implementar, podremos aplicar eso de escribir el test primero y luego el código (¡y después refactorizar!).

Capítulo 3: Desarrollo Dirigido por Tests de Aceptación

Los tests de aceptación o de cliente son el criterio escrito de que el software cumple los requisitos de negocio que el cliente demanda. Son ejemplos escritos por los dueños de producto.

TDD es una forma de afrontar la implementación de una manera totalmente distinta a las metodologías tradicionales. El trabajo del analista de negocio se transforma para reemplazar páginas y páginas de requisitos escritos en lenguaje natural (nuestro idioma), por ejemplos ejecutables surgidos del consenso entre los distintos miembros del equipo, incluido por supuesto el cliente. No habla de reemplazar toda la documentación, sino los requisitos, los cuales se consideran un subconjunto de la documentación.

Las historias de usuario

Cada historia de usuario contiene una lista de ejemplos que cuentan lo que el cliente quiere, con total claridad y ninguna ambigüedad. El enunciado de una historia es tan sólo una frase en lenguaje humano, de alrededor de cinco palabras, que resume qué es lo que hay que hacer. Puede encontrarse una similitud con los casos de uso.

Posibles ejemplos:

  • Formulario de inscripción.
  • Login en el sistema.
  • Reservar una habitación.
  • Añadir un libro al carrito de la compra.
  • Pago con tarjeta de crédito.

Cada historia provoca una serie de preguntas acerca de los múltiples 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 añadir al carrito ya está dentro de él? ¿Qué sucede si se ha agotado el libro en el almacén? ¿Se le indica al usuario que el libro ha sido añadido al carrito de la compra?

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 e-mail al usuario cuando llegue”.

Para cada test de aceptación de una historia de usuario, habrá un conjunto de tests unitarios y de integración de grano más fino que se encargará, primero, de ayudar a diseñar el software y, segundo, de afirmar que funciona como sus creadores querían que funcionase.

Qué y no Cómo

Salvo casos muy justificados, el Dueño del Producto no debe decir cómo se implementa su solución. La mayoría de las veces, el usuario no sabe exactamente lo que quiere pero, cuando le sugerimos ejemplos sin ambigüedad ni definiciones, generalmente sabe decirnos si es o no es eso lo que busca.

¿Está hecho o no?

Otra ventaja de dirigir el desarrollo por las historias y, a su vez, por los ejemplos, es que vamos a poder comprobar muy rápido si el programa está cumpliendo los objetivos o no. Conocemos en qué punto estamos y cómo vamos progresando. Piénselo bien: ¡la propia máquina es capaz de decirnos si el programa cumple las especificaciones del cliente o no!

Fuera del contexto ágil, TDD tiene pocas probabilidades de éxito ya que si los analistas no trabajan estrechamente con los desarrolladores y testers, no se podrá originar un flujo de comunicación suficientemente rico como para que las preguntas y respuestas aporten valor al negocio.

Capítulo 4: Tipos de Test y su Importancia

Existe gran cantidad de nomenclaturas y clasificaciones de los tipos de test que se utilizan y no se encuentran estandarizados, sino que cada comunidad o equipo de desarrollo adopta su propio enfoque para identificarlos. Por otra parte, tampoco existen reglas universales para construir cada uno de estos test y sus combinaciones. Más allá de eso, lo importante es ser capaces de entender la naturaleza de los tests que se escriben, por qué se escriben y qué están probando.

Terminología en la Comunidad TDD

El primer aspecto por el cual se distinguen los tipos de test es la potestad, es decir, mirando a quien le pertenecen. Aquí se encuentran: Tests escritos por Desarrolladores y Tests escritos por el Dueño del Producto.

Test de Aceptación/Cliente

Es un test que permite comprobar que el software cumple con un requisito de negocio, escrito con el lenguaje del cliente pero que puede ser ejecutado por la máquina. Cuando se escribe el código que permite ejecutar este test, y se ejecuta positivamente, se entiende que el cliente acepta el resultado. Ejemplo: si el paciente nació el 1 de junio de 1981, su edad es de 28 años en agosto de 2009.

Test Funcional

Un test funcional es un subconjunto de los tests de aceptación. Es decir, comprueban alguna funcionalidad con valor de negocio. Un test funcional es un test de aceptación pero, uno de aceptación, no tiene por qué ser funcional. Ya que los tests de aceptación involucran cuestiones que van más allá de la funcionalidad como: tiempos de respuesta, capacidad de carga de la aplicación, etc.

Test de Sistema

El mayor de los tests de integración, ya que integra varias partes del sistema. Se trata de un test que puede ir, incluso, de extremo a extremo de la aplicación o del sistema. Así pues, un test del sistema se ejercita tal cual lo haría el usuario humano, usando los mismos puntos de entrada y llegando a modificar la base de datos o lo que haya en el otro extremo. Estos pueden automatizarse mediante software como Selenium o Watin.

Test de Integración

Los tests de integración se pueden ver como tests de sistema pequeños. Como su nombre indica, integración significa que ayuda a unir distintas partes del sistema. Un test de integración puede escribir y leer en una base de datos para comprobar que, efectivamente, la lógica de negocio entiende datos reales. Es el complemento a los tests unitarios, donde habíamos “falseado” el acceso a datos para limitarnos a trabajar con la lógica de manera aislada. Es conveniente que traten de ser inocuos y rápidos. Si tiene que acceder a base de datos, es conveniente que luego la deje como estaba.

Test Unitarios

Son los tests más importantes para el practicante TDD, los ineludibles.

Caracteristicas de los tests

Cada test unitario o test de unidad es un paso que andamos en el camino de la implementación del software. Todo test unitario debe ser:

  • Atómico: significa que el test prueba la mínima cantidad de funcionalidad posible. Esto es, probará un solo comportamiento de un método de una clase.
  • Independiente: significa 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.
  • Inocuo: quiere decir 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ía emails ni crea ficheros, ni los borra. Es como si no se hubiera ejecutado.
  • Rápido: tiene que ser porque ejecutamos un gran número de tests cada pocos minutos y se ha demostrado que tener que esperar unos cuantos segundos cada rato, resulta muy improductivo. Un sólo test tendría que ejecutarse en una pequeña fracción de segundo.

Concluimos el capítulo afirmando que los tests unitarios, de integración y de aceptación son los más importantes dentro del desarrollo dirigido por tests.  

Capítulo 5: 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 sólo un prefijo a modo de comodín para referirnos de manera genérica a los numerosos frameworks 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 filosofía es siempre la misma. Además en Java, desde la versión 4 de JUnit, se soportan las anotaciones por lo que NUnit y JUnit se parecen todavía más.

Una clase que contiene tests se llama test case (conjunto de tests) y para definirla en código heredamos de la clase base TestCase del framework correspondiente (con JUnit 3 y Pyunit) o bien la marcamos con un atributo especial (JUnit 4 y NUnit). Los métodos de dicha clase pueden ser tests o no. Si lo son, serán tests unitarios o de integración, aunque podrían ser incluso de sistema. El framework no distingue el tipo de test que es, los ejecuta a todos por igual. En este capítulo todos los tests son unitarios.

Para etiquetar los métodos como tests, en Java y .Net usamos anotaciones y atributos respectivamente. En Python se hace precediendo al nombre del método con el prefijo test, igual que pasaba con la versión 3 de JUnit. Los métodos que no están marcados como tests, se utilizan para unificar código requerido por ellos.

Es normal que haya código común para preparar los tests o para limpiar los restos su ejecución, por lo que xUnit provee una manera sencilla de organizar y compartir este código: los métodos especiales SetUp y TearDown. SetUp suele destinarse a crear variables, a definir el escenario adecuado para después llamar al SUT y TearDown a eliminar posibles objetos que no elimine el recolector de basura.

Las tres partes del test: AAA

Un test tiene tres partes, que se identifican con las siglas AAA en inglés: Arrange (Preparar), Act (Actuar), Assert (Afirmar). Una parte de la preparación puede estar contenida en el método SetUp, si es común a todos los tests de la clase. Si la etapa de preparación es común a varios tests de la clase pero no a todos, entonces podemos definir otro método o función en la misma clase, que aúne tal código. No le pondremos la etiqueta de test sino que lo invocaremos desde cada punto en que lo necesitemos. El acto consiste en hacer la llamada al código que queremos probar (SUT) y la afirmación o afirmaciones se hacen sobre el resultado de la ejecución, mediante validación del estado o bien mediante validación de la interacción. Se afirma que nuestras espectativas sobre el resultado se cumplen. Si no se cumplen el framework marcará en rojo cada falsa expectativa.

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 aplicación en un solo conjunto de tests (una sola clase). Los tests siempre son de tipo void y sin parámetros de entrada.

Cada prueba es independiente de las demás.

La validación de estado generalmente no tiene mayor complicación, salvo que la ejecución del SUT implique cambios en el sistema y tengamos que evitarlos para respetar las cláusulas que definen un test unitario.

Se considera validación de comportamiento, cuando no se valida estado ni tampoco interacción entre colaboradores. Cuando un test se ejecuta sin que una excepción lo aborte, éste pasa, aunque no haya ninguna afirmación. Es decir, cuando no hay afirmaciones y ninguna excepción interrumpe el test, se considera que funciona.

Cuando se quiere probar que se lanza una excepción, se debe expresar exactamente cuál es el tipo de la excepción esperada y no capturar la excepción genérica. Si usamos la excepción genérica, estaremos escondiendo posibles fallos del SUT, excepciones inesperadas.

Por otro lado, la validación de interacción es el recurso que usamos cuando no es posible hacer validación de estado. Es un tipo de validación de comportamiento. Es recomendable recurrir a esta técnica lo menos posible, porque los tests que validan interacción necesitan conocer cómo funciona por dentro el SUT y por tanto son más frágiles. La mayoría de las veces, se puede validar estado aunque no sea evidente a simple vista. Quizás tengamos que consultarlo a través de alguna propiedad del SUT en vez de limitarnos a un valor devuelto por una función. Si el método a prueba es de tipo void, no se puede afirmar sobre el resultado pero es posible que algún otro miembro de la clase refleje un cambio de estado.

El caso de validación de interacción más común es el de una colaboración que implica alteraciones en el sistema. Elementos que modifican el estado del sistema son por ejemplo las clases que acceden a la base de datos o que envían mensajes a través de un servicio web (u otra comunicación que salga fuera del dominio de nuestra aplicación) o que crean datos en un sistema de ficheros. Cuando el SUT debe colaborar con una clase que guarda en base de datos, tenemos que validar que la interacción entre ambas partes se produce 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 integración que se encargue de verificar el acceso a base de datos. Para llevar a cabo este tipo de validaciones es fundamental la inyección de dependencias. Si los miembros de la colaboración no se han definido con la posibilidad de inyectar uno en el otro, difícilmente podremos conseguir respetar las reglas de los tests unitarios.

La validación de la interacción se hace con frameworks de objetos mock.

Capítulo 6: Mocks y otros Dobles de Prueba

Antes de decidirnos a usar objetos mock hay que pensarlo bien. Lo primero, es saber en todo momento qué es lo que vamos a probar y por qué. En las listas de correo a menudo la gente pregunta cómo deben usar mocks para un problema determinado y 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 código del test puede llegar a ser difícil de leer.
  • El test corre el riesgo de volverse frágil si conoce demasiado bien el interior del SUT. Frágil significa que un cambio en el SUT, por pequeño que sea, romperá el test forzándonos a reescribirlo.

La gran ventaja de los mocks es que reducen drásticamente el número de líneas de código de los tests de validación de interacción y evitan que el SUT contenga hacks (apaños) para validar. En los tests de validación de estado, también se usan mocks o stubs cuando 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 cuándo debemos evitarlos.

Un mock es un tipo concreto de doble de test. La expresión “doble” se usa en el sentido de que se hace pasar por un colaborador del SUT cuando en realidad no es la entidad que dice ser. Martin Fowler publicó un artículo donde habla de los distintos dobles, los cuales son:

  • Dummy: se pasa como argumento pero nunca se usa realmente. Normalmente, los objetos dummy se usan sólo para rellenar listas de parámetros.
  • Fake: tiene una implementación que realmente funciona pero, por lo general, toma algún atajo o cortocircuito que le hace inapropiado para producción (como una base de datos en memoria por ejemplo).
  • Stub: proporciona respuestas predefinidas 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 también grabar información sobre las llamadas; tal como una pasarela de email que recuerda cuántos mensajes envió.
  • Mock: objeto pre-programado con expectativas que conforman la especificación de cómo se espera que se reciban las llamadas. Son más complejos que los stubs aunque sus diferencias son sutiles. Las veremos a continuación.

El stub es como un mock con menor potencia, un subconjunto de su funcionalidad. Mientras que en el mock podemos definir expectativas con todo lujo de detalles, el stub tan sólo devuelve respuestas pre-programadas a posibles llamadas. Un mock valida comportamiento en la colaboración, mientras que el stub simplemente simula respuestas a consultas. El stub hace el test menos frágil pero, al mismo tiempo, nos aporta menos información sobre la colaboración entre objetos. Para poder discernir entre usar un mock o un stub, volvemos a recalcar que primero hay que saber qué estamos probando y por qué. Los mocks tienen ventajas e inconvenientes sobre los stubs.

Cuándo usar un objeto real, un stub o un mock

Generalmente, para los métodos de consulta se usan stubs pero el factor determinante para decantarse por un mock o un stub es el nivel de especificidad que se requiere en la colaboración.

Desde luego, el framework hace una gran cantidad de trabajo por nosotros, ahorrándonos una buena suma de líneas de código y evitándonos código específico de validación dentro del SUT. Si por motivos de rendimiento, por ejemplo, queremos obligar a que el SUT se comunique una única vez con el colaborador, siendo además de la forma que dicta el test, entonces un mock está bien como colaborador.

Cualquier cosa que se salga de lo que pone el test, se traducirá en luz roja. Digo rendimiento porque quizás queremos cuidarnos del caso en que un módulo hiciese varias llamadas al servicio por despiste del programador o por cualquier otro motivo.

El stub no dispone de verificación de expectativas sino que hay que usar el Assert de NUnit para validar el estado.

Hay que plantearse cuál es el verdadero objetivo del test. Si se trata de describir la comunicación entre un módulo y un servicio con total precisión, debería usarse un mock. Si basta con que el módulo obtenga los datos necesarios para su funcionamiento y desenlace, usaría un stub. Cuando no estamos interesados en controlar con total exactitud la forma y el número de llamadas que se van a hacer al colaborador, también podemos utilizar un stub. Es decir, para todos aquellos 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 fin y al cabo, siempre podemos cubrir el código con un test de integración posterior que nos asegure que todas las partes trabajan bien juntas.

La metáfora Record/Replay

Algunos frameworks de mocks como EasyMock (también Rhino.Mocks) usan la metáfora conocida como Record/Replay. Necesitan que les indiquemos explícitamente cuándo hemos terminado de definir expectativas para comenzar el acto. Afortunadamente, es fácil hacerlo, es una línea de código pero a la gente la desconcierta esto de record y replay. Si hemos comprendido todos los tests de este capítulo, la metáfora no será ningún problema.

Prácticamente todo el código de los test es igual, lo único es que se debe realizar es decir explícitamente replay (serviceMock) para cambiar de estado al mock. Si se olvida activar el replay, el resultado del test es bastante raro puesto que, lo que debería ser el acto se sigue considerando la preparación y es desconcertante. Suele pasar al principio con este tipo de frameworks.

Capítulo 7: Diseño Orientado a Objetos

TDD tiene una estrecha relación con el buen diseño orientado a objetos y por tanto, con los principios S.O.L.I.D que veremos a continuación. En el último paso del algoritmo TDD, el de refactorizar, entra en juego nuestra pericia diseñando clases y métodos.

Diseño Orientado a Objetos

Todos los lenguajes y plataformas actuales se basan en el paradigma de la programación orientada a objetos. La potencia de la orientación a objetos lleva implí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 código más frágil. Es decir, no siempre conviene crear una jerarquía de clases, dependiendo del caso puede ser más conveniente crear una asociación entre objetos que colaboran.

Una de las mejores formas que hay, de ver si la API que estamos diseñando es intuitiva o no, es usarla. TDD propone usarla antes de implementarla, lo que le da un giro completo a la forma en que creamos nuestras clases. Puesto que todo lo hacemos con objetos, el beneficio de diseñar adecuadamente cambia radicalmente la calidad del software.

Principios S.O.L.I.D.

Son cinco principios fundamentales, uno por cada letra, que hablan del diseño orientado a objetos en términos de la gestión de dependencias. Las dependencias entre unas clases y otras son las que hacen al código más frágil o más robusto y reutilizable.

Single Responsibility Principle (SRP)

Este principio nos dice que cada clase debería tener un único motivo para ser modificada. Si estamos delante de una clase que se podría ver obligada a cambiar ante una modificación en la base de datos y a la vez, ante un cambio en el proceso de negocio, podemos afirmar que dicha clase tiene más de una responsabilidad o más de un motivo para cambiar.

Se aplica tanto a la clase como a cada uno de sus métodos, con lo que cada método también debería tener un solo motivo para cambiar.

Open-Closed Principle (OCP)

Una entidad software debe estar abierta a extensiones pero cerrada a modificaciones. Puesto que el software requiere cambios y que unas entidades dependen de otras, las modificaciones en el código de una de ellas puede generar indeseables efectos colaterales en cascada. Para evitarlo, el principio dice que el comportamiento de una entidad debe poder ser alterado sin tener que modificar su propio código fuente.

Hay varias técnicas dependiendo del diseño, una podría ser mediante herencia y redefinición de los métodos de la clase padre, donde dicha clase padre podría incluso ser abstracta. La otra podría ser inyectando dependencias que cumplen el mismo contrato (que tienen la misma interfaz) pero que implementan diferente funcionamiento.

Liskov Substitution Principle (LSP)

Este principio dice que si una función recibe un objeto como parámetro, de tipo X y en su lugar le pasamos otro de tipo Y, que hereda de X, dicha función debe proceder correctamente. Por el propio polimorfismo, los compiladores e intérpretes admiten este paso de parámetros, la cuestión es si la función de verdad está diseñada para hacer lo que debe, aunque quien recibe como parámetro no es exactamente X, sino Y.

Si una función no cumple el 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, modificarla.

Interface Segregation Principle (ISP)

Cuando empleamos el SRP también empleamos el ISP como efecto colateral. El ISP defiende que no obliguemos a los clientes a depender de clases o interfaces que no necesitan usar. Tal imposición ocurre cuando una clase o interfaz tiene más métodos de los que un cliente (otra clase o entidad) necesita para sí mismo. Seguramente sirve a varios objetos cliente con responsabilidades diferentes, con lo que debería estar dividida en varias entidades.

Cuando un cliente depende de una interfaz con funcionalidad que no utiliza, se convierte en dependiente de otro cliente y la posibilidad de catástrofe frente a cambios en la interfaz o clase base se multiplica.

Dependency Inversión Principle (DIP)

La inversión de dependencias da origen a la conocida inyección de dependencias, una de las mejores técnicas para lidiar con las colaboraciones entre clases, produciendo un código reutilizable, sobrio y preparado para cambiar sin producir efectos “bola de nieve”. DIP explica que un módulo concreto A, no debe depender directamente de otro módulo concreto B, sino de una abstracción de B. Tal abstracción es una interfaz o una clase (que podría ser abstracta) que sirve de base para un conjunto de clases hijas.

Inversión del Control (IoC)

Inversión del Control es sinónimo de Inyección de Dependencias. Dado el principio de la inversión de dependencias, nos queda la duda de cómo hacer para que la clase que requiere colaboradores de tipo abstracto, funcione con instancias concretas.

IoC Containers: es la herramienta externa que gestiona las dependencias y las inyecta donde hacen falta. Los contenedores necesitan de un fichero de configuración o bien de un fragmento de código donde se les indica qué entidades tienen dependencias, cuáles son y qué entidades son independientes. Afortunadamente hay una gran variedad de contenedores libres para todos los lenguajes modernos.

Si la aplicación es pequeña no necesitamos ningún contenedor de terceros sino que en nuestro propio código podemos inyectar las dependencias como queramos.    

¡Interferencia de bloqueo de anuncios detectada!


Wikia es un sitio libre de uso que hace dinero de la publicidad. Contamos con una experiencia modificada para los visitantes que utilizan el bloqueo de anuncios

Wikia no es accesible si se han hecho aún más modificaciones. Si se quita el bloqueador de anuncios personalizado, la página cargará como se esperaba.

También en FANDOM

Wiki al azar