viernes, 28 de septiembre de 2018

Plantilla para proyectos en PHP con TDD y DDD con composer


En esta publicación voy a explicar cómo implementar la base para un proyecto o librería que queramos desarrollar con TDD + DDD utilizando Composer desde cero.

Para desarrollar con TDD vamos a necesitar un framework de pruebas. En este caso se va a utilizar PHPUnit que es un framework de pruebas muy extendido. Para instalar los paquetes como PHPUnit disponemos de varias herramientas siendo Composer de las más comunes.

Otro problema al que nos enfrentamos a la hora de llevar a cabo el proyecto es la configuración del fichero de composer para que todo funcione correctamente. Así que voy a explicar cómo llevar a cabo cada paso para tener un proyecto desarrollado con TDD + DDD con Composer.

Por supuesto, el control de versiones es imprescindible en cualquier proyecto así que en este caso se va a utilizar Git.

Lo primero es crear una carpeta con el nombre del proyecto. Así que, suponiendo que tenemos una carperta “projects” en nuestra home de usuario, desde una consola creamos la carpeta con el nombre del proyecto, por ejemplo: “php-tdd-ddd-template-composer”.

mkdir ~/projects/php-tdd-ddd-template-composer

Una vez creada accedemos a ella.

cd ~/projects/php-tdd-ddd-template-composer

Ahora hay que iniciar un proyecto en Git. Así que creamos nuestro flamante proyecto en git con el siguiente comando.

git init

Siempre es recomendable añadir un archivo README.md ya que tanto para otros usuarios como para nosotros mismo en un futuro nos va a ser útil. No hay un estándar para hacer ficheros de este estilo aunque seguro que con una rápida búsqueda puedes encontrar recomendaciones o plantillas.

Una vez creado el archivo README.md lo incluimos en el repositorio.

git add .

Hacemos “commit” del mismo.

git commit -m “Add README.md” file.

Creado el proyecto y el repositorio, el siguiente paso es crear un archivo de composer para que sea más fácil la administración de los distintos paquetes.

composer init

Este comando va a crear una serie de preguntas

Package name (<vendor>/<name>) [eltortuganegra/php-tdd-template-composer]:
Description []: Template for php projects for develop with TDD. Composer is used for install project.
Author [, n to skip]: Jorge Sánchez <jorge.sanchez@eltortuganegra.com>
Minimum Stability []:
Package Type (e.g. library, project, metapackage, composer-plugin) []:
License []: MIT
Would you like to define your dependencies (require) interactively [yes]?
Search for a package:
Would you like to define your dev dependencies (require-dev) interactively [yes]? Yes
Search for a package: phpunit

Found 15 packages matching phpunit

[0] phpunit/phpunit
[1] phpunit/phpunit-mock-objects
[2] phpunit/php-token-stream
[3] phpunit/php-timer
[4] phpunit/php-text-template
[5] phpunit/php-file-iterator
[6] phpunit/php-code-coverage
[7] symfony/phpunit-bridge
[8] phpunit/phpunit-selenium
[9] johnkary/phpunit-speedtrap
[10] codedungeon/phpunit-result-printer
[11] jean85/pretty-package-versions
[12] brianium/paratest
[13] phpunit/dbunit
[14] spatie/phpunit-snapshot-assertions

Enter package # to add, or the complete package name if it is not listed: 0

Los paquetes que vamos a instalar son, de momento, phpunit en su última versión.
En este caso añadimos un 0 y pulsamos enter, lo que nos lleva a buscar otro paquete. Como no queremos añadir más paquetes. Pulsamos enter.

Para ver las dependencias escribimos “yes” y pulsamos enter.

Search for a package:

{
"name": "eltortuganegra/php-tdd-template-composer",
"description": "Template for php projects for develop with TDD. Composer is used for install project.",
"type": "project",
"require-dev": {
"phpunit/phpunit": "^7.3"
},
"license": "MIT",
"authors": [
{
"name": "eltortuganegra",
"email": "jorge.sanchez@eltortuganegra.com"
}
]
}

Do you confirm generation [yes]? yes
Would you like the vendor directory added to your .gitignore [yes]? yes


Sólo queda confirmar y se procede a la generación del archivo “composer.json”.

Ahora pasamos a instalar con composer todas las dependencias.

composer install

En este momento se puede observar como se ha creado una carpeta «vendor» donde se han instalado todas las dependencias.

Llega el momento de organizar las distintas carpetas del proyecto. Cada uno tiene su organización de carpetas pero una buena manera de empezar es crear una carpeta «tests» y otra «src» donde se almacenará el código de los tests (sí, ¡las pruebas se hacen primero!).

Para comprobar si Phpunit está funcionando vamos a crear una prueba por defecto: DefaultTest.php

Cuando ejecutamos phpunit para que ejecute la prueba

./vendor/bin/phpunit tests/DefaultTest.php

Debería devolver el siguiente resultado.

PHPUnit 7.3.5 by Sebastian Bergmann and contributors.

. 1 / 1 (100%)

Time: 98 ms, Memory: 4.00MB

OK (1 test, 1 assertion)

Con esta sencilla prueba se verifica que Phpunit está funcionando correctamente. Aún que el propósito es probar el código fuente así que vamos a añadir una prueba donde se utilice una clase que esté ubicada en la carpeta destinada a tal fin, la carpeta «src».

Una prueba simple podría ser crear una instancia de la clase ubicada en la carpeta «src» y comprobar que la nueva instancia es una instancia de esa clase.

Ejecutamos la prueba recién creada para confirmar que da un error, tal y como se indica en TDD.

PHPUnit 7.3.5 by Sebastian Bergmann and contributors.

.E 2 / 2 (100%)

Time: 83 ms, Memory: 4.00MB

There was 1 error:

1) DefaultTest::testDeleteMeShouldCreateAnInstanceWhenWeCreateAnInstance
Error: Class 'DeleteMe' not found

/home/vagrant/www/php-tdd-template-composer/tests/DefaultTest.php:17

ERRORS!
Tests: 2, Assertions: 1, Errors: 1.

Como se puede apreciar la ejecución de la prueba da un error. Ahora hay que crear la clase “DeleteMe” en la carpeta «src» para que pasar la prueba. Añadimos el namespace «app».
En este caso, si volvemos a ejecutar la prueba vamos a tener el mismo error. ¿Cómo es posible si hemos creado la clase? El problema es que no estamos indicando que cargue las clases automáticamente. Para configurarlo hay que modificar el fichero de configuración de composer y añadirle un atributo “autoload”.

{
"name": "eltortuganegra/php-tdd-template-composer",
"description": "Template for php projects for develop with TDD. Composer is used for install project.",
"type": "project",
"require-dev": {
"phpunit/phpunit": "^7.3"
},
"license": "MIT",
"authors": [
{
"name": "eltortuganegra",
"email": "jorge.sanchez@eltortuganegra.com"
}
],
"autoload": {
"classmap": [
"src/"
]
}
}

Actualizamos composer para que incorpore los cambios en el fichero de configuración.

composer dump-autoload

Y volvemos a ejecutar la prueba. Con el siguiente resultado.

./vendor/bin/phpunit tests/DefaultTest.php
PHPUnit 7.3.5 by Sebastian Bergmann and contributors.

.. 2 / 2 (100%)

Time: 67 ms, Memory: 4.00MB

OK (2 tests, 2 assertions)

Llegados a este punto, tenemos la base del proyecto funcionando. Una carpeta «tests» donde se almacenan todos las pruebas y una carpeta «src» donde se almacena todo el código que tiene que pasar las pruebas.

Según el DDD no hay una organización estándar de estructuras así que cada uno es libre de organizarla como considere oportuno. En el libro de Eric Evans Domain-Driven Design book tiene un ejemplo “Cargo Sample” del cual se puede encontrar implementaciones en GitHub como como el php-ddd-cargo-sample. En el se puede apreciar la estructura de carpetas que utilizan.

Sin embargo, esta publicación es para crear un proyecto sencillo y de cero por lo que, en principio y como ejercicio paso a paso, crearía una carpeta por cada concepto que se detalla en DDD.

Mi solución sencilla para aquellos que están empezando y quieren empezar a introducirse en este mundo es utilizar la plantilla para proyectos en php con TDD y DDD.

En esta plantilla hay una estructura de carpetas que albergan un clase por defecto por cada concepto y su correspondiente prueba en la carpeta «tests».


miércoles, 5 de septiembre de 2018

Introducción a DDD

Qué es el DDD


El diseño guiado por el dominio, en inglés «domain-driven design», o DDD, es un enfoque para el desarrollo de software.

El DDD no es una tecnología ni una metodología, sino un conjunto de prácticas y terminologías que aportan valor a la hora de tomar decisiones sobre el diseño de software.

El DDD tiene más que ver con que ver con sobre el debatediiscusiónscusión debate,, la escucha, el entendimiento, el descubrimiento y el valor de negocio en un esfuerzo por centralizar el conocimiento del dominio. Si se es capaz de entender el negocio se puede participar en el proceso de descubrimiento del modelo de software para producir un lenguaje ubicuo.

El lenguaje ubicuo es el lenguaje común de los expertos del dominio y los desarrolladores. Este facilita la comunicación entre todas las partes implicadas, por lo que todos saben que está pasando con el negocio.

Otra característica del DDD es que todo está basada en el dominio siendo este una abstracción del negocio.

Una vez que los conceptos del dominio están definidos en el lenguaje común se puede modelar el mismo. Un modelo de dominio es una parte muy específica del dominio de negocio. Este modelo se implementa como un modelo objeto con unos datos y un comportamiento con un significado literal y preciso dentro dcon el negocio.

Nunca se trata de modelar todo el negocio en un único y enorme modelo de dominio si no que los modelos tienden a ser pequeños y muy enfocados.

Qué valor aporta DDD


El DDD aporta, principalmente, los siguientes valores:

  • Involucra a los expertos de dominio y a los desarrolladores durante el diseño del software por lo que este tiene sentido para todos los implicados y no sólo para los desarrolladores.
  • El software desarrollado tiene un significado semántico para los expertos de dominio.
  • Supone una mejor comprensión del negocio para todos los implicados ya que el negocio está en constante evolución.
  • El conocimiento del software no está centralizado exclusivamente en los desarrolladores si no que al estar involucrado el negocio estos también tienen el conocimiento.
  • Mejora la comunicación entre los expertos del dominio y los desarrolladores al utilizar el lenguaje ubicuo con lo que no se necesita hacer traducciones.
  • El DDD provee técnicas de desarrollo de software que dirigen el diseño estratégico y táctico.

El diseño estratégico tiene el objetivo de dirigir el negocio por lo que se centra en definir los distintos aspectos del negocio y priorizar aquellos que aporten un mayor valor.

El diseño táctico dispone de herramientas de modelado para el desarrollo de los entregables de software ejecutables. Estas herramientas permiten analizar y desarrollar software con el modelo mental de los expertos de dominio.

Cuando hay que usar el DDD


El factor fundamental a tener en cuenta es si la inversión derivada de aplicar el DDD aporta valor al proyecto, pues hay que hacer una inversión de tiempo y esfuerzo.

El DDD no aporta un valor sustancial a las partes que son triviales y que son fácilmente reemplazables. Sin embargo, sí aporta valor en aquellas áreas más complejas, las más valorables e importantes y de mayor valor para el sistema.

A estas áreas más importantes las llamamos «Core Domain» y en segunda prioridad «Supporting Subdomains». Estas son las que aportan mayor valor al negocio.

Uno de los objetivos del DDD es modelar un sistema complejo de la manera más simple posible por lo que nunca se debería usar el DDD para obtener hacer la solución más compleja.

La complejidad dependerá de cada negocio por lo que puede ser difícil definir qué es complejo. Una estrategia alternativa es definir qué es trivial. Una vez establecido qué no es trivial el equipo tendrá que valorar si merece la pena hacer la inversión de hacer uso del DDD.

Cómo hacer DDD


Para llevar el DDD a la práctica hay que dominar dos aspectos características: la ubicuidad del lenguaje y los contextos delimitados.

Lenguaje ubicuo

El lenguaje ubicuo es un lenguaje que desarrollan y comparten todos aquellos que están implicados en el desarrollo del proyecto sin importar su rol.

Los expertos de negocio tendrán un mayor peso a la hora de desarrollar el lenguaje ya que son ellos quienes conocen el negocio. A pesar de tener un mayor peso deben trabajar junto con el resto del equipo, como los desarrolladores, para confeccionar un modelo de dominio de manera que se definan los mejores conceptos, términos y significados. El lenguaje evoluciona a medida que los avances se van alcanzando.

Algunas recomendaciones para definir el lenguaje ubicuo:

  • Tener una representación gráfica del dominio de manera que se puedan apreciar los distintos elementos que lo componen.
  • Crear un glosario de términos con los elementos del dominio.
  • Cualquier otro tipo de documentación aclare los conceptos, términos y significados del dominio.
  • Cualquiera de las anteriores tiene que tener la aprobación del resto del equipo. Estas podrán ir evolucionando con el tiempo por lo que habrá que ir actualizándolas.

Contexto delimitado

Los contextos delimitados son límites conceptuales del sistema. Estos límites resaltan un contexto y, al igual que en el lenguaje natural, los distintos términos y frases tienen un significado específico dentro de ese contexto. Cualquier uso de un término fuera de ese contexto podría tener un significado diferente.

Los contextos delimitados son tan pequeños como se puedan imaginar. Cada uno de ellos tendrá su propio lenguaje ubicuo aunque algunos de los términos entre los distintos contextos delimitados que componen el sistema se puedan sobreponer.

Los retos de aplicar el DDD


Los principales retos son:

  • Involucrar a los expertos del dominio durante todo el proceso del desarrollo del negocio.
  • La inversión de tiempo y esfuerzo necesarios para crear la ubicuidad del lenguaje.
  • Cambiar la manera en la que los desarrolladores piensan en las soluciones del dominio.

Uno de los mayores retos es concienciar a los expertos de negocio de su implicación durante el desarrollo.

Otro gran reto es pensar sobre el dominio y, junto con la ayuda de los expertos de negocio, definir el lenguaje ubicuo así como los contextos delimitados de manera que el conocimiento del dominio sea lo más completo posible.

El último gran reto es cambiar la manera en la que piensan los desarrolladores ya que estos piensan desde la perspectiva técnica. No es que pensar técnicamente sea algo malo pero en el momento en que hay que comunicarse con gente de otros ámbitos es mejor adaptarse y pensar un poco menos técnicamente.

Conclusiones


Durante este capítulo se ha visto el enfoque del DDD para el desarrollo de software y cómo afecta sobre el diseño del software.

Se ha visto también el esfuerzo que se requiere a la hora de involucrar a los expertos de negocio así como a la hora de crear lenguaje ubicuo y los contextos delimitados.

El DDD se utiliza en partes en las que la complejidad es elevada con el objetivo de modelarlo de la forma más sencilla posible.

También se han visto los principales retos a los que un equipo se enfrentará.

domingo, 15 de julio de 2018

Capítulo 34: Refactoring - Parte 2 - TDD By Example

Extract Method

Cuando un método se torna demasiado largo hay muchas posibilidades de poder separar parte del mismo en un nuevo método y hacer una llamada a este método.

La manera de abordar un refactor con «Extract Method» es:

  • Encontrar una parte del código que tenga sentido por si sola en su propio método. Los bloques de los bucles, los bucles completos, los bloques de los condicionales son unos buenos candidatos.
  • Hay que verificar que no se hagan asignaciones a variables temporales que estén declaradas fuera de la visibilidad del la región que se va a extraer.
  • Copiar el código del método al nuevo método.
  • Por cada variable temporal o parámetro del método original que se usa en el nuevo método hay que añadir un parámetro al nuevo método.
  • Hacer la llamada del nuevo método desde el método original.

Los IDE actuales son capaces de hacer este trabajo por ti así que trata de usar su funcionalidad para ahorrar tiempo.

Esta refactorización se utiliza para tratar de entender un código complicado. Separar el código en funciones con nombres descriptivos hace más fácil entender que está haciendo ese código.
También se utiliza para eliminar la duplicación que puede darse en varias partes del código de manera que se pueda reutilizar este bloque.

Habrá momentos que se llevará al extremo hacer «Extract Method» no viendo una manera clara de continuar. En estos casos se puede usar la refactorización contraria «Inline Method» para deshacer la refactorización y volver a empezar con «Extract Method».

Inline Method


Es el paso contrario a «Extract Method». En las ocasiones en las que tiene más sentido que el código esté en el método desde que se hace la llamada.

Los pasos a seguir son:

  • Copiar el método.
  • Pegar el contenido del método sobre la llamada.
  • Reemplazar todo los parámetros con los parámetros que se usen dentro del método al cual se está copiando.

Extract Interface

Para introducir una segunda implementación de una operación se crea una interfaz que contenga definidas las operaciones.

Para llevar a cabo la refactorización:

  • Se declara una interfaz. A veces el nombre coincide con el de la case que existe en cuyo caso habría que renombrar la clase.
  • La clase que existe debe implementar la nueva interfaz.
  • Hay que definir en la interfaz aquellos métodos que sean necesarios.
  • Cambiar las declaraciones de tipo de la clase a la interface cuando sea posible.

Cuando se necesita un refactorizar con «Extract Method» es porque se necesita una segunda implementación. Por ejemplo, si se tiene una clase «Rectangle» y quiere añadir un Oval, entonces puedes crear una interface «Shape» que defina los métodos de «Rectangle».

Move Method


Si un método pertenece realmente a otra clase habría que moverlo a la nueva clase e invocarlo desde donde se utilice.

Los pasos para hacer el refactor:

  • Copiar el método.
  • Pegar el método en la clase objetivo.
  • Si el método tiene referencias al objeto original entonces hay que pasar el objeto como parámetro. Si hay referencia a variables del objeto original entonces son estas las que se pasan como parámetro. Si se asignan variables del objeto original entonces se debería parar y no hacer esta refactorización.
  • Reemplazar el cuerpo del método original con una invocación al nuevo método.

Por ejemplo, calcular el área es responsabilidad de «Shape».

Cada vez que se ve más de un mensaje a otro objeto en un método hay que sospechar. En este caso hay cuatro llamadas a distintos métodos de «$bounds» por lo que se debería mover esta parte del método a la clase «Rectangle».

Y desde el método original se hace la llamada a este nuevo método.

Las tres grandes propiedades de «Extract Method» son:

  • Es fácil de ver sin necesidad de tener un conocimiento profundo de la lógica. Si se ven dos o más mensajes a un objeto es el momento de aplicarlo.
  • Los pasos para hacer el refactor son rápidos y seguros.
  • A veces los resultados son reveladores. “Pero los Rectángulos no hacen cálculos … Oh! Espera. Esto es mejor.”

A veces sólo se querrá mover una parte del método por lo que habrá que usar primero «Extract Method» y move el método.

Method Object

Cuando un método se complica tanto como para tener varios parámetros y variables locales se puede hacer un objeto que absorba la funcionalidad de este método.

Los pasos para refactorizar con «Method Object» son:

  • Crear un objeto con los mismos parámetros que el método.
  • Crear tantos atributos como variables locales.
  • Crear un método llamado «run()» cuyo contenido sea el mismo que el contenido del método original.
  • En el método original crear un nuevo objeto y hacer una llamada al método «run()».

Las refactorizaciones «Method Object» son útiles ya que permiten añadir un nuevo tipo de lógica al sistema. Por ejemplo, se pueden tener varios métodos para calcular el flujo de efectivo en un componente para el cálculo de estos flujos. Se puede practicar una refactorización de estos métodos con «Method Object». También se puede escribir un nuevo tipo de cálculo con sus propias pruebas y con lo que conectarlo será un sólo paso.

Los «Method Object» también son buenos para simplificar el código al que no se puede aplicar la refactorización «Extract Method». En ocasiones habrá un bloque de código que al aplicar «Extract Method» nos encontremos con una función con demasiados parámetros y el aspecto de esta no es mucho mejor que el código original. En este caso, crear un nuevo objeto da un nuevo espacio el cual puede albergar las variables locales sin tener que pasar nada.

Add Parameter


Pues sí, «Add Parameter» es un tipo de refactorización:

  • Si el método es de una interfaz entonces se añade el parámetro a la interfaz.
  • En los lenguajes compilados habrá errores que adviertan del código que se necesita cambiar. Si no habrá que usar las funcionalidades del IDE para buscar aquellos lugares.

Añadir un parámetro es a veces un paso de extensión. Primero se tiene el primer caso ejecutándose sin necesidad de parámetros para más tarde, pasar un parámetro que añade más información para realizar la operación correctamente.

Añadir un parámetro puede ser parte de una migración una representación de un dato a otro. Primero se añade el parámetro, entonces se borra todos los usos del parámetro viejo y después el parámetro viejo.

Method Parameter to Constructor Parameter


Mover parámetros de un método o métodos al constructor es otra manera de refactorizar.

Los pasos son:

  • Añadir un parámetro al constructor.
  • Añadir un atributo con el mismo nombre que el parámetro.
  • Asignar valor del parámetro al atributo en el constructor.
  • Cambiar todas las referencias del parámetro en el método por referencias al atributo.
  • Cuando ya no haya más referencias al parámetro se borra el parámetro de la firma del método y todos los que hacen la llamada.

En los casos en los que el mismo parámetro se pasa en diferentes métodos dentro del mismo objeto se puede simplificar la API pasando el parámetro una sóla vez (eliminando la duplicación). Esta refactorización se puede aplicar de forma inversa si se encuentra con que un atributo sólo se utiliza en un método.

viernes, 13 de julio de 2018

Capítulo 34: Refactoring - Parte 1 - TDD By Example

La parte más importante del «refactoring» es saber el porqué podrías querer usarlo.

En TDD se utiliza de manera que no se cambia la semántica del programa bajo ninguna circunstancia. Así que las pruebas que ya han pasado definen la semántica que hay hasta ahora así que, por ejemplo, se pueden reemplazar las constantes por variables y llamar a esta operación «refactorización» porque esta no cambia el conjunto de pruebas que pasan.

Reconciliar las diferencias


Cuando hay dos piezas de código que son muy similares se puede adoptar la estrategia de, gradualmente, acercarlas hasta que son absolutamente idénticas y en ese caso unificarlas.

Habrá refactorizaciones que sean obvias y que apenas cambien el comportamiento del sistema mientras que otras obligarán a revisar el control de flujo y los datos.

TDD brinda las herramientas necesarias para hacer refactorizaciones en pasos pequeños y tener «feedback» en el momento por lo que se deberían evitar los saltos de fé con refactorizaciones de dimensiones que no podamos controlar.

Las refactorizaciones ocurren en todos los ámbitos:
  • Si dos estructuras de bucle son iguales se pueden hacer idénticas y se puede unir.
  • Si dos ramas de un condicional son similares se pueden hacer idénticos y eliminar el condicional.
  • Si dos métodos son similares se pueden hacer idénticos y eliminar uno.
  • Si dos clases son similares se pueden hacer idénticas y eliminar una.

A veces se necesita abordar la reconciliación de las diferencias al revés, pensar como el último paso del cambio podría ser trivial y continuar así. Por ejemplo, si se quiere eliminar varias subclases el paso trivial es si la clase no contiene nada, en este caso, la subclase se puede reemplazar por la superclase sin cambiar el comportamiento del sistema. Para vaciar la clase el método debe ser idéntico a uno en la superclase. Entonces se vacían todas las subclases y, cuando todas estén vacías, se puede reemplazar las referencias de estas por la superclase.

Aislar el cambio


En el momento que se quiere hacer un cambio hay que identificar en qué parte del código se va a implementar, esto es aislar la parte que va a cambiar.

Habrá ocasiones en las que hemos aislado y hecho el cambio este resulte tan trivial que se pueda deshacer el aislamiento. Si encontramos que todo lo que se necesitaba era devolver una variable in «findRate()» deberíamos considerar la inclusión de todos los sitios donde se usa y borrarlo. Antes de borrarlo hay que valorar si tener un método adicional con el valor de tener un concepto explícito adicional  en el código.

Algunas estrategias para aislar el cambio son «Extract Method», «Extract Object» o «Method Object».

Migrar los datos


Las migraciones de datos deberían duplicarse de manera temporal.

Una estrategia para la migración es desde el interior hacia el exterior. En esta estrategia se cambia la representación interna para, posteriormente, cambiar la interface externa visible.

  • Añadir una nueva variable de instancia en el nuevo formato.
  • Asignar a la nueva variable en todos los lugares donde la vieja variable estaba asignada.
  • Usar la nueva variable en todos los lugares donde la vieja variable se usaba.
  • Borrar la vieja variable.
  • Cambiar la interfaz externa para reflejar la nueva variable.

En otras ocasiones la estrategia será cambiar del exterior al interior. Se cambia la interface externa visible para, posteriormente, cambiar la representación interna. Entonces se debería:

  • Añadir un parámetro en el nuevo formato.
  • Traducir la representación interna del viejo formato al nuevo formato.
  • Borrar el parámetro antiguo.
  • Reemplazar el uso del viejo formato con el nuevo.
  • Borrar el formato antiguo.

Las migraciones de datos de uno a una colección («One to Many») siempre son complejas de abordar. Por ejemplo, en el caso se pasar de ejecutar una prueba a tener un conjunto de pruebas en xUnit. Se debería empezar:

La clase «TestSuite», la cual está en la parte «One» de «One to Many», tiene la siguiente implementación.

Ahora empezamos con la duplicación de datos. Primero inicializamos la colección de pruebas.

En todos los lugares donde se asigna un valor a la variable «test» añadimos a la colección.

Ahora es el momento de usar la colección de «test» en vez de usar la variable «test». En este caso el cambio preserva la semántica del código ya que sólo hay un elemento en la colección.

Finalmente se limpia el código de las variables de «test» que no se usan.

lunes, 9 de julio de 2018

Capítulo 33: Design Patterns - Collecting Parameter - TDD By Example

Para recopilar el resultado de una operación que está extendida entre varios objetos se puede añadir un parámetro a la operación en el cual se puedan recopilar estos resultados.

En xUnit no se necesito que «TestResult» recopilará el resultado de varias pruebas hasta que no se tuvieron varias pruebas.

A medida que se espera que los resultado crezcan se puede encontrar la necesidad de usar «Collecting parameter». Por ejemplo, si se quiere escribir una expresión y todo lo que necesitamos es escribir un texto plano entonces la concatenación es suficiente.

Teniendo la clase Sum el método «toString()».

Si se quiere añadir sangría en forma de árbol en la expresión tendremos que modificar la prueba:

En este caso tendremos que implementar el patrón «Collecting Parameter».

Capítulo 33: Design Patterns - Composite - TDD By Example

Para implementar un objeto cuyo comportamiento sea igual que el comportamiento de una lista de otros objetos se crea un «Imposter» para el objeto de la lista.

Por ejemplo, una transacción incrementa un valor.

Una cuenta calcula su balance sumando los valores de sus transacciones.

Un cliente podrá tener una o más cuentas y, en cualquier caso, podrá ver el balance general de todas sus cuentas. La implementación más obvia sería crear una nueva clase «OverallBalance» que sume el balance de cada una de las cuentas.

Aunque a primera vista puede ser un poco más difícil de ver, existe duplicación de código. Si una «Account» y «Transaction» implementaran la misma interfaz, por ejemplo «Holding» la duplicación desaparecería ya que el «OverallBalance» podría ser una cuenta que contiene cuentas.

La transacción puede implementar el método «balance()».


Una cuenta está compuesta por una o más «Holding».

El olor de «Composite» viene ilustrado por ejemplo anterior. Las transacciones no tienen un balance en el mundo real pero aplicando el patrón los beneficios para el diseño son sustanciales ya que elimina la duplicidad. La traducción no viene del mundo real pero hace el código mucho más simple.

No siempre es tan obvio en qué caso una colección de objetos es una simple colección o se tiene un «Composite». A medida que se va cogiendo experiencia en la refactorización se podrá apreciar la duplicación de código y se podrá valorar si hay que aplicar el patrón «Composite».

jueves, 5 de julio de 2018

Capítulo 33: Design Patterns - Imposter - TDD By Example

Para introducir nuevas variaciones en un proceso se puede introducir un nuevo objeto con el mismo protocolo que el objeto que existe pero con la nueva implementación.

Si la variación se puede aplicar en un lugar que sea obvio y además no se está duplicando el código entonces se puede aplicar la variación con una instrucción «if». En otras ocasiones la variación requerirá del cambio en varios métodos.

Se deberá escribir una prueba para la variación ya que se necesita representar el nuevo escenario. Por ejemplo si se está probando un editor gráfico y ya está probado que se dibujan correctamente los objetos de tipo rectángulo con una prueba como la siguiente.

Si queremos dibujar un óvalo, en este caso el Impostor debería reemplazar el objeto de clase «RectangleFigure» por «OvalFigure».

Generalmente, detectar la posibilidad de utilizar un «Imposter» requiere de un poco de conocimiento. La percepción de Ward expone que un vector de dinero puede actuar como dinero es precisamente ese momento.

Dos ejemplos de «Imposter» que vienen durante la refactorización:

  • «Null Object»: se puede tratar la ausencia de datos al igual que la presencia de datos.
  • «Composite»: se puede tratar una colección de objetos como si de un objeto se tratase.

Encontrar dónde aplicar «Imposter» durante la refactorización reduce la duplicación de código al igual que toda refactorización es para eliminar la duplicación.

Capítulo 33: Design Patterns - Factory Method - TDD By Example

Cuando se requiere de flexibilidad a la hora de crear un nuevo objeto se puede crear este en un método en vez de usar el constructor.

Los constructores no son expresivos mientras que los método pueden comunicar la intención. Además, tiene la ventaja que el método no tiene porqué devolver la misma clase si no que puede utilizar una clase diferente para crear el objeto.

En el ejemplo de la clase «Money» se podía devolver un objeto de una clase diferente. Se tenía la siguiente prueba.

Se quería introducir la clase «Money» pero existía el bloqueo de crear una instancia de «Dollar». Este bloqueo se podría solventar añadiendo un nivel de indirección a través de un método de manera que se puede ganar la flexibilidad de devolver un objeto de una clase diferente sin cambiar la prueba.

Este método se llama factoría porque crea objetos.

El inconveniente de usar «Factory Pattern» es precisamente su indirección y recordar que el método está creando un objeto aunque no se vea como un constructor. Este patrón sólo se debería usar cuando se necesite la flexibilidad de crear nuevos objetos. De otra manera, los constructores funcionan bien para crear objetos.

miércoles, 4 de julio de 2018

Capítulo 33: Design Patterns - Pluggable Selector - TDD By Example

Para ejecutar un comportamiento diferente para diferentes instancias se puede almacenar el nombre del método y ejecutarlo dinámicamente.

Crear subclases en un mecanismo pesado para capturar pequeñas variaciones. Por ejemplo, si se tiene una clase «Report» se podría tener varios tipos de informes

Una alternativa es tener una sola clase con instrucción «switch». Dependiendo del valor del campo, se puede invocar un método de forma dinámica. El nombre del método aparecería en:
  • La creación de la instancia.
  • La instrucción «switch».
  • La definición del propio método.
Cada vez que surge la necesidad de aplicar un nuevo comportamiento hay que definir el nuevo método y modificar la instrucción «switch».

La solución aplicando el patrón «Pluggable Selector» pasa por invocar el método dinámicamente:

Ahora hay una dependencia entre los métodos y el nombre de los métodos aunque elimina la instrucción «switch».

Uno de los principales problemas de aplicar este patrón es la trazabilidad del código. Sólo se debería usar el patrón «Pluggable Selector» cuando se de una situación bastante sencilla en la que un conjunto de subclases tengan un único método.

Capítulo 33: Design Patterns - Pluggable Object - TDD By Example

La manera más simple de expresar una variación es hacerlo de manera explícita a través de condicionales:

En el momento que se hace una variación de manera explícita esta se va a comenzar a extender por todo el código añadiendo complejidad al mismo. Por esta razón la segunda vez que se vea un condicional de este tipo es el momento de utilizar «Plugabble Object».

Cuando en su día Erich Gamma y Kent Beck escribieron un editor gráfico dieron con un problema donde aplicar este patrón. Cuando estaban desarrollando la herramienta de selección si estaban sobre una figura y se presionaba el botón del ratón el resto de movimientos del ratón arrastraba la figura hasta soltar el botón. Si no había una figura entonces se estaba seleccionando un grupo de figuras. Arrastrar el ratón provocaba incrementar el rectángulo de selección hasta soltar el botón y en ese momento se seleccionaban todas las figuras que estuvieran en el rectángulo de selección.

El código inicial podría ser algo similar al siguiente.

En este caso se puede apreciar como la variación de si hay una figura seleccionada se extiende por el código. Una posible solución para resolver este problema es crear un «Pluggable Object», un «SelecctionMode» con dos implementaciones «SingleSelection» y «MultipleSelection».


Se tendrá que implementar una interfaz con las dos implementaciones.

Capítulo 33: Design Pattern - Template Method - TDD By Example

Hay ocasiones en las que se implementa un método que definen una serie de operaciones. Si tenemos otro método que implementa las misma serie de operaciones pero con algunas partes variables entonces se puede hacer un refactorización en la que se crea una clase padre con el método con las partes comunes y las subclases implementen las partes variables.

En el ejemplo de xUnit se puede apreciar la secuencia de operaciones de la clase padre «TestCase».

Las clases hijas son las que implementan los distintos métodos.

La pregunta que surge es cuándo se debería escribir una implementación por defecto. En los casos de los métodos «setUp()», «tearDown()» no tienen operaciones por defecto aunque son necesarios para el funcionamiento de la secuencia por lo que habría que implementarlos.

Si la secuencia de operaciones necesita una operación la cual necesita una implementación específica entonces se debería advertir declarando el método abstracto o implementando el método y lanzando una excepción.

lunes, 2 de julio de 2018

Capítulo 33: Design Pattern - Null Object - TDD By Example

Hay ocasiones en las que los programadores tendrán que comprobar si una variable tiene un valor nulo y esta comparación puede repetirse a lo largo de diversas partes del código.

En el siguiente ejemplo se puede apreciar como, cada vez que se hace una búsqueda de una campaña se tiene que hacer una comprobación por si fuera nulo el valor devuelto.


Una posible solución es representar este caso especial con un objeto.


Cada vez que alguien utilice el método «find()» del repositorio, en aquellos casos en los que no se encuentre la campaña, se devolverá el objeto del caso especial.


Con esta estrategia ya no hay que preocuparse de realizar la comprobación ya que esta se realiza en un único sitio y en caso de ser nula devuelve el objeto especial. Con lo que el código original queda más sencillo.

Capítulo 33: Design Pattern - Value Object - TDD By Example

Si dos objetos A y B hacen referencia a un tercero C y A cambia el estado de C entonces B no debería confiar en el estado de C.

Una posible solución es no compartir los objetos en los que se confía si no hacer copias de los mismos. Esto lleva a un mayor uso de espacio y tiempo. Otra solución sería utilizar el patrón Observer de manera que se avise explícitamente de la modificación cuando se produzca un cambio. Esto complica los flujos de control y la lógica.

Otra solución es tratar al objeto como un objeto que no cambia. De esta manera se podrá hacer referencia a él sin miedo a que cambie el estado.

Cuando se implementa el patrón «Value Object» cada operación que se define sobre un objeto devuelve un nuevo objeto sin alterar el objeto original desde el que se hizo la operación.

Cuando se usa un patrón «Value Object» todos los objetos que lo implementan deberían implementar un mecanismo para comprobar la igualdad entre dos de ellos. Cinco euros deberían ser iguales a cinco euros independientemente de si son referencias al mismo objetos o si son objetos distintos con el mismo valor.

Capítulo 33: design Patterns - Command - TDD By Example

Cuando una operación es más complicada que una simple llamada a un método se crea un objeto que albergue la operación y se ejecuta. Se encapsula la funcionalidad en el objeto y se le pasan los parámetros adecuados para su correcto funcionamiento.

Este patrón establece una interfaz común que permite ejecutar las operaciones de forma uniforme así como extender el sistema con nuevas operaciones de una forma sencilla.

Capítulo 33: Design Patterns - TDD By Example

Los patrones de diseño son soluciones que se aplican a problemas comunes. Estos no son un diseño final si no es una descripción de cómo resolver un problema y que se puede aplicar en diversa situaciones. Se pueden considerar como buenas prácticas para resolver estos problemas comunes cuando se está diseñando una aplicación o sistema.

Se van a presentar los siguientes patrones de diseño:

  • Command: representa la invocación de un operación como un objeto no simplemente como un mensaje.
  • Value Object: evita problemas creando objetos que nunca cambian una vez que se han creado.
  • Null Object: representa el caso base de una operación por un objeto.
  • Template Method: representa secuencias invariables de operaciones con un método abstracto el cual se define a través de la herencia.
  • Pluggable Object: evita subclases invocando dinamicamente diferentes métodos para diferentes instancias.
  • Factory Method: crea un objeto llamando a un método en vez de a un constructor.
  • Composite: representa la composición del comportamiento de una lista de objetos con un objeto.
  • Collecting Parameters: 
  • Imposter: introduce variaciones introduciendo a nueva implementación de un protocolo existente.

Patrón Escritura de prueba Refactor
Command X
Value Object X
Null Object X
Template Method X
Plugabble Object X
Pluggable Selector X
Factory Method X X
Composite X X
Collecting Parameter X X
Imposter X X

domingo, 1 de julio de 2018

Capítulo 32: xUnit Patterns - TDD By Example

La manera de comprobar si un código funciona correctamente es escribir un expresión binaria que juzgue si el código funciona.

En el caso de las pruebas automatizados significa que hay que tener todas esas expresiones almacenadas en forma de pruebas de manera que se lance un instrucción o se pulse un botón de manera que se ejecuten y comprueben todas esas expresiones. Esto sugiere:

  • Que las expresiones tienen que ser binarias. Funciona o no funciona.
  • El estado de las expresiones las comprueba el entorno llamando a alguna variantes de un método «assert()».

Las afirmaciones tienen que ser lo más precisas posibles. Si un el área de un rectángulo tiene que ser 50, di que este debería ser 50 y (assertTrue($rectangle->area() == 50)) y no algo maś genérico como (assertTrue($rectangle->area() != 0)).

Es recomendable revisar si la librería xUnit tiene una afirmación especial para comparar valores así una entrada para un mensaje de error en el que podamos indicar qué está sucediendo. Por ejemplo,



Las pruebas no deberían en ningún caso depender de la implementación por lo que si esta cambia, las pruebas, deberían seguir ejecutándose normalmente.

Fixture

Cuando tenemos varias pruebas que utilizan un objeto común podemos sobreescribir el método «setUp()» de la prueba e inicializar el objeto común.

Un ejemplo realmente simple podría ser inicializar un objeto común a varias pruebas.


En este caso podríamos deshacernos de la duplicación:


La relación entre las subclases de «TestCase» y las instancias de aquellas subclase es una de las partes más complicadas de xUnit. Cada nuevo tipo de «fixture» debería ser una nueva subclase de «TestCase». Cada nuevo «fixture» se crea en una instancia de esa subclase, se usa y se descarta.

En el ejemplo, si queremos escribir  una prueba para un rectangulo que no esté vacío entonces habrá que escribir una nueva clase, «NormalRectangleTest». En general, si hay un «fixture» ligeramente diferente entonces hay que crear una nueva clase de «TestCase».

External Fixture

La manera de liberar recursos externos en el «fixture» es sobreescribir el método «tearDown()» y liberar estos recursos.

El objetivo de las pruebas es comprobar si el código funciona pero las pruebas tienen que ejecutarse de manera independiente por lo que el estado debe ser siempre el mismo antes y después de ejecutarse una prueba.

Por ejemplo, si se abre un archivo al final de la ejecución de la prueba debería cerrarse.


Si «MyFile» se usa en varios pruebas entonces se podría hacer parte del «fixture».


Primero, si se utiliza en varias pruebas se puede apreciar que hay una duplicación de la cláusula «finally» que indica que se está perdiendo algo en el diseño. Segundo hay que cerrar el archivo mediante el método «close()» lo que se puede olvidar con cierta facilidad. Por último hay varios componentes en la prueba inicial, que son la parte «try», la parte «finally» y el propio cierre.

xUnit garantiza ejecutar el método «tearDown()» después de ejecutar el método de prueba e independientemente de lo que pase en este método. Así que se puede transformar el código anterior en el siguiente.


Test Method

Los método de prueba, por convección, se representan con un método que comienza con la palabra «test».

En el caso de utilizar «fixtures» todas las pruebas que comparten una misma «fixture» serán método de la misma clase. Las pruebas que tengan un «fixture» distinto tendrán una clase distinta.

Los métodos de prueba deberían tener un nombre descriptivo que comunique la intención de lo que se está probando.

Los método de prueba deberían ser fáciles de leer y deberían tener pocas líneas. Si un método de prueba se vuelve complejo de entender es posible que se necesite hacer varios métodos de prueba más sencillos.

Exception Test

Hay ocasiones en la que queremos probar si una excepción se lanza durante una ejecución. La prueba fallará sólo si la excepción no se lanza.


Alltest

Aunque actualmente la mayoría de IDE son capaces de ejecutar todos las pruebas.

Si se añade un prueba a un paquete y se añade un método de pruebas la siguiente vez que se ejecuten todos las pruebas también se debería ejecutar esta nueva prueba. En el caso de que el IDE no xUnit no soporte esta característica cada paquete debería declarar una clase «AllTest» que implemente un método estático «suite()» que devuelva una «TestSuite».

Se puede añadir este «AllTest» al método «main()» así la clase puede ejecutarse desde el IDE o la línea de comandos.

sábado, 30 de junio de 2018

Capítulo 31: Green Bar Patterns - TDD By Example

El objetivo del ciclo «red/green/refactor» es, principalmente, estar el menor tiempo posible en «red». Hay que salir de ese estado lo más rápido posible y se puede salir de ese estado realmente rápido aunque a veces el resultado sea un paso transitorio para hacer una refactorización posterior.

Los siguientes patrones se utilizan para salir de estado «red».

Fake it (‘Til You Make It)

La primera implementación cuando se tiene una prueba con error es devolver una constante. De esta manera se sale del estado «red», entramos en «green» y pasamos a «refactor» de manera que gradualmente se transforma la constante en un uso de variables.

En la implementación de xUnit se puede observar que en uno de los primeros pasos se devuelve la cadena como una constante:

Y se va transformando con el uso de variables:

Y más tarde:

Hay dos efectos derivados de estar en «green»:
  • Psicológico: en el estado «green» tenemos la seguridad de que todo se ejecuta correctamente. Se puede refactorizar con confianza.
  • Control del alcance: empezar con un ejemplo concreto e ir generalizando es más fácil que tratar de abarcar todos los cambios de una sola vez.

«Fake It» no vulnera la regla de no escribir código que no es necesario porque en los pasos de refactorización se elimina la duplicación de datos entre la prueba y el código. Por ejemplo:

Como se puede observar la duplicación se encuentra entre la prueba y el código. Se puede evitar cambiando el método «yesterday» para que no esté duplicado.

Se ha cambiado pero sigue habiendo duplicación. Sin embargo, desde que  «MyDate('1.3.02')» es el propio objeto «this» podemos eliminar la duplicación.

No todo el mundo está convencido de algo tan sofisticado por lo que puede utilizar «Triangulate» hasta que uno se canse y empiece a utilizar «Fake It» o incluso «Obvious Implementation».

Triangulate

Se puede hacer las abstracciones de las pruebas de una manera más conservadora si sólo se abstrae cuando se tienen dos o más ejemplos.

Por ejemplo, si queremos hacer una prueba para implementar una función la cual queremos que sume dos números tendríamos:

Si estamos triangulando entonces tendríamos que escribir otra prueba.

Con esta segunda prueba ya surge la necesidad de abstraer la solución y utilizar las variables.

La triangulación es una herramienta útil en aquellos casos en los que no se está seguro sobre la correcta abstracción. En cualquier otro caso se recomienda usar «Fake It» u «Obvious Implementation».

Obvious Implementation

Tanto «Fake It» como «Triangulation» se centran en el desarrollo en pequeños pasos. Si la implementación es tan sencilla que no necesita de pequeños pasos para realizarse entonces se puede implementar directamente. Esto es la «Obvious implementation».

Es posible que hacer un código que funcione y que a la par sea un código limpio sea demasiado para hacerlo a la vez. Si esto empieza a suceder habría que regresar para solventar la parte de que funcione y, más tarde, el código limpio.

One to Many

Cuando tenemos que implementar una operación que trabaje con colecciones de objetos la estrategia a seguir es implementar primero la operación para una objeto y después hacer que esta funcione con colecciones.

Por ejemplo, supongamos que tenemos que escribir una función que sume un array de números. Primero empezamos sumando un número.

El objetivo es sumar una colección de número así que añadimos un nuevo parámetro a la función para que admita una array de valores.

Este paso es un ejemplo de cómo aislar el cambio. Una vez añadido el parámetro en la prueba se puede cambiar la implementación sin que afecte a la ejecución de la prueba. Ahora podemos cambiar la implementación para  usar el array.

Ahora podemos eliminar el parámetro «value» que inicialmente definimos.

viernes, 29 de junio de 2018

Capítulo 30: Testing Patterns - TDD By Example


Pruebas hijas

Cuando escribimos un caso de prueba tan grande que no se puede continuar con su desarrollo por su dimensión habrá que abordarlo de manera que se escriba un caso de prueba más pequeño que represente parte del caso de prueba más grande. Una vez que el caso de prueba más pequeño está funcionando entonces se reintroduce el caso de prueba más grande.

El ciclo «red/green/refactor» es tan importante para el éxito del desarrollo que cuando se está en riesgo de perderlo merece la pena el esfuerzo de mantenerlo.

Cuando se escribe una prueba demasiado larga hay que tratar de aprender y analizar el porqué ha sido demasiado larga.

Por ejemplo, si en un caso de uso hay que desarrollar tres elementos (A, B y C) podría darse el caso de que desarrollarlas a la vez fuera demasiado complejo. Si se desarrollaran por separado A, B y C el caso de uso sería mucho más sencillo de implementar.

Objetos simulados o Mock objects

¿Como se prueba un objeto que depende de un recurso caro o complicado? Pues creando una versión simulada de ese recurso que contenga datos predefinidos.

Un ejemplo clásico de un recurso para simular es la base de datos. Esta requiere de un tiempo para empezar, es difícil de mantener limpia, etc.

La solución más común es no usar la base de datos real si no escribir en un objeto que actúe como la base de datos pero que esté utilizando realmente la memoria.

Otra ventaja de los objetos simulados, además del rendimiento y de la confiabilidad, es la legibilidad ya que este tendrá definidos los datos que se van a utilizar. Al utilizar una base de datos esta podría contener resultados de otras pruebas y, por ejemplo, una consulta podría devolver 14 elementos y no se sabría realmente porqué tendría que devolver 14 resultados como respuesta correcta.

Los objetos simulados añaden un riesgo ya que podrían no comportarse como lo haría el recurso real. Esto podría solucionarse si este tuviera un conjunto de pruebas para que coincida con los resultado del recurso que se está simulando cuando este esté disponible.

Self-Shunt

En los casos en los que haya que probar si un objeto se comunica correctamente con otro se podría utilizar un objeto simulado («mock object») para comprobar si la comunicación es la correcta. Otra opción es utilizar la propia clase de prueba en vez del objeto simulado o de algún «mock framework».

Por ejemplo, se quiere actualizar de forma dinámica la barra verde de la interfaz de pruebas del usuario. Si se pudiera conectar un objeto «ResultListener» al objeto «TestResult», este podría notificar al primero si la prueba falla, si ha finalizado, etc. Cada vez que se ejecuta una prueba se debe actualizar la interfaz.

La prueba podría escribirse de la siguiente manera.


El objeto que va a contar las notificaciones.


Aunque realmente, si la clase «ResultListener» se está desarrollando como un objeto simulado podemos utilizar la propia clase de la prueba para que haga la misma función.


Las pruebas que están escritas con el patrón «Self Shunt» tienden a leerse mejor. En el ejemplo anterior se puede apreciar que el valor inicial de la cuenta es 0 y después pasa a 1. ¿Cuando se ejecuta el método «startTest()»? Este debe ejecutarse cuando se ejecutan las pruebas.

Es posible que aplicando «Self Shunt» haya que crear algún tipo de interfaz para definir el comportamiento del objeto con el que se quiere comunicar.

Puede darse el caso de que la interfaz vaya añadiendo comportamiento por lo que este comportamiento debe ser implementado en aquellas clases que la utilicen, incluida la clase de pruebas. Esto puede dar lugar a multitud de implementaciones en la clase dejando la clase un poco menos legible.

Trazas de cadenas Log string

En ocasiones tendremos el problema de saber si una secuencia de mensajes se está ejecutando en el orden adecuado. Para ello se puede utilizar un «log» con las cadenas de los mensajes.

Hay un ejemplo de este tipo de estrategia durante el desarrollo de xUnit.

Las cadenas de «log» son especialmente útiles cuando se quiere implementar «Observers» y se esperan una secuencia de notificaciones en un orden determinado. En los casos en los que el orden no sea importante se puede guardar un conjunto de cadenas de manera que se compare el conjunto en la afirmación.

Crash test dummy

Los «crash test dummy» se utilizan como un objeto especial que se encarga de lanzar las excepciones que podrían darse en situaciones excepcionales de error.

Por ejemplo, si queremos probar que ocurre cuando nuestra aplicación quiere guardar un archivo y el disco duro está lleno. El enfoque no consistiría en crear muchos ficheros grandes hasta llenar el disco duro sino en simular el efecto.

El «crash test dummy» podría ser el siguiente.


Por lo que se podría escribir la prueba.


Los objecto Los «crash test dummy» son como un «Mock object» a excepción de que no se necesita simular el objeto entero. Se puede sobreescribir el método que se necesita simular.

Broken Test

Cuando uno está programando en solitario y se termina una sesión de programación se recomienda dejar la última prueba que no se ejecute. De esta manera, cuando se retome el desarrollo en la siguiente sesión hay un punto de partida obvio para empezar y es más fácil retomar el estado mental en el que se dejó la sesión previa.

Clean Check-in

Cuando se está programando en equipo y se termina una sesión, todos las pruebas del equipo deben poder ejecutarse.

Al comenzar una sesión de programación no se sabe cómo habrá cambiado el código por lo que el lugar ideal para empezar es un lugar confianza y certeza. Este lugar es aquel en el que todos las pruebas se pueden ejecutar sin errores.

martes, 26 de junio de 2018

Capítulo 29: Test-Driven Development

Hay algunas preguntas estratégicas básicas que necesitan respuesta antes de poder hablar sobre los detalles de como probar:

    • ¿Qué significa probar?
    • ¿Cuando probar?
    • ¿Cómo elegir qué lógica probar?
    • ¿Cómo elegir qué datos probar?

Probar N

La manera de probar el software es escribir pruebas automatizadas.

Cuando se desarrolla una nueva funcionalidad se pueden probar los cambios pero probar esos cambios no es lo mismo que tener pruebas. Por ejemplo, las pruebas se ejecutan automáticamente mientras que probar se hace de manera manual, por ejemplo buscar un botón en una interfaz.

Las pruebas aportan seguridad cuando se realizan cambios ya que advierten si se ha producido un error. Cuanto más miedo de producir un error más veces habrá que ejecutar las pruebas, de esta manera se reducen el número de errores y además se reduce el estrés de los programadores.

Aislar las pruebas

Las pruebas deberían ser independientes entre sí de manera que el resultado de unas no influya en otras. Se podría dar el caso de que, al compartir algunos datos, unas pruebas dejaran inconsistente el sistema haciendo fallar otra o, lo que es peor, validando las pruebas cuando no debería hacerlo.

Por esta razón las pruebas deberían estar aisladas. Esto conlleva a que el orden de ejecución puede ser independiente.

Las pruebas deberían ejecutarse lo más rápido posible de manera que se puedan ejecutar a menudo.

Listado de pruebas

El listado de pruebas es, tal y como indica, un listado donde se van a incluir todas las pruebas que, inicialmente, hay que hacer.

Esto tiene una ventaja y es que una vez seleccionada la prueba que se quiere desarrollar se puede centrar toda la atención en esa prueba. Si surge una prueba nueva o una nueva tarea, se apunta en el listado para más tarde valorar cuando desarrollarlo.

Así que en la lista se colocan los ejemplos de cada operación que se sabe que se se necesita implementar. Seguido se incluyen aquellas operaciones que no existen todavía pero que se necesitarán en un futuro. Para finalizar se incluyen las refactorizaciones que se piensa que se tendrán que realizar para limpiar el código.

Una vez terminado el listado de pruebas inicial se podría plantear el implementar todas ellas aunque no se recomienda en absoluto. La primera razón es porque con cada prueba que se implementa hay que hacer una refactorización. Si se hacen diez pruebas la refactorización va a ser más complicada. La segunda razón viene dada cuando hay diez pruebas rotas. Es mucho más complejo tratar de conseguir solucionar diez pruebas que una. Hay que tratar de evitar estar en rojo lo menos posible.

Una vez que la prueba se ejecuta correctamente, es posible que la implementación implique nuevas pruebas. Estas deben incluirse al final del listado de pruebas. Lo mismo para las refactorizaciones, como por ejemplo «esto se está viendo feo».

Cuando se termina una sesión de programación hay que tener cuidado de aquellas pruebas que están pendientes y tienen prioridad en la lista. Se pueden usar varias listas, dependiendo de las prioridades, por ejemplo, se puede tener una lista «now» para las pruebas más prioritarias y otra lista «later» para pruebas que lo son menos o no es el momento de abordarlas. Una refactorización de cierta envergadura que no impacte en la prueba que se está desarrollando debería incluirse en la lista «later» para abordarla en un paso posterior.

En aquellos casos en los que se piensa que puede haber una prueba que no está implementado aún y que podría no funcionar entonces esta prueba debería tener prioridad incluso sobre desplegar el código.

Probar primero

¿Cuando habría que escribir la prueba? Siempre se deberían la prueba antes de escribir el código que tiene que probarse.

En los casos en los que de desarrolla la implementación sin realizar las pruebas se empieza a añadir un estrés a la hora de probar de forma manual. A medida que el sistema va añadiendo funcionalidades el estrés va aumentado dado que, posiblemente, no se haya probado lo suficiente.

Para romper ese patrón se puede escribir una prueba antes de escribir el código que tiene que probarse. De esta manera se reduce el estrés de probar.

La ventaja inmediata es para el TDD es que se empieza y se mantiene haciendo bajo un estrés moderado.

Afirmación primero

¿Cuando debería escribirse la afirmación? Debería ser lo primero en escribirse.

Escribir la afirmación primero simplifica el proceso de escribir una prueba. Cuando se escribe una prueba se están resolviendo varios problemas a la vez incluso si no se ha pensado sobre la implementación.

    • ¿Dónde pertenece la funcionalidad? ¿Es una modificación de un método existente, un método nuevo en una clase que ya existe, en una clase nueva?
    • ¿Qué nombres deberían utilizarse?
    • ¿Cómo se va validar la respuesta correcta?
    • ¿Qué es la respuesta correcta?
    • ¿Qué otros pruebas sugiere esta prueba?

Tratar de abordar todos estos problemas a la vez es, cuanto menos, una osadía para la mayoría de la gente por lo que separar el problema en partes más pequeñas es una buena  estrategia. Las preguntas «¿qué es la respuesta correcta?» y «¿cómo se va a validar la respuesta correcta?» son dos problemas que se pueden separar fácilmente del resto y pueden servir como punto de partida para el resto.

Por ejemplo, en el caso de querer comunicarnos con otro sistema a través de un «socket» y que, una vez hecha la comunicación el «socket» debería cerrarse y deberíamos haber leído la cadena «abc».

El primer paso sería escribir las afirmaciones.


Una vez que están escritas, resolvemos el problema de dónde viene «reply» qué será del «socket» claro está.


¿Y «socket»? Este vendrá dado por una conexión a un servidor.


Y antes de esto necesitamos crear un servidor.


De esta manera se crea el guión de la prueba en pequeños pasos.

Datos de prueba

El objetivo de los datos de prueba es que hay que sean fáciles de leer y entender.

Si hay diferencia entre datos entonces estos deberían tener un significado, o sea, que si no hay una diferencia conceptual entre 1 y 2 entonces se debería usar 1.

Si un sistema tiene múltiples entradas de datos y con 3 datos se cubren las distintas posibilidades, o sea, los distintos conceptos entonces no tiene sentido tener un lista de 10 datos como entrada.

Un truco es tratar de que la misma constante signifique más de una cosa.

La alternativa a los datos de prueba son los datos reales y estos son útiles cuando:

    • Se hacen pruebas en sistemas de tiempo real usando trazas de eventos externos.
    • Si se está comparando la salida del sistema actual con la salida de un sistema previo («Parallel Testing»).
    • Si se está refactorizando una simulación y se espera precisamente la misma respuesta cuando ha finalizado, particularmente si la precisión de los números con coma flotante puede ser un problema.

Datos evidentes

Los datos que se utilizan en la prueba deben representar su intención de una manera clara. Las pruebas se escriben para personas.

El valor esperado y el valor actual deben tener relación aparente. Por ejemplo, en el caso de hacer un cambio de una moneda a otra con una tasa de cambio por parte del banco de un 1,5%, si cambiamos 100USD a GBP y el cambio está a 2:1 entonces el resultado debería ser 50GBP-1,5% = 49.25GBP.


En este caso aparece el número mágico 49.25, que podemos evitar haciendo los cálculos.



En este ejemplo se puede observar cómo se calcula el resultado esperado.

Los datos evidentes tienen el beneficio de hacer la programación más fácil. Una vez que vemos la expresión en la afirmación ya sabemos que tenemos que programar. Además, tenemos la ventaja de poder utilizar datos falsos para poder pasar la prueba e ir evolucionando esta de manera incremental.

Los datos evidentes se pueden ver como una excepción a la regla de los números mágicos. Dentro del alcance de un método la relación entre estos números es obvia. En el caso de tener definidas constantes simbólicas habría que utilizar estas constantes simbólicas.