TDD en Castellano
TDD en Castellano
TDD en Castellano
Edición 2020
Este es un libro de Leanpub. Leanpub anima a los autores y publicadoras con el proceso de
publicación. Lean Publishing es el acto de publicar un libro en progreso usando herramientas
sencillas y muchas iteraciones para obtener feedback del lector hasta conseguir tener el libro
adecuado.
Prólogo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Prefacio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Agradecimientos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Test mantenibles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Larga vida a los test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Principios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Nombrando las pruebas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Claros, concisos y certeros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Agrupación de test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Los test automáticos no son suficiente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
Test basados en propiedades . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Criterios de aceptación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
Las aserciones confirman las reglas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
Los criterios de aceptación no son ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
Mock Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Mock y Spy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
Uso incorrecto de mocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
Stubs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
ÍNDICE GENERAL
Combinaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
Ventajas e Inconvenientes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
Otros tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
Código legado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
Estilos y Errores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
Outside-in TDD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
Inside-out TDD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
Combinación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
Errores típicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
me hacía más y más rápido y el retorno era evidente. Empecé a desarrollar un ritmo que nunca había
tenido. El bucle rojo-verde-refactor era totalmente adictivo. Esa maravillosa sensación de que estas
continuamente progresando y siempre con el código bajo control, siempre sabiendo lo que estaba
hecho y lo que faltaba por hacer. Ya no me parecía lento. Programar en pares fue también mágico.
Cuanto más programaba con compañeros más aprendíamos todos. Y entonces aprendes diferentes
estilos de tests, aprendes a testar a diferentes niveles, aprendes mocks y muchas otras técnicas.
Este libro que estas leyendo contiene muchas de las lecciones que hemos aprendido por el camino
difícil. Carlos Blé hace un gran trabajo reuniendo diferentes técnicas y enfoques de TDD, todo
mezclado con su propia experiencia en la materia. Este libro te proporcionará una base sólida para
empezar en este fascinante camino hacia el Diseño Ágil con TDD.
Sandro Mancuso - Software Craftsman / Managing Director at Codurance.
Prefacio
Cuando descubrí el valor de TDD sentí la necesidad de contarlo a los demás y tras fracasar en el
intento de traducir un libro de Kent Beck me dispuse a publicar mi propio libro. En esta década que ha
transcurrido desde mi primer libro, me he encontrado con personas que han tenido la amabilidad de
contarme que aquel libro les abrió la puerta a una nueva forma de trabajar. Recibir agradecimientos
y reconocimiento por el trabajo realizado es la mejor recompensa que se puede obtener. Provoca
sentimientos de gratitud recíproca y motivación para seguir trabajando con el espíritu de aportar
valor a los demás. Al pasar el tiempo supe que quería ofrecerles algo mejor que mi primer libro. Tenía
poca experiencia en la materia cuando lo escribí y había demasiadas cosas que no me gustaban. Pese a
que aquel libro era gratuito, sólo llegó a ser conocido en contadas instituciones académicas públicas.
Una de mis intenciones con este nuevo trabajo es que llegue a alumnas y alumnos que aspiran a
trabajar como developers en el futuro. Sobre todo porque pasados unos años serán mis compañeras
y compañeros y trabajaremos mejor juntos si ya saben escribir buenos test.
Agradecimientos
Este libro no existiría sin el apoyo de mis seres queridos, que me quieren a pesar de que conocen
bien y sufren mis muchos defectos y mis errores. Son quienes me dan la energía para levantarme
cada mañana y vencer a la resistencia.
Tampoco sería posible sin la ayuda de mi gran equipo, Lean Mind, que me ha tenido la paciencia
y el respeto que necesitaba para escribir a pesar de la gran carga de trabajo que tenemos. Mención
especial por las correcciones y sugerencias a: Mireia Scholz, Samuel de Vega, Ricardo García, Cristian
Suárez, Adrián Ferrera, Viviana Benítez y Juan Antonio Quintana.
Gracias a todas las personas que me han enviado correcciones y sugerencias de mejora durante la
edición: Dácil Casanova, Luis Rovirosa, Miguel A. Gómez y Adrià Fontcuberta.
Agradezco al maestro Sandro Mancuso el detallazo de escribir el prólogo de este libro. Es para mi
un honor.
Estoy muy agradecido a Vanesa González¹ por su lindo trabajo con la portada del libro. Su diseño
original es la mejor forma de vestir este libro. Le auguro grandes éxitos como diseñadora.
Gracias a todas las instituciones que deciden utilizar este libro como material para la docencia,
especialmente a mi amigo Jose Juán Hernández por abrirme las puertas de la ULPGC.
Y por último pero no menos importante, gracias a la comunidad. A ella dedico este nuevo libro
porque sin el marco de crecimiento profesional y personal que nos ofrecen las comunidades de
práctica, al menos en nuestro sector, no hubiera llegado tan lejos en mi carrera.
¹http://vanesadesigner.com/
¿Qué es Test-Driven Development?
Programación Extrema
TDD es una de las prácticas de ingeniería más conocida de XP (eXtreme Programming). XP es un
amplio método de desarrollo de software que abarca desde la cultura de las relaciones entre las
personas hasta técnicas de programación. Sus pilares son sus valores:
• Simplicidad
• Comunicación
• Feedback
• Respecto
• Valor
Las prácticas de XP son las herramientas que permiten a los miembros del equipo entender
y promover estos valores. Conectan lo abstracto de los valores con lo concreto de los hábitos.
Fundamentalmente las prácticas son:
• TDD
• Programación en pares
• Refactoring
• Integración Continua
Además de las prácticas, existen una serie de principios o reglas que conectan con los valores. Estos
son algunos de esos principios:
El método tiene su origen en la década de 1990. Fue introducido por el innovador programador y
escritor americano Kent Beck, con la ayuda de sus compañeros Ward Cunningham y Ron Jeffries.
En su libro Extreme Programming Explained: Embrace Change², Kent Beck explica con gran detalle
la filosofía de XP, la cual se basa en las personas, sus capacidades, sus necesidades y las relaciones
entre ellas en los equipos de desarrollo. En este libro no se explica la técnica, no contiene listados
de código fuente sino que se centra en los valores y principios que dan sentido a las prácticas. La
primera edición es de 1999 y la segunda de 2004. Para profundizar en las prácticas, Beck publicó en
2002 un libro específico de TDD llamado Test-Driven Development by Example³. Completando la
bibliografía sobre prácticas de ingeniería de XP, su amigo y colaborador Martin Fowler, programador
y escritor británico, publicó en 2002 el libro Refactoring, Improving the Design of Existing Code⁴.
Refactoring es una de las prácticas fundamentales de XP y puede aplicarse independientemente de
TDD si bien lo contrario no es cierto, TDD no puede llevarse a cabo sin Refactoring. Lo ideal es
refactorizar con el respaldo de unos test automáticos que nos cubran. Tales test podrían haber sido
escritos con TDD o bien pueden haberse añadido posteriormente o incluso escribirse justo antes
de iniciar el refactor. El libro de Martin Fowler ha tenido tanto impacto y éxito en el desarrollo
de software moderno que la mayoría de los Entornos de Desarrollo Integrado ofrecen opciones
de automatización del catálogo de recetas de refactoring original: Rename, Extract method, Inline
method, Extract class… fue el primer libro que leí sobre metodologías ágiles y me fascinó.
Refactorizar o hacer refactor o refactoring, como quiera que se le diga, consiste en cambiar el código
del sistema sin cambiar su funcionalidad. Ni añadir, ni restar funcionalidad al sistema, sólo cambiar
detalles de su implementación. No significa eliminar por completo el código y volverlo a escribir
sino realizar migraciones de bloques de código, a ser posible pequeños. Cuanto más frecuentemente
se practica refactoring, más fácil resulta. Lo ideal es dedicarle unos minutos todos los días, es como
limpiar y ordenar la casa. El objetivo es simplificar el código para hacerlo más fácil de entender para
quien lo lea.
Desde entonces varios autores como Ron Jeffries y otros firmantes del Manifiesto Ágil⁵ como Dave
Thomas, Andy Hunt, han escrito libros de éxito relacionados con prácticas ágiles de ingeniería.
Destaca especialmente Robert C. Martin por el éxito de su libro Clean Code⁶. Martin Fowler es el
que cuenta con mayor número de publicaciones. El último libro técnico publicado por Kent Beck
hasta la fecha de este escrito es Implementation Patterns⁷, un completo catálogo de principios de
codificación.
La capacidad de innovar y de redescrubir de Kent Beck, plasmada en sus libros, artículos, ponencias
y otros recursos, le han llevado a ser uno de los programadores más prestigiosos del mundo.
²https://www.amazon.es/Extreme-Programming-Explained-Embrace-Embracing/dp/0321278658
³https://www.amazon.es/Driven-Development-Example-Addison-Wesley-Signature/dp/0321146530
⁴https://martinfowler.com/books/refactoring.html
⁵https://agilemanifesto.org/
⁶https://www.amazon.es/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882
⁷https://www.amazon.es/Implementation-Patterns-Addison-Wesley-Signature-Kent/dp/0321413091
¿Qué es Test-Driven Development? 7
excepto la primera de todas que contiene el nombre de los campos. Ejemplo de fichero:
• Es válido que algunos campos estén vacíos (apareciendo dos comas seguidas o una coma final)
• El número de factura no puede estar repetido. Si lo estuviese eliminaríamos todas las líneas con
repetición.
• Los impuestos IVA e IGIC son excluyentes, sólo puede aplicarse uno de los dos. Si alguna línea
tiene contenido en ambos campos debe quedarse fuera.
• Los campos CIF y NIF son excluyentes, sólo se puede usar uno de ellos.
• El neto es el resultado de aplicar al bruto el correspondiente impuesto. Si algún neto no está
bien calculado se queda fuera.
Además de las reglas de negocio los programadores debemos ponernos también el sombrero de tester
y pensar en casos extraños o anómalos y en cuál debería ser la respuesta del sistema ante ellos. Para
que cuando se produzcan no se detenga el programa sin más. Las leyes de Murphy aplican con
frecuencia en los proyectos de software. A veces, las dudas que surgen explorando casos límite hay
que trasladarlas incluso a los expertos de negocio porque se puede abrir una caja de pandora. Por
ejemplo, ¿qué hacemos si la primera línea de cabecera con los nombres de los campos no está? ¿se
puede dar el caso de que algún fichero venga con los campos ordenados de otra forma? ¿qué sucede
si hay más campos que nos resultan desconocidos? Cuanto antes nos anticipemos a lo malo que
puede ocurrir, mejor. Con toda la información procedemos a ordenar una lista de posibles test que
queremos hacer en base a su dificultad:
• Un fichero con una sola factura donde todo es correcto, debería producir como salida la misma
línea
• Un fichero con una sola factura donde IVA e IGIC están rellenos, debería eliminar la línea
• Un fichero con una sola factura donde el neto está mal calculado, debería ser eliminada
• Un fichero con una sola factura donde CIF y NIF están rellenos, debería eliminar la línea
• Un fichero de una sola línea es incorrecto porque no tiene cabecera
• Si el número de factura se repite en varias líneas, se eliminan todas ellas (sin dejar ninguna).
• Una lista vacía o nula producirá una lista vacía de salida
Y la lista de test aún podría completarse con un buen puñado de casos más. No es casualidad que
haya elegido los primeros ejemplos con una sola factura, porque así me evito tener que visitar los
elementos de la lista de partida. Típicamente, las soluciones que trabajan con colecciones tienen
tres variantes significativas: no hay elementos, hay un elemento o hay más de un elemento. Si me
¿Qué es Test-Driven Development? 9
centro en la lógica de validación hasta que la tenga completada, puedo ocuparme de la duplicidad
de elementos después. Así, en cada test me centro en un único comportamiento del sistema y no
tengo que estar pensando simultáneamente en todas las variantes, lo cual reduce enormemente la
carga cognitiva del trabajo. De esta forma no tengo que estar ejecutando el programa en mi cabeza
constantemente mientras lo escribo. Es un gran alivio y me permite enfocarme muy bien en la
tarea que estoy haciendo para que el código sea conciso y preciso. Cuando juegas al ajedrez debes
mantener en la cabeza las posibles vulnerabilidades a que está expuesta cada una de tus fichas en el
tablero. Programar sin TDD para mí era un poco así. Me dejaba muchos casos sin cubrir por un lado
y, por otro, solía complicar la solución mucho más de lo que era necesario. Cuando uso TDD tengo
mi lista de test apuntada como recordatorio y me puedo centrar en una única pieza del tablero. Si
descubro nuevas variantes por el camino las apunto en mi lista y me despreocupo hasta que le llegue
el turno. Es metódico, ordenado y enfocado como un láser.
Para este primer ejemplo voy a usar el lenguaje Kotlin y JUnit con la librería de aserciones AssertJ.
Empiezo escribiendo el primer test:
1 package csvFilter
2 import org.assertj.core.api.Assertions.assertThat
3 import org.junit.Test
4 class CsvFilterShould {
5 @Test
6 fun allow_for_correct_lines_only(){
7 val headerLine = "Num_factura, Fecha, Bruto, Neto, IVA, IGIC, Concepto, CIF_\
8 cliente, NIF_cliente"
9 val invoiceLine = "1,02/05/2019,1000,810,19,,ACER Laptop,B76430134,"
10
11 val result = CsvFilter().filter(listOf(headerLine, invoiceLine))
12
13 assertThat(result).isEqualTo(listOf(headerLine, invoiceLine))
14 }
15 }
Puedo escribir el test sin que exista la clase CsvFilter, simplemente resulta que no va a compilar
pero mi intención la puedo plasmar en el test desde que tengo claro el comportamiento del sistema.
Ahora hago el código mínimo para que compile y pueda ejecutar el test a fin de verlo fallar:
¿Qué es Test-Driven Development? 10
1 package csvFilter
2 class CsvFilter {
3 fun filter(lines: List<String>) : List<String> {
4 return listOf()
5 }
6 }
Compila y falla tal como esperaba. Ejecutar el test para verlo en rojo es esencial para detectar errores
en el test. Me ha pasado mil veces que he escrito un test que no era correcto sin darme cuenta, bien
porque le faltaba un assert o porque el assert decía lo contrario de lo que debía. También me ha
pasado que, sin darme cuenta, estoy ejecutando sólo un test en lugar de los N test que tengo y
resulta que el último que he añadido no se está ejecutando porque se me ha olvidado marcarlo como
test. Ver el test fallar cuando espero que falle, es un metatest, es asegurarme que el test está bien
hecho. Hay que verlo en rojo y además fijarse en que, si es un test nuevo, el número de test total se
incrementa en uno. Es muy importante hacerlo sobre todo cuando ya existen varios test escritos.
Alguien que no conozca TDD tendría la tentación de ponerse a escribir el código al completo de
esta función para que cumpla con todos los requisitos. Error. El objetivo es hacer el código mínimo
para que este test pase, no más. En la siguiente sección explicaremos por qué. La idea es que
completaremos el código poco a poco con cada test. En los primeros test tiene una implementación
muy concreta pero, conforme añadimos más, va siendo más genérica para poder gestionar todos los
casos a la vez. Vamos a hacer que el test pase lo antes posible:
1 package csvFilter
2 class CsvFilter {
3 fun filter(lines: List<String>) : List<String> {
4 return lines
5 }
6 }
¡Verde! El código no es completo pero ya funciona bien para los casos en los que todas las líneas del
fichero de entrada sean correctas. Ciertamente es incompleto, con lo cual debemos añadir más test.
1 @Test
2 fun exclude_lines_with_both_tax_fields_populated_as_they_are_exclusive(){
3 val headerLine = "Num_factura, Fecha, Bruto, Neto, IVA, IGIC, Concepto, CIF_\
4 cliente, NIF_cliente"
5 val invoiceLine = "1,02/05/2019,1000,810,19,8,ACER Laptop,B76430134,"
6
7 val result = CsvFilter().filter(listOf(headerLine, invoiceLine))
8
9 assertThat(result).isEqualTo(listOf(headerLine))
10 }
Rojo porque devuelve la misma lista que en la entrada. Pasemos a verde con el mínimo esfuerzo:
¿Qué es Test-Driven Development? 11
1 class CsvFilter {
2 fun filter(lines: List<String>) : List<String> {
3 val result = mutableListOf<String>()
4 result.add(lines[0])
5 val invoice = lines[1]
6 val fields = invoice.split(',')
7 if (fields[4].isNullOrEmpty() || fields[5].isNullOrEmpty()){
8 result.add(lines[1])
9 }
10 return result.toList()
11 }
12 }
¡Verde! Al escribir este código tan simple y explícito, me acabo de dar cuenta de que podría darse
el caso de que ninguno de los dos campos de impuestos, IVA e IGIC estuviesen rellenos. Tengo que
preguntarle a los expertos de negocio, ¿qué hacemos con esas facturas?. Resulta que nos dicen que
eliminemos las líneas donde faltan los dos campos, así que añado un nuevo test para ello:
1 @Test
2 fun exclude_lines_with_both_tax_fields_empty_as_one_is_required(){
3 val headerLine = "Num_factura, Fecha, Bruto, Neto, IVA, IGIC, Concepto, CIF_\
4 cliente, NIF_cliente"
5 val invoiceLine = "1,02/05/2019,1000,810,,,ACER Laptop,B76430134,"
6
7 val result = CsvFilter().filter(listOf(headerLine, invoiceLine))
8
9 assertThat(result).isEqualTo(listOf(headerLine))
10 }
Ejecuto los tres que llevamos escritos hasta ahora para comprobar que los dos primeros están en
verde y este último en rojo. Correcto. Vamos a enmendarlo:
1 class CsvFilter {
2 fun filter(lines: List<String>) : List<String> {
3 val result = mutableListOf<String>()
4 result.add(lines[0])
5 val invoice = lines[1]
6 val fields = invoice.split(',')
7 if ((fields[4].isNullOrEmpty() || fields[5].isNullOrEmpty()) &&
8 (!(fields[4].isNullOrEmpty() && fields[5].isNullOrEmpty()))){
9 result.add(lines[1])
10 }
¿Qué es Test-Driven Development? 12
11 return result.toList()
12 }
13 }
Los condicionales con operaciones lógicas las carga el diablo. Me equivoco siempre con ellas. Menos
mal que tengo test escritos para todos los casos que llevamos. El código de producción y los test,
empiezan a necesitar un poco de limpieza. Hacemos un poco de refactor para aclararle bien lo que
estamos haciendo a la programadora que tenga que mantener esto en el futuro:
1 class CsvFilter {
2 fun filter(lines: List<String>) : List<String> {
3 val result = mutableListOf<String>()
4 result.add(lines[0])
5 val invoice = lines[1]
6 val fields = invoice.split(',')
7 val ivaFieldIndex = 4
8 val igicFieldIndex = 5
9 val taxFieldsAreMutuallyExclusive =
10 (fields[ivaFieldIndex].isNullOrEmpty() ||
11 fields[igicFieldIndex].isNullOrEmpty()) &&
12 (!(fields[ivaFieldIndex].isNullOrEmpty()
13 && fields[igicFieldIndex].isNullOrEmpty()))
14 if (taxFieldsAreMutuallyExclusive){
15 result.add(lines[1])
16 }
17 return result.toList()
18 }
19 }
He aplicado el refactor “Introduce explaining variable” para darle un nombre a las operaciones que
estoy realizando. Así me evito tener que poner un comentario en el código para explicar algo que
puedo perfectamente explicar con código. Los comentarios me los reservo para la información que
el código no puede expresar, como por ejemplo el contexto que justifica tal código, el por qué, para
qué, por qué no…
Los test también se pueden limpiar, por ejemplo, moviendo la variable headerLine al ámbito de la
clase porque está repetida en los tres test.
Ahora se me ocurre otro caso extraño, que el campo IVA tuviese letras en lugar de números. Vamos
a protegernos de ese caso:
¿Qué es Test-Driven Development? 13
1 @Test
2 fun exclude_lines_with_non_decimal_tax_fields(){
3 val invoiceLine = "1,02/05/2019,1000,810,XYZ,,ACER Laptop,B76430134,"
4
5 val result = CsvFilter().filter(listOf(headerLine, invoiceLine))
6
7 assertThat(result).isEqualTo(listOf(headerLine))
8 }
Falla este test y pasan todos los demás. Si nos fijamos, le estamos dando un único motivo de fallo
porque lo otros campos son correctos. Esto es muy importante. En la medida de lo posible trato de
no mezclar casos de manera que el test sólo tiene un motivo para fallar. En el test de antes podría
haber buscado un ejemplo donde hubiese otra regla que se incumpliera como, por ejemplo, que
tanto IVA como IGIC estuvieran rellenos. Pero entonces, cuando fallase, no estaría seguro de si es
porque hay letras o si es porque los dos campos están rellenos. Cuanto más precisos sean los test,
señalando el motivo de fallo, antes lo podremos corregir. Los test son rentables cuando cumplen este
tipo de características dado que nos permiten ganar tiempo en el desarrollo en el medio y largo plazo.
No vale con escribir cualquier test, porque en el largo plazo se pueden volver en nuestra contra e
impedirnos cambiar el código fuente cuando se rompen constantemente sin un verdadero motivo
para hacerlo. Con un poquito de ayuda de StackOverflow he decidido usar una expresión regular
para que el test pase, con muy poco esfuerzo:
1 class CsvFilter {
2 fun filter(lines: List<String>) : List<String> {
3 val result = mutableListOf<String>()
4 result.add(lines[0])
5 val invoice = lines[1]
6 val fields = invoice.split(',')
7 val ivaFieldIndex = 4
8 val igicFieldIndex = 5
9 val ivaField = fields[ivaFieldIndex]
10 val igicField = fields[igicFieldIndex]
11 val decimalRegex = "\\d+(\\.\\d+)?".toRegex()
12 val taxFieldsAreMutuallyExclusive =
13 (ivaField.matches(decimalRegex) ||
14 igicField.matches(decimalRegex)) &&
15 (!(ivaField.matches(decimalRegex)
16 && igicField.matches(decimalRegex)))
17 if (taxFieldsAreMutuallyExclusive){
18 result.add(lines[1])
19 }
20 return result.toList()
¿Qué es Test-Driven Development? 14
21 }
22 }
El código se va haciendo más complejo. ¿Se me habrían ocurrido todos estos casos si no hubiese
seguido el proceso TDD, es decir, si no me hubiera ceñido estrictamente a la mínima implementación
para que el test pase? En mi caso, no. Y no me puedo engañar a mí mismo. De hecho, se me acaba
de ocurrir un test que falla:
1 @Test
2 fun exclude_lines_with_both_tax_fields_populated_even_if_non_decimal(){
3 val invoiceLine = "1,02/05/2019,1000,810,XYZ,12,ACER Laptop,B76430134,"
4
5 val result = CsvFilter().filter(listOf(headerLine, invoiceLine))
6
7 assertThat(result).isEqualTo(listOf(headerLine))
8 }
1 package csvFilter
2
3 class CsvFilter {
4 fun filter(lines: List<String>) : List<String> {
5 val result = mutableListOf<String>()
6 result.add(lines[0])
7 val invoice = lines[1]
8 val fields = invoice.split(',')
9 val ivaFieldIndex = 4
10 val igicFieldIndex = 5
11 val ivaField = fields[ivaFieldIndex]
12 val igicField = fields[igicFieldIndex]
13 val decimalRegex = "\\d+(\\.\\d+)?".toRegex()
14 val taxFieldsAreMutuallyExclusive =
15 (ivaField.matches(decimalRegex) || igicField.matches(decimalRegex)) &&
16 (ivaField.isNullOrEmpty() || igicField.isNullOrEmpty())
17 if (taxFieldsAreMutuallyExclusive){
18 result.add(lines[1])
19 }
20 return result.toList()
21 }
22 }
¿Qué es Test-Driven Development? 15
Dejamos el primer ejemplo por ahora para analizarlo en más detalle en las próximas secciones y
capítulos. Para sacarle el máximo partido a este ejercicio, mi propuesta es que usted termine de
implementar la función filter con TDD, hasta el final, antes de seguir leyendo el libro. Terminarla
significa ir añadiendo los test de todos los casos posibles hasta conseguir un código listo para
desplegar en producción. De esta forma le asaltaran dudas que quizás están resueltas más adelante
en el texto y cuando continúe leyendo, podrá reflexionar sobre su trabajo y comparar. Existe un
repositorio de código para este ejemplo y se encuentra en Github⁸. Contiene al menos un commit
por cada test en verde. No está completo en dicho repositorio, es sólo un punto de partida.
El código que tenemos por ahora no es precisamente el mejor ejemplo de código limpio, ¿deberíamos
refactorizar para mejorarlo? ¿deberíamos seguir las recomendaciones de Robert C. Martin de
que las funciones tengan pocas líneas? La experiencia me ha enseñado que conviene refactorizar
progresivamente. Hacer demasiados cambios en el código cuando todavía se encuentra en una fase
de implementación temprana, dificulta el progreso. Tiende a hacer el código más complejo, a menudo
introduciendo abstracciones incorrectas. Durante el proceso de TDD me limito a aplicar los refactors
que aportan una mejora evidente de legibilidad del código, como extraer una variable, una constante,
renombrar una variable… a veces puede ser extraer un método pero como esto introduce indirección
y por tanto potencialmente más complejidad, procuro no precipitarme. Una vez que la funcionalidad
se ha implementado por completo, cumpliendo con todos los casos, estudio si tiene sentido extraer
métodos para convertirla en una función más pequeña, o quizás extraer una clase, o cualquier otro
cambio de diseño de mayor dimensión. Los mejores ajustes en el diseño se hacen cuando se adquiere
mayor conocimiento sobre el negocio y la solución y esto típicamente ocurre cuando el software ya
está corriendo en producción y tenemos feedback de los usuarios. Por eso es preferible no darle
demasiadas vueltas de tuerca al código en fases tempranas del desarrollo. Puesto que contamos
con baterías de test automáticos, siempre podremos releer el código y aplicar mejoras cada vez que
detectemos que las abstracciones y las metáforas que hemos introducido pueden confundir a quien
lea luego el código. Eso sí, se necesita mucha disciplina para releer el código y aplicar mejoras
una vez que está en producción. En mi experiencia tiene un retorno de inversión altísimo ya que el
conocimiento adquirido por los programadores se vuelca en el código y lo enriquece conforme pasa el
tiempo. Lo más habitual en los proyectos es, encontrarse lo contrario, el paso del tiempo empobrece
el código alejándolo cada vez más del conocimiento que hay en la cabeza de los programadores.
Justamente por la ausencia de refactoring. Por tanto, refactor sí, pero en la medida y en el momento
adecuados. Kent Beck solía decir, “Make it work, make it right, make it fast”, haciendo referencia al
orden en el que pone foco a cada fase de la implementación. Mi amigo Luis Rovirosa dice, “Make it
work, then make it smart”.
Es muy importante refactorizar en verde y no en rojo, para estar seguros de que al aplicar cambios
en el código no rompemos nada.
Lo que sí refactorizaría en este momento son los test, que están empezando a ser farragosos. ¿Cómo
podríamos mejorar la mantenibilidad de estos test?, lo veremos en el próximo capítulo.
⁸https://github.com/carlosble/csvfilter
¿Qué es Test-Driven Development? 16
Control de versiones
Los sistemas de control de versiones distribuidos como Git o Mercurial hacen que podamos guardar
versiones locales sin afectar al resto del equipo ni al repositorio origen. En terminología Git, podemos
hacer “commit” sin necesidad de hacer “push”. Cada vez que estoy en verde me gusta guadar un
“commit”. Al cabo del día puedo llegar a hacer más de veinte microcommits. Desde que me habitué
a ir almacenando estos pequeños incrementos, no he vuelto a perder tiempo tratando de volver
a un punto anterior en que el código funcionaba. Antes me pasaba que quizás hacía un refactor
que no salía bien y quería volver atrás y deshacer los últimos cambios. Entonces usaba la función
deshacer (Ctrl+Z) del editor repetidas veces hasta encontrar el momento en que todo funcionaba
pero, si había hecho muchos cambios, podía estar horas navegando hacia atrás en el tiempo. Ahora,
si me pasa, sólo tengo que descartar los cambios locales (git reset) para volver al punto en el que los
test pasan. Mi productividad aumentó considerablemente a la par que se redujo mi miedo a hacer
pequeños cambios exploratorios o experimentales en el código. TDD no dice nada del uso de control
de versiones originalmente pero personalmente recomiendo ir guardando los cambios cada pocos
minutos. Si luego no se quiere que esos pequeños incrementos sueltos formen parte del historial del
control de versiones por algún motivo o política del equipo, pueden unificarse (git squash) antes de
subirlos al repositorio principal.
Beneficios de TDD
Cada uno de los valores de XP se refleja en la práctica de TDD. Se trata de una técnica que evoluciona
en el tiempo conforme van evolucionando las herramientas y paradigmas de programación. Desde
que Kent Beck practicaba TDD con SmallTalk hasta la actualidad, la técnica ha ido cambiando. Los
propios programadores tendemos a adaptar la técnica a nuestro estilo con el paso del tiempo. Seguro
que Kent Beck ha cambiado su estilo con los años. Steve Freeman y Nat Pryce creadores de los Mock
Objects⁹ y del popular libro GOOS¹⁰ han dicho, en más de una ocasión, que su estilo ha evolucionado
desde que escribieron su libro. El mío ciertamente ha cambiado mucho desde que escribí la primera
edición de este libro hace diez años. Pero, en el fondo, TDD sigue retroalimentando los cuatro valores
fundamentales de XP.
Simplicidad
El principal beneficio de TDD es que obliga a pensar antes de programar. Exige definir el
comportamiento del sistema o de parte del mismo antes de programarlo, pero sin llegar a prescribir
cómo debe codificarse. Con lo cual, la interacción con el sistema y sus respuestas deben ser aclaradas
a priori pero las posibles alternativas de codificación de la solución quedan bastante abiertas.
Lo que se busca, al dejar la puerta abierta a la implementación emergente y gradual, es la simplicidad.
El objetivo es resolver el problema con la solución más simple posible, lo cual es muy complicado
⁹http://www.mockobjects.com/
¹⁰http://www.growing-object-oriented-software.com/
¿Qué es Test-Driven Development? 17
de conseguir. Los proyectos de software tienen por un lado un grado de complejidad inherente al
problema, es decir, a problemas más complejos se requieren soluciones más complejas. Y por otro
una complejidad accidental que puede definirse como aquella complejidad innecesaria introducida
por accidente por los programadores. TDD es un proceso que ayuda a encontrar las soluciones más
simples a los problemas. La simplicidad, según Kent Beck puede ser explicada en cuatro reglas. El
código fuente debe:
Para que el código fuente pase las baterías de test, obviamente, alguien tiene que haber escrito
test. Actualmente es indiscutible que cualquier software que sea diseñado para tener una vida útil
superior a varias semanas, debe contar con baterías de test automáticos de respaldo. Hay multitud de
referentes en los que fijarse y los equipos de desarrollo de las empresas tecnológicas más exitosas,
cubren el código con test. Puede verse en los repositorios abiertos de sus librerías, frameworks y
aplicaciones open source.
Las cuatro reglas del diseño simple de Kent Beck son, en mi experiencia, una herramienta
imprescindible para escribir código mantenible. Si son bien entendidas minimizarán la complejidad
accidental del software. Mantenible no significa necesariamente reutilizable. Mantener el código
significa poder cambiarlo; actualizarlo, añadirle o quitarle funcionalidad y corregir los defectos que
se encuentren. Cuanto más fácil sea mantenerlo, más sencillo será entenderlo y, por ende, más rápido
podremos adaptarlo a los cambios y aportar valor a los usuarios. Además podremos cambiarlo sin
romper funcionalidad existente. Por otra parte la idea de construir software reutilizable implica
típicamente añadir mucha más complejidad de la que realmente hace falta para que funcione, acorde
a los requisitos que se conocen hoy. Anticiparse a los posibles requisitos de mañana dispara las
probabilidades de introducir complejidad accidental. El propio Beck cuenta que, cuando cambió su
forma de programar para ceñirse estrictamente a los requisitos del presente, fue cuando en realidad
empezó a escribir el código que mejor se adaptaba a los cambios del futuro. Esto no significa que
ignoremos la arquitectura del software. Los requisitos no funcionales deben abordarse tan pronto
como sean conocidos; seguridad, internalización, localización, tolerancia a fallos, interoperabilidad,
usabilidad, separación de capas… lo que trato de evitar cuando programo es la tentación de añadir
código “por si acaso”. A menudo tendemos a oscilar de un extremo a otro, saltamos del negro al
blanco olvidando los grises que hay en medio. Nadie dijo que en XP no se escribe documentación,
los comentarios en el código no están prohibidos y la arquitectura del software no se ignora ni se
menosprecia.
La falacia de la reutilización llevada al extremo ha provocado que algunos productos software
se hayan desarrollado con posibilidad de configurar cientos o miles de sus parámetros mediante
ficheros de configuración externos o bases de datos. He conocido equipos de producto donde se
requería de especialistas en configuración de parámetros para poder instalar el software a sus clientes
y tales especialistas eran el cuello de botella en la estrategia de ventas de la empresa.
¿Qué es Test-Driven Development? 18
Por otra parte, incluso aunque el código respete las cuatro reglas del diseño simple, no garantiza
que otras personas sean capaces de tomar el relevo y seguir con la evolución del producto, porque la
complejidad inherente al problema sigue ahí. Sin embargo, es la estrategia que mayor mantenibilidad
proporciona de todas las que conozco y que no presenta ningún efecto adverso, siempre y cuando
se aplique en el contexto adecuado. Un código que dispone de una sólida batería de test, claros y
concisos, supone tener gran parte de la batalla ganada.
Los test no tienen por qué escribirse mediante TDD, de hecho, tras casi veinte años de la aparición del
libro original, el uso de la técnica sigue siendo minoritario dentro del sector. Y no siempre es posible
practicar TDD. Por ejemplo, cuando el código ya está escrito es obvio que no podemos volver atrás
en el tiempo para escribir el test primero.
No es relevante que un código haya sido desarrollado con TDD o no. Lo que se necesita es que sea
mantenible, para lo cual es crucial que cuente con los test adecuados. Existen situaciones en las que
TDD ayuda y es altamente aplicable y situaciones en las que no aplica o no añade ningún valor
frente a hacer el test después. Se trata de una herramienta y no de un dogma. El dogmatismo puede
venir tanto de quien practica TDD y cree que es la única herramienta válida, como de quien no tiene
suficiente dominio de la herramienta y dice que no sirve para nada.
Más allá de los factores técnicos y del conocimiento/experiencia, la motivación y la adherencia
también tienen peso a la hora de decidir si aplicar TDD. Escribir test a posteriori, para un código
que ya existe, me resulta aburrido y tedioso. Por un lado soy poco ocurrente pensando en casos de
prueba, con lo que la cobertura se queda corta. Por otro lado si el código no está escrito para poder
ser testado, se hace muy pesado estar haciendo un apaño encima de otro para armar los test. Pueden
llegar a ser extremadamente costosos de hacer y de entender, muy frágiles, muy lentos, etc. Para
mí es mucho más interesante pensar en las pruebas primero, consigo un mejor resultado a nivel de
cobertura y una mayor simplicidad de los propios test.
Es perfectamente posible seguir las reglas del diseño simple sin TDD. Sin embargo, para mí es muy
difícil hacerlo, tiendo a complicarme en exceso, por lo que utilizo TDD para facilitarme la tarea. Una
de sus ventajas es que me guía para simplificar el diseño del software. Una persona con experiencia
dilatada escribiendo test mantenibles, es capaz de hacer un diseño modular y testable sin TDD,
porque ya conoce cómo se diseña un código para que sea testable. En cierto modo, se podría decir
que tiene mentalidad test-first aunque no siga el ciclo rojo-verde-refactor. Por cierto, test-first y TDD
no son exactamente lo mismo. En ambos casos se escribe el test primero pero test-first se queda en
eso, no prescribe nada más, mientras que TDD es todo el ciclo incluyendo refactoring.
A quienes están empezando en la profesión les digo con frecuencia que, antes de preocuparse por la
aplicación de Patrones de Diseño del GoF¹¹, Principios SOLID¹², o de Domain Driven Design¹³, o del
paradigma orientado a objetos (OOP) o del paradigma funcional (FP), o de cualquier otro elemento
de diseño de software, se aseguren de estar cumpliendo con las reglas del diseño simple, sobre todo
que el código tenga test. Un código con los test adecuados admite refactoring, abre la puerta para
que podamos ir introduciendo cualquiera de los elementos de diseño citados anteriormente.
¹¹https://en.wikipedia.org/wiki/Design_Patterns
¹²https://es.wikipedia.org/wiki/SOLID
¹³https://en.wikipedia.org/wiki/Domain-driven_design
¿Qué es Test-Driven Development? 19
Comunicación
La mayoría de las veces que he visto fracasar proyectos durante mi carrera profesional ha sido
por problemas de comunicación entre personas. Transmitir una idea de un cerebro a otro es un
proceso muy complejo. Lo que piensa el emisor debe ser traducido a frases habladas o escritas
en lenguaje natural, que llegan a una receptora que debe interpretarlas con todos sus prejuicios
y experiencias previas. Si encima de esta dificultad ponemos una cadena de mensajeros entre el
emisor original y quien programa, las posibilidades de acabar jugando al teléfono escacharrado
aumentan proporcionalmente al tamaño de la cadena. Sin embargo, así es como se gestiona una
parte importante de los proyectos de software, con cadenas de empresas que subcontratan a otras
empresas y conversaciones que nunca llegan a ocurrir entre quien de verdad tiene el problema y
quienes diseñan la solución.
Incluso cuando hay comunicación directa y conversaciones cara a cara entre las partes interesadas
y las programadoras, tal como propone XP, existe un margen para la ambigüedad que suele causar
desperdicio. Podrían entender mal el problema y trabajar en una solución inadecuada.
El problema fundamental es que, aunque dos personas puedan llegar a cierto entendimiento respecto
al problema a solucionar, las máquinas actuales tienen que ser programadas con un nivel de detalle
diminuto y una precisión total, sin espacio para la ambigüedad. Las personas que intentan reducir la
brecha existente entre el lenguaje abstracto de una conversación humana y el lenguaje que entienden
las máquinas, son los programadores. Habitualmente, cuando se programa, surgen dudas sobre cómo
hacer la traducción de lenguaje natural a lenguaje formal. Pequeños detalles sobre cómo debería
comportarse el sistema. En ocasiones, nadie se da cuenta de que existe algún vacío, una casuística
que se ha olvidado gestionar y que termina por detener el programa y frustrar a la usuaria con un
pobre mensaje de “Lo sentimos, ha ocurrido un error inesperado”.
Una forma de anticipar al máximo la aparición de esas dudas es mediante TDD. Obligarte a pensar
en cómo testar un software que todavía no existe, implica obligarte a definir muy bien cómo debe
comportarse en todo momento. Si las dudas se resuelven antes de empezar a programar, se adquiere
¿Qué es Test-Driven Development? 20
un conocimiento de la solución más completo. Eso se traduce en que podemos elegir mejores
estrategias para implementarla. Evitamos tener que tirar a la basura el trabajo cuando nos damos
cuenta de haber tomado la dirección incorrecta a mitad del camino. Además, no siempre hay acceso
directo a las partes interesadas para tratar de resolver esas dudas, a veces sólo es posible tener una
reunión semanal para planificar el trabajo, o incluso mensual. Para sacarle el máximo partido a estas
reuniones, podemos plantear ejemplos concretos que luego pueden ser traducidos a test automáticos.
Los británicos Chris Matts y Dan North le dieron una vuelta de tuerca al impacto que TDD tiene en
la comunicación y para enfatizarlo, le llamaron BDD¹⁴ (Behaviour-Driven Development). En esencia
lo que se busca es lo mismo, evitar el desperdicio mediante comunicación efectiva. Aun así, la escuela
británica de BDD ha profundizado mucho más en cómo tomar requisitos de software; convirtiéndose
en una técnica que, por definición, involucra a todas las partes interesadas y les anima a encontrar
juntos las especificaciones funcionales y no funcionales del sistema a implementar. A la hora de
codificar, en BDD es muy habitual combinar los estilos de Outside-in TDD con Inside-out TDD,
que veremos más adelante en este libro. Se dice que BDD es el eslabón perdido entre las historias
de usuario y TDD. Podríamos escribir un libro entero sobre BDD, de hecho, existen ya varios libros
muy buenos sobre ello.
Muchas personas entienden que BDD incluye a TDD y se centra en una mejor recogida de requisitos
del software. Otras personas entienden que TDD bien hecho es lo mismo. Hay personas que asocian
BDD con definir los requisitos de alto nivel de abstracción y TDD con definir el comportamiento
de artefactos de programación como funciones, clases o módulos. Hay equipos que hablan de BDD
para referirse a pruebas de integración de extremo a extremo, olvidándose por completo de XP, de
la comunicación y de todo lo demás. Lo importante no es averiguar cuál es la definición correcta
sino entender a qué se refieren los demás cuando hablan de BDD o de TDD, cuáles de sus beneficios
están enfatizando.
Definitivamente, cuando los ejemplos exponen de forma clara las reglas de comportamiento de
sistema, son un mecanismo de comunicación excelente para todos los miembros del equipo. Si
se trata de ejemplos relacionados con artefactos de programación, mejoran la comunicación entre
quienes escriben esos test y quienes los lean en el futuro (que podrían ser ellas mismas). Refuerzan
la intención de quien escribe el código. Sirven de documentación viva que se mantiene actualizada.
Ayudan a identificar rápidamente la combinación de casos para los que el sistema está preparado.
Facilitan la labor de añadir test para subsanar defectos (bugs) en el sistema cuando aparecen.
Feedback
He decidido evitar traducir esta palabra inglesa por el peso tan importante que tiene en XP y mi
incapacidad para encontrarle una equivalencia en castellano que aglutine tantos significados. Con
los ejemplos de esta sección se podrá entender lo que significa en cada contexto.
En la primera mitad del siglo XX, en realidad casi hasta la década de los 80, los ciclos de respuesta
en programación duraban días o incluso semanas. Las programadoras escribían código en papel sin
saber si funcionaría, luego lo pasaban a tarjetas perforadas o al medio tecnológico disponible y,
¹⁴https://dannorth.net/introducing-bdd/
¿Qué es Test-Driven Development? 21
por último, se computaba en esas máquinas gigantescas que por fin generaban una respuesta ante
el programa de entrada. Si se había cometido algún error, las programadoras debían enmendarlo
y volver a ponerse en la cola para poder acceder a las máquinas, ejecutar su programa y obtener
nuevamente una respuesta. Durante la mayor parte del ciclo de desarrollo estaban programando a
ciegas sin saber si estaban cometiendo errores. Me imagino la frustración que podrían sentir aquellas
personas cuando, para cada pequeño ajuste, debían esperar horas o días antes de volver a obtener
una nueva respuesta.
La llegada de SmallTalk en 1980 supuso un hito en la historia de la programación. Alan Kay no sólo
nos trajo el paradigma de la orientación a objetos sino también uno de los lenguajes y entornos que
más ha influido en la programación hasta el día de hoy.
En SmallTalk el ciclo de respuesta pasó a ser inmediato, instantáneo. Ofrecía la característica de
poder trabajar con “Just-in-time programming”, que significa que el programa se está compilando a
la vez que se escribe y por tanto, el programador obtiene una respuesta inmediata sobre los cambios
que está introduciendo en el programa. Esto enamoró a los programadores de la época, entre los
cuales se encontraban Kent Beck y Ward Cunningham y les influenció para siempre. Beck dio un
paso más allá en la búsqueda de ciclos cortos de respuesta buscando, no sólo que el código compilase,
sino que su comportamiento fuese el deseado. Para ello desarrolló SUnit, la librería de test para
SmallTalk y que dio origen a todos los xUnit que vinieron después como JUnit.
Aunque el concepto de definir la prueba antes de implementar la solución había sido utilizado por la
NASA en la década de los 60, se atribuye a Beck haberlo redescubierto y traído a la programación.
En esta entrada del C2 wiki¹⁵, el primer wiki de la historia, se describe como Kent Beck programaba
en aquella época ayudado del feedback inmediato que le proporcionaba SmallTalk.
Hoy en día la mayoría de los editores y entornos de desarrollo integrado implementan algún tipo de
indicador al estilo “Just-in-time programming” que nos permite saber si, al menos sintácticamente,
estamos escribiendo un programa correcto o no. También hay webs que ofrecen una consola REPL
para casi cualquier lenguaje de programación. Además, las herramientas de “hot reloading” o “hot
swap”, nos permiten recargar el programa en tiempo real conforme vamos programando, para poder
probarlo manualmente sin esperar por compilación y despliegue. Sin embargo, estas herramientas
tienen sus limitaciones y no siempre es fácil reproducir el comportamiento que queremos probar.
A veces se requiere de datos, pre-condiciones y acciones encadenadas para ejercitar la parte del
programa en la que estamos trabajando. El proceso de depuración se hace lento, tedioso y propenso
a errores.
Típicamente en un desarrollo sin test, los programadores empiezan el proyecto invirtiendo la mayor
parte del tiempo en escribir nuevas líneas de código durante las primeras horas o días. Poco a poco, la
velocidad va cayendo porque una parte del tiempo se va en probar la aplicación a mano y en depurar
con salidas por consola o con puntos de ruptura, llegando un momento en que la mayor parte del
tiempo se va depurando y tratando de entender el código. Es frustrante esperar para relanzar la
aplicación y trazar una y otra vez la ejecución del código por los mismos lugares. Cuando se arregla
un problema se introduce otro, porque nos olvidamos de realizar algunas de las pruebas que hicimos
ayer o hace unos minutos. El proceso de desarrollo con TDD es muy diferente, el ritmo de progreso es
¹⁵http://c2.com/xp/JustInTimeProgramming.html
¿Qué es Test-Driven Development? 22
Respeto
En ningún caso deberíamos faltar el respeto al equipo, considerando parte del equipo a todas las
partes interesadas, usuarios, clientes…
Quienes escribimos código no deberíamos considerarlo como una extensión nuestra o un hijo
nuestro. No deberíamos tomarnos de forma personal las críticas a un código que hemos escrito.
Sobre todo, si las críticas están hechas con un espíritu constructivo. Todos podemos equivocarnos y
todos podemos escribir hoy mejor código que el de ayer. El apego al código provoca que tengamos
miedo de cambiarlo o incluso de borrarlo, aunque a veces lo más productivo sería dejar de depurar
un fragmento de código y borrarlo. Tardaríamos menos en hacerlo de nuevo desde el principio. Una
vez escuché al programador y conferenciante Brian Marick hablar de un escritor que reescribía sus
artículos muchas veces argumentando que cada vez que volvía a escribirlos aumentaba la calidad del
texto. Marick lo decía en este contexto, lamentaba que los programadores tengamos tanto reparo a
reescribir nuestro código. Hace algunos años las herramientas de edición de documentos y de correo
electrónico no guardan el texto automáticamente, muchos hemos perdido documentos o correos
que nos había llevado horas escribir y hemos tenido que reescribirlos. Al perder el documento, nos
fastidia la mala noticia pero el resultado de reescribir el texto solía ser de mayor calidad que el
¿Qué es Test-Driven Development? 23
original. Sin ir más lejos, la edición actual de este libro va mucho más al grano que la anterior. He
practicado la reescritura de código fuente y el resultado es, con frecuencia, un código más conciso
y más claro, más elegante. Borrar y rehacer fragmentos de código problemáticos, es una potente
herramienta para mejorar la mantenibilidad del código. Me refiero sobre todo a tareas que no van
a llevar más de unas cuantas horas de trabajo: escribir una función, una clase, un módulo, un test…
No estoy sugiriendo tirar a la basura un proyecto entero y reescribirlo desde cero, porque entonces
tendríamos que poner en la balanza muchos más factores.
Aquellas personas que, a su paso, dejan el código mejor de lo que lo encontraron, infunden respeto
en sus compañeros. Cuando te enfrentas a un código que desprende dejadez, desconocimiento, prisa
o una mezcla de todo eso, lo más fácil es seguir esa inercia. Por tanto, la voluntad y la disciplina de
mejorarlo un poquito cada día, genera respeto. Un ejemplo de mejora puede ser empezar a añadir
test donde no los hay. Aunque estemos ante un código legado, si la función que vamos a escribir es
nueva, podemos intentar introducir TDD en el proyecto. Y, si no es nueva, podemos intentar añadir
test.
El código se gana el respeto cuando tiene buenos test que lo cubren. Los mantenedores de cualquier
proyecto open source relevante al que se quiera contribuir, piden test adjuntos para las propuestas
de bugfixes o de nueva funcionalidad. Sino no aceptarán sugerencias (Pull Request por ejemplo). Los
test hacen más respetable al código, son un aval de calidad.
Valor
En XP el valor se refiere a comunicar la verdad sobre el avance del proyecto, las estimaciones, … a
no ocultar información, a afrontar las dificultades sin buscar excusas en los demás.
Queremos tener el valor de poder hacer cambios en el código para reaccionar a las necesidades de
negocio e incluso ser proactivos para aportar ventaja competitiva.
Un código con una buena batería de test automáticos y un sistema de integración continua nos da
mucha confianza, por ejemplo, para realizar despliegue continuo, para hacer varios despliegues al
día en producción. Hacerlo sin test no sería tener valor sino ser kamikaze.
Curva de aprendizaje
La curva de aprendizaje de TDD es más larga de lo que muchas personas están dispuestas a invertir
para asimilar la técnica. Sobre todo si en su entorno cercano no existen otras personas de las que
aprender ni comunidades de práctica en las que apoyarse. Entender el ciclo rojo-verde-refactor
parece sencillo en la teoría pero en la práctica cuesta mucho reemplazar los viejos hábitos.
Cuando somos estudiantes, adquirimos conocimientos durante años sin llegar a ponerlos en práctica
en un entorno real y pocos de nuestros docentes tienen experiencia real en mantenimiento de
¿Qué es Test-Driven Development? 24
software. Luego, cuando llegamos al trabajo, dejamos atrás el estudio como si se hubiese cerrado para
siempre una etapa de la vida, como si hubiéramos finalizado nuestro proceso de aprendizaje. Esto
explica muchas de las deficiencias, mitos, malentendidos y descontextualizaciones de la industria
del software.
Ayudando a otros aprender TDD, he visto que, aquellos que tienen una mentalidad abierta al
aprendizaje (a menudo porque todavía están terminando sus estudios) y que cuentan con mentores
en los que apoyarse, consiguen sacarle partido a la técnica en cuestión de meses, entre tres y seis.
Entienden sus virtudes y se forman una opinión con criterio sobre cuándo usarlo y cuándo no, así
como de los diferentes estilos. A menudo dicen que les resulta natural programar haciendo el test
primero. Esto no quita su falta de experiencia en la resolución de problemas, no les convierte en
seniors ni expertos, simplemente le sacan el partido que pueden a TDD dentro de sus conocimientos
y experiencia.
Cuando he trabajado con desarrolladores veteranos sin experiencia escribiendo test, he comprobado
que su resistencia al cambio es mucho mayor. No basta con un curso intensivo de varios días ni
con leer un libro. Hace falta mucho esfuerzo y voluntad para ir cambiando de hábitos. Pasito a
pasito, sin prisa, pero de forma constante. La ayuda de personas experimentadas con TDD en el
día a día, acelera muchísimo la velocidad de aprendizaje. Las comunidades de estusiastas que se
reúnen para practicar juntas en actividades como los coding dojo son de mucha ayuda tanto en la
parte técnica como en la motivacional. Hoy en día existen multitud de comunidades, algunas de
las cuales están formadas mayoritariamente por mujeres para animar a otras mujeres a acercarse a
la tecnología y a practicar en grupo. El rendimiento que alguien con experiencia le puede sacar a
TDD es mucho mayor que el de una persona que está empezando. Está añadiendo una herramienta
más que complementa muy bien a otras herramientas de diseño. También debo decir que he visto a
desarrolladores senior adoptar y dominar TDD en cuestión de pocos meses, lo más importante es la
capacidad de abrir la mente y el entusiasmo.
En resumen, el problema fundamental que he observado en la adopción de TDD es que mucha gente
desiste antes de llegar al punto de inflexión en el que se dan cuenta de las ventajas que les puede
aportar. Como una startup que muere antes de llegar al “break-even”. Curiosean, experimentan
frustración y abandonan.
Código legado
La mayor parte del tiempo trabajamos en código que ya existe. Michael Feathers en su libro Working
Effective With Legacy Code¹⁶, llama código legado (legacy code) al código que no está cubierto con
test. A día de hoy mientras escribo estas líneas, todavía veo más proyectos en los que no hay test
que proyectos con una cobertura significativa. Incluso todavía hay equipos que arrancan proyectos
nuevos sin test.
Si el código ya está escrito, obviamente no podemos hacer TDD. Por eso, estadísticamente, tiene
sentido que los proyectos donde se haga TDD sean una minoría. No obstante los mantenimientos de
software no son todos correctivos sino que los productos de éxito introducen nuevas características
¹⁶https://www.oreilly.com/library/view/working-effectively-with/0131177052/
¿Qué es Test-Driven Development? 25
con el paso del tiempo. Cada vez que incluimos nueva funcionalidad en un proyecto existente se
abre la posibilidad de introducir test e incluso de introducir TDD. Se requieren técnicas de trabajo
con código legado como ,por ejemplo, envolver código viejo en código nuevo con una fachada más
cómoda de aislar y testar. No es fácil, pero en el medio y largo plazo la inversión se recupera con
creces. La técnica y el nivel de inversión depende del contexto. También se requieren grandes dosis
de pragmatismo para no estancarse en el análisis de la estrategia. A veces, cuando se trata de código
legado voy más rápido escribiendo primero el código de producción que haciendo TDD. Sobre todo
cuando no tengo garantías de que el sistema se comporte como yo espero que lo haga, por lo que
plantear test partiendo de premisas incorrectas puede ser una pérdida de tiempo. En esos casos el
proceso que sigo es:
1. Guardar cualquier cambio pendiente que tuviera hasta ese momento (típicamente git commit)
2. Añadir la nueva funcionalidad.
3. Ejecutar la aplicación a mano para validar que funciona como espero.
4. Explorar la aplicación como usuario o más bien como tester para asegurarme que ninguna otra
funcionalidad está rota.
5. Añadir test automáticos que dan cobertura a la nueva funcionalidad.
6. Verlos ejecutarse en verde.
7. Volver a dejar el código como estaba al principio (volviendo al commit anterior si es necesario)
8. Lanzar de nuevo los test y verlos en rojo, confirmando que el error es el esperado.
9. Recuperar la última versión del código para ver los test en verde de nuevo.
Los años de práctica de TDD me han convertido en un programador metódico que sigue protocolos
rigurosos como este, aunque no sea TDD. He aprendido que no puedo confiar en test en verde sin
verlos también en rojo en algún momento, porque pueden estar mal escritos.
Limitaciones tecnológicas
Hace algunos años, los frameworks y librerías no se diseñaban para que el código que se apoyaba
en ellos pudiera ser testable. Mucha gente desconocía los test, incluidos los que diseñaban esas
herramientas. Los frameworks de test tipo xUnit ni siquiera existían para muchos lenguajes. Fueron
llegando poco a poco a cada lenguaje. Intentar escribir test para tecnología como Enterprise
JavaBeans era una odisea. Después, los frameworks más populares empezaron a integrar soporte
para test, gracias en parte al empuje de la comunidad open source, pero en la mayoría de los casos era
para lanzar pesados test de integración que tardaban mucho en ejecutarse. Hoy en día los frameworks
y librerías modernos se diseñan pensando en que el código que se apoya en ellos debe poderse testar
con todo tipo de test, incluso con bastante independencia de las librerías. El propio código fuente de
esas herramientas posee baterías de test exhaustivas. A día de hoy no usaría código de fuente abierta
si no tiene test de calidad. Una de las estrategias que sigo para tomar decisiones sobre las librerías y
frameworks de código abierto que voy a usar en mis proyectos, es leer su código fuente y el de sus
test.
¿Qué es Test-Driven Development? 26
A pesar de esta evolución y madurez de la industria, hoy en día siguen saliendo al mercado
herramientas que no permiten testar el código tanto como se quiera. Sucede por ejemplo con algunas
tecnologías desarrolladas para dispositivos móviles y para dispositivos industriales.
No hace mucho estuve ayudando a un equipo de desarrollo cuyo producto eran máquinas industria-
les utilizadas para diagnóstico de enfermedades mediante muestras de sangre y líquidos reactivos.
Ellos fabrican tanto el hardware como el software. Los fabricantes de microchips industriales suelen
distribuir un SDK con un entorno de desarrollo propietario a la medida y un juego de instrucciones
reducido de propósito específico. Los recursos como la memoria RAM son muy limitados y los
tiempos de ejecución son críticos, no se pueden permitir cambios de rendimiento que aumenten
la ejecución en unos pocos milisegundos porque la cadena de movimientos de brazos robóticos,
agujas, etc, tiene que estar sincronizada. En este entorno, escribir test automáticos es un gran reto.
Aún así el equipo se las ingenió para desarrollar su propio doble de pruebas para las placas base
de las máquinas (de tipo fake en este caso) que permitía testar bien las capas de más alto nivel del
sistema.
Desconocimiento de la tecnología
Para escribir el test primero no solamente debo tener claro el comportamiento que deseo que tenga
mi código, sino que también necesito conocer cómo funciona su entorno. Necesito comprender el
paradigma con el que estoy trabajando, el lenguaje de programación, el ciclo de vida de los artefactos,
la forma en que los programadores de las librerías y frameworks han diseñado su uso… porque al
escribir primero el test estoy asumiendo comportamientos en las interacciones. Cuando aterrizo en
un stack tecnológico nuevo, no tengo la capacidad de identificar cuáles son las unidades funcionales
que puedo diseñar como cajas negras. No soy capaz de descomponer el sistema en capas si no conozco
bien esas capas.
En XP existe el concepto de experimento a modo de prueba de concepto y se le llama spike. Cuando
no conocemos bien como funciona la tecnología, hacemos un programa pequeñito con el menor
número de piezas posible que nos permita aprenderla. Código de usar y tirar con el objetivo de
adquirir conocimiento. Ni siquiera llega a ser un prototipo, son simplemente pruebas de concepto
aisladas. Una vez comprendemos el funcionamiento, ya podemos traer nuestra caja de herramientas
y escribir test o practicar TDD.
Así lo he hecho con cada oleada de tecnología que ha ido llegando. El ejemplo más reciente que
recuerdo fue hace algunos años cuando aprendí a programar con la librería React desarrollada por
Facebook para JavaScript. Al principio hice un curso online donde se explicaba cómo funcionaba y
por suerte también daba ejemplos de cómo escribir test. Hice una pequeña aplicación de ejemplo
para asegurarme que entendía los conceptos. Luego estudié cómo podía aprovechar mi caja de
herramientas con la librería, ¿tiene sentido inyectar dependencias?¿cómo puedo hacerlo? ¿cómo
puedo usar mock objects en mis test? ¿cómo puedo ejecutar mis test unitarios en unos pocos
milisegundos? En aquel entonces no encontré artículos en Internet que explicaran todo lo que yo
quería hacer en mis test pero, como había adquirido suficiente nivel de conocimiento de la librería,
no me costó mucho esfuerzo implementar en este nuevo escenario lo que ya había hecho antes en
otros.
¿Qué es Test-Driven Development? 27
Los prototipos entrañan un peligro bien conocido y es que una vez funcionan se conviertan en la
base de código sobre la que el proyecto sigue creciendo. Hace falta disciplina y visión de futuro para
no construir un proyecto entero partiendo de un prototipo experimental de aprendizaje. Aferrarse
al coste hundido¹⁷ de la inversión dedicada al experimento, se puede llegar a traducir en un desastre
en el medio y largo plazo. Los spikes no se realizan para aprovechar el código sino el conocimiento
adquirido.
¿Cuál es el coste de que sean los usuarios los que detecten un fallo del sistema en producción?
Tendemos a medir el tiempo en que desarrollamos una nueva característica del software como si
representara el 100% del coste, pero pocas veces medimos el impacto que tiene para el negocio la
molestia que causamos a los usuarios cuando el software falla. Cuanto antes detectemos y arreglemos
un fallo, menos coste. Lo más caro es que el defecto llegue hasta los usuarios. Las métricas de costes
son muy útiles cuando consideramos toda la vida del producto y no sólo el desarrollo inicial. Resulta
que lo más caro es mantener el código, no escribirlo.
Es el desconocimiento el que evita que escribamos test. Tanto a nivel técnico como a nivel de gestión
de proyectos como a nivel de estrategia de negocio de producto digital.
Cuando existe una situación de verdadera urgencia por mostrar una prueba de concepto, una demo
comercial, un prototipo pequeño… si de verdad vamos a ir más rápido sin test, tiene todo el sentido
no hacerlos. Nuestro trabajo como programadores consiste en solucionar problemas, aportando valor
al negocio con flexibilidad para adaptar las prácticas al contexto, pero considerando, no sólo el corto
plazo, sino todas las consecuencias de nuestras decisiones y siendo capaces de comunicarlas con
claridad al equipo. Con ese valor del que habla XP en sus pilares.
Prejuicios
A los largo de los años he tenido multitud de conversaciones con otras personas que tenían todo
tipo de ideas acerca de la práctica de TDD. Algunas ideas eran prejuicios, bien contra el método o
bien contra programadores que dicen practicar TDD habitualmente. Se habían formado una opinión
pese a no haber participado en ningún proyecto real en que se usara TDD. Puede que el aparente
dogmatismo de algunos programadores que abogan por TDD pueda haber provocado resistencia y
crítica destructiva. No es una herramienta que valga para todos los casos ni que solucione todos los
problemas. Diría que ninguna herramienta vale para todos los problemas, por lo que tiene sentido
que, cuando alguien oye a otra persona decir “con esta herramienta se van a solucionar todos los
problemas, en todas las situaciones” se genere desconfianza y rechazo. También hay quien critica
destructivamente a otras personas porque no hacen TDD o no escriben test y esto sólo genera más
oposición, más fricción.
Existe el prejuicio de que los que utilizamos TDD y otras prácticas de XP como pair programming, no
somos pragmáticos priorizando los intereses de negocio de las partes interesadas sino que preferimos
recrearnos con la elegancia del código, que entregar valor lo más rápido posible. Que elegimos dejar
plantado a un cliente antes que dejar de escribir un test. Que estamos obsesionados con la excelencia
y la perfección. Que somos egocéntricos y prepotentes… Esta visión esta muy alejada de mi realidad y
mi entendimiento de XP. Justamente escribo test como ejercicio de humildad porque no confío en mi
capacidad de programar sin cometer decenas de errores al día. Si me creyese el mejor programador
de la tierra, lo último que usaría es XP. Los métodos ágiles valoran a las personas y sus problemas
por encima de las prácticas de ingeniería. Mi recomendación contra los prejuicios es ir a la fuente
original, leer y escuchar a Kent Beck, Ward Cunningham, Ron Jeffries, Martin Fowler… tratar de
entender su contexto y ver si podemos sacarle partido a sus hallazgos en nuestro contexto. Después
practicar en un entorno controlado y así formarse una opinión en base a la experiencia real.
¿Qué es Test-Driven Development? 29
Existe el clásico mito de que XP no sirve para los grandes proyectos de ingeniería que cuestan
millones de euros. Mi punto de vista, basado en mi experiencia con proyectos de diferente tamaño
es que un proyecto grande bien planteado, no es más que la suma de subproyectos más pequeños.
Divide y vencerás.
Test mantenibles
Larga vida a los test
Una prueba o test automático es un bloque de código que ejecuta a otro código para validar de
forma automática su correcto funcionamiento. Los test no suelen formar parte del ensamblado final,
o sea, del entregable que se despliega en los entornos de producción. Por eso existe una tendencia
a considerar el código de los test como de segunda clase, tratándole con menos cuidado que el
código de producción. Es un error muy común que cometen developers y testers que no tienen
experiencia manteniendo grandes baterías de test. La realidad es que el código de los test también
debe mantenerse con el paso del tiempo si queremos poderlos seguir utilizando como mecanismo de
validación y por esta razón su código debe ser tan limpio y efectivo como cualquier otro código que
se despliega en producción.
De hecho hay sistemas donde un subconjunto de los test son lanzados en producción tras cada
despliegue de versión, porque resulta imposible reconstruir el entorno real. Si pensamos en la
gigantesca cantidad de máquinas y de sistemas que sirven a los usuarios de Facebook, Netflix,
Amazon, etc, podremos entender que no siempre es posible disponer de un entorno de preproducción
donde lanzar test y que se comporten de forma idéntica al entorno real. Por eso algunos equipos
no tienen más remedio que hacer pruebas en entornos reales de forma automatizada, donde la
validación no sólo se hace mediante aserciones sino mediante métricas de impacto analizando datos.
Las baterías de test deben avisar de los defectos que se introducen en el sistema sin provocar falsas
alertas. Cuando hacemos refactor, si de verdad estamos respetando la funcionalidad existente, los test
no deberían romperse porque hagamos cambios en el código. Si se rompen constantemente cuando
no deberían, serán un lastre. Evitarán que el equipo de desarrollo practique refactoring, que es el
hábito más saludable para mantener el código limpio.
Cuando los test son concisos, claros y certeros, aportan mucho más valor que el de la validación
automática. Sirven como documentación viva del proyecto. Ponen de manifiesto de forma explícita
el comportamiento del sistema para que quien los lea sepa perfectamente lo que puede esperar del
mismo. La existencia de test de calidad favorece la incorporación de más test de calidad, es decir,
cuando los desarrolladores que llegan al proyecto encuentran conjuntos de test bien escritos, se
inspiran para seguir añadiendo test con el mismo estilo.
Los test automáticos son una herramienta que nos permite ganar velocidad de desarrollo y evitar
problemas de mantenimiento severos, siempre y cuando sean adecuados. Escribir test requiere una
inversión de tiempo; recuperarla o no depende de la calidad de los mismos. A continuación veremos
las cualidades que buscamos en los test para que nos permitan maximizar sus beneficios.
Test mantenibles 31
Principios
Existen múltiples categorizaciones de test: integración, unitarios, aceptación, extremo a extremo
(end-to-end), de caja negra, caja blanca… que sirven para establecer un contexto cuando el equipo
conversa sobre los test del proyecto. Curiosamente cada equipo suele darle un significado diferente
a estas categorías. Lo importante no es tanto la categorización de un test sino entender las
consecuencias que sus atributos tienen en la mantenibilidad del propio test y del código que está
ejercitando. Para cada test buscamos un equilibrio en la combinación de atributos como su:
• Velocidad
• Granularidad
• Independencia
• Inocuidad
• Acoplamiento
• Fragilidad
• Flexibilidad
• Fiabilidad
• Complejidad
• Expresividad
• Determinismo
• Exclusividad
• Trazabilidad
Sólo por citar algunos. La granularidad se refiere a la cantidad de artefactos (capas, módulos, clases,
funciones…) del código que se prueba, que son atravesados por el test durante su ejecución. A mayor
granularidad más artefactos ejercita. Si comparamos un test de integración que vaya de extremo a
extremo de la aplicación con un test unitario que ataca a una función pura, que calcula una raíz
cuadrada, vemos que sus atributos tienen un peso diferente. El primero tiene mayor granularidad,
pero menor velocidad, justo al revés que el segundo. Es más complejo y más frágil pero aporta más
fiabilidad a la hora de validar que la unión de las piezas del sistema funciona. Está menos acoplado
a la implementación del sistema que el segundo, que a cambio es más expresivo y más simple, más
conciso. Requiere más infraestructura para poder ser inocuo y depende del entorno de ejecución
del sistema, mientras que el segundo sólo necesita un framework de testing. Lo que se gana por un
lado se pierde por otro, de ahí que sea difícil escribir buenos test, porque se necesita experiencia para
encontrar el equilibrio. Existen metáforas como la pirámide del test¹⁸ y posteriormente el iceberg del
test¹⁹ que pueden servir como idea de densidad poblacional de los test. Se dice que debe haber pocos
test de extremo a extremo y muchos test unitarios. Las reglas de negocio o criterios de aceptación no
necesariamente deben validarse con test de integración, a veces tiene más sentido usar test unitarios,
depende del contexto. Lo cierto es que cuando existe una combinatoria de casos que atraviesan un
mismo camino de ejecución, no tiene sentido usar test de granularidad gruesa para todos esos casos
¹⁸https://martinfowler.com/bliki/TestPyramid.html
¹⁹http://claysnow.co.uk/the-testing-iceberg/
Test mantenibles 32
sino que la mayoría de los test deberían atacar al código que toma las decisiones, más de cerca. Puede
tener sentido dejar un test de integración que compruebe la unión de las piezas y que incluya uno
de los casos de la combinatoria, pero el resto mejor abordarlos en aislamiento.
Cuando un test se rompe debería hacerlo por un solo motivo y debería ser muy expresivo señalando
el motivo de fallo como para que no haga falta depurar el sistema con un depurador. Una métrica
que ayuda a conocer la calidad de nuestras baterías de test es la cantidad de veces que necesitamos
recurrir a un depurador para comprender test rotos y arreglarlos. Depurar para arreglar fallos es
perder el tiempo, cuanto menos tengamos que hacerlo, mejor. Cuando uno de mis test de extremo a
extremo (también llamados end2end) se rompe, me gusta que haya un test de granularidad más fina
que también se rompa, porque es quien me señala con precisión el origen del problema. Uno aporta
mayor ámbito de cobertura (granularidad) y el otro aporta claridad y velocidad (feedback rápido).
Otra técnica que ayuda a evaluar la calidad y la cobertura de los test es la mutación de código
(mutation testing). Consiste en introducir pequeños cambios en el código de producción a modo de
defectos como por ejemplo darle la vuelta a una condición para que en lugar de ser “mayor que”
sea “menor o igual que” y después ejecutar los test para ver si fallan y si el mensaje de error es fácil
de entender. Mutation testing puede aplicarse realizando los cambios a mano o con herramientas
automáticas. Es una herramienta interesante especialmente para sesiones de revisión de código y de
pair testing.
Intentando simplificar las consideraciones vistas en los párrafos anteriores y tratando de aterrizar a
la práctica, lo que me planteo es que los test cumplan con lo siguiente:
• Claros, concisos y certeros: quiero que mis test tengan como máximo tres bloques (arrange, act,
assert o given, when, then) y que me cuenten nada más que los datos mínimos relevantes que
necesito ver para entender y distinguir cada escenario. Sin datos superfluos, sin ruido. Quiero
que los nombres de los test me cuenten cuál es la regla de negocio que está cumpliendo el
sistema, no que me vuelvan a decir lo que ya puedo leer dentro del test. No quiero redundancia.
• Feedback rápido e informativo: ¿es correcto el código?¿funciona?¿cuánto tiempo tardan en
darme una respuesta? y cuando fallan, ¿cómo de entendible es el error? ¿cuánto me cuesta
encontrar la causa del problema?. Cada vez que siento la necesidad de levantar la aplicación,
entrar en ella como usuario y probarla a mano para comprobar que está bien, siento que necesito
tener un test que haga eso por mí. Está bien que lo haga una vez a mano, pero no continuamente.
La rentabilidad de los test se produce porque dejamos de malgastar horas diarias en compilar,
desplegar, crear datos, probar a mano…
Quiero que mis test se ejecuten lo más rápido posible. Que pueda lanzarlos de forma cómoda
en cualquier momento. Que pueda elegir lanzar diferentes suites de test, no siempre necesito
ejecutarlos todos.
• Simplicidad: ¿cuántas dependencias tienen mis test? - librerías, frameworks, bases de datos,
otros servicios de terceros… ¿cuánto cuesta entender cómo funcionan todas esas piezas? ¿y
mantenerlas?
• Robustez: cada test debería romperse sólo por un motivo, porque la regla de negocio que está
ejercitando ha dejado de cumplirse. No quiero que los test se rompan cuando cambio algún
detalle que no altera el comportamiento del sistema, como el aspecto del diseño gráfico.
Test mantenibles 33
• Flexibilidad: quiero poder cambiar el diseño de mi código (refactorizar) sin que los test se
rompan, siempre y cuando el sistema se siga comportando correctamente. Los test no deben
impedirme el refactoring, no deben ser un lastre.
• Fast (rápido)
• Independent (independiente)
• Repeatable (repetible)
• Self-validated (auto-validado)
• Timely (oportuno)
Claro que la gracia de los test está en que se auto-validan, la herramienta nos muestra color rojo
o verde y no tenemos que hacer comprobaciones a ojo. La última letra, la T, se refiere al mejor
momento para escribir un test, que resulta ser antes de escribir el código de producción.
Los test no se pueden anidar con este tipo de frameworks. Para hacer hincapié en la legibilidad del
test, se puede jugar a concatenar el nombre de la clase con el del test para que forme un frase con
sentido. Cuando el test falla, el framework suele mostrar ambos textos unidos. En el ejemplo de arriba
diría “TheSystemUnderTestShoud.implement_some_business_rule”. A veces el sufijo “should” en el
nombre de la clase ayuda a pensar en nombres de test que hablen de comportamiento pero tampoco
evita que se puedan poner nombres de test inapropiados (“should return 2”). También es muy común
ver el sufijo “test” en los nombres de las clases de test. Ambos estilos son perfectamente válidos. En
el ejemplo el nombre de mi test no sigue la convención Java lowerCamelCase sino que prefiero
usar snake_case porque los nombres de los test tienden a quedarme muy largos. Mi ex-compañero
Alfredo Casado explicaba que puesto que son métodos que no vamos a invocar desde ninguna otra
parte del código, no le suponía ningún problema la diferencia de estilo. Seguir la convención del
lenguaje, en este caso lowerCamelCase también es perfectamente correcto y tiene la ventaja de que
nadie va a extrañarse por el estilo.
El otro estilo popular hoy en día es el de RSpec, con bloques describe y bloques it que sí pueden
anidarse. Se importó de Ruby a JavaScript con gran éxito y es el estilo por el que han apostado
frameworks modernos como Jest, y anteriormente Mocha y Jasmine.
Se trata de dos funciones, con dos argumentos. El primero es una cadena de texto y el segundo es
una función anónima. Así el framework no tiene que tirar de reflexión sino que puede directamente
ejecutar esas funciones anónimas. En una de mis clases²⁰ grabé una aproximación, a groso modo,
²⁰https://www.youtube.com/watch?v=JplQuz0tkGk&list=PLiM1poinndeMSR6ATToTWPWLmFqg50-Vb&index=10
Test mantenibles 35
de cómo funciona un framework JavaScript de este tipo (en realidad es bastante más complejo). La
ventaja de que el primer argumento sea una cadena de texto es que tenemos más libertad para ser
tan verbosos como haga falta explicando el comportamiento del sistema. La siguiente ventaja es
que se pueden anidar los test, haciendo más fácil la agrupación por contexto como veremos más
adelante. Cuando el test falla, los frameworks también suelen concatenar el texto o presentarlo de
forma anidada, por lo que ayuda que la unión de textos forme una frase con sentido. La palabra “it”
de los test se refiere a la tercera persona del singular en inglés, “la cosa”, por lo que conviene que en
la medida de lo posible el verbo que pongamos aparezca también en tercera persona.
Los frameworks de test suelen venir acompañados de funciones para escribir las aserciones, por
ejemplo JUnit viene con assertEquals y otras funciones estáticas y Jest viene con expect. Además
existen otra librerías que pueden agregarse a los proyectos para dotar de más expresividad en las
aserciones. Por ejemplo con JUnit funciona muy bien tanto Hamcrest como AssertJ. Estas librerías
son especialmente útiles para trabajar con colecciones y suelen ser extensibles para que se puedan
añadir comprobaciones a la medida (custom matchers).
Especificaciones funcionales
Cuando hablo del “nombre del test”, me estoy refiriendo al nombre del método de test en frameworks
tipo xUnit o a la cadena de texto libre en los frameworks tipo RSpec. Este texto debe ser más abstracto
que el contenido. El contenido del test es un ejemplo concreto de un escenario, una fotografía
del comportamiento del sistema en un momento dado con unos valores puntuales. Contiene datos
concretos. Mientras que el nombre del test no debe tener datos sino la regla de negocio general que
está demostrando ese test. Así pues no es útil como documentación escribir test del estilo siguiente:
Los primeros hacen referencia al dato concreto de salida o de entrada usado en el cuerpo del test.
Este estilo es un error común porque no hace falta pensar nada para escribir el nombre del test.
Es un error porque es información redundante que ya se puede leer claramente dentro del test, no
aporta absolutamente nada a quien lo lea. El último ejemplo hace referencia a un comportamiento
pero sigue siendo excesivamente concreto, le falta abstracción para hablar en términos de negocio.
Se podría expresar como “Does not allow for blanks”, que es algo más abstracto y sigue siendo
válido si mañana deja de ser una excepción y es otro detalle de implementación. Los nombres de
test deberían seguir siendo válidos cuando los pequeños detalles de implementación cambian. No
deberían cambiar mientras que las reglas de negocio no cambien. Es otro motivo para no poner
constantes numéricas ni otros detalles de ese nivel. Los nombres de los test son afirmaciones claras
en lenguaje de negocio sobre el comportamiento del sistema:
Son las especificaciones técnicas de la solución. Como si fuera el manual de instrucciones. Si una
persona es capaz de entender el comportamiento del sistema tan sólo de los nombres de los test,
significa que están correctamente nombrados. Los detalles concretos siempre pueden verse dentro
del test para aclarar cualquier ambigüedad que pueda surgir. En sucesivos capítulos podremos ver
más ejemplos de nombres de test.
En los ejemplos de este libro estoy usando inglés para los nombres de los test porque es como suelo
programar. Sin embargo hay proyectos donde es preferible escribirlos en castellano o en cualquiera
que sea el idioma que las partes interesadas usan para comunicarse. Cuando traducir al inglés se
hace torpe y propenso a errores, es preferible dejar los nombres de los test en el idioma materno
de quienes los escriben y de quienes los van a mantener. Es preferible castellano, catalán, gallego,
euskera, o lo que sea que el equipo hable, antes que un inglés forzado que no van a entender ni
siquiera los anglosajones que lo lean.
Pensemos en los test como una herramienta de comunicación con los futuros lectores. Los nombres
de los test son buenos candidatos para refinar cuando practicamos refactoring. Puede que no se nos
ocurra el mejor nombre cuando escribimos el test pero minutos o días más tarde somos capaces de
verlo más claro y entonces podemos aplicar el refactor mas rentable de todos, el renombrado.
Una de las aportaciones de TDD es que al tener que pensar en las reglas de comportamiento primero,
los nombres de los test vienen de regalo. Me ayuda a centrarme tanto en el nombre del test como en
su contenido.
Este estilo de nombrado de las pruebas funciona muy bien en el contexto de TDD y también puede
funcionar bien cuando se escriben test a posteriori sobre un código que ha escrito uno mismo o que
conoce muy bien. Pero no es el único. Hay otros estilos de nombrado de test que funcionan mejor en
otros contextos como por ejemplo cuando toca escribir test para un código legado que es totalmente
desconocido para quien tiene que añadirle los test. En este caso las reglas de negocio puede que
incluso sean desconocidas para quien se dispone a escribir los test. Hay quien opta por añadir el
nombre de la clase y del método que se esta probando al nombre del test, como un prefijo.
A veces nos atascamos en discusiones infructuosas con el equipo o con la comunidad en los foros
porque nos olvidamos de establecer un contexto que de sentido a las prácticas, las técnicas o los
principios de las que estamos hablando. Me gusta recordar una expresión de Dan North que dice:
• Practices = Principles(Context)
Lo que viene a decir es que nuestras prácticas deberían ser el resultado de aplicar nuestros principios
a nuestro contexto. Para ello hace falta tener unos principios y ser bien conscientes de cuál es
Test mantenibles 37
nuestro contexto. La historia está llena de equipos que fracasaron intentando aplicar prácticas de
otros equipos con contextos totalmente diferentes.
1 package csvFilter
2 import org.assertj.core.api.Assertions.assertThat
3 import org.junit.Before
4 import org.junit.Test
5 class CsvFilterShould {
6 private val headerLine = "Num_factura, Fecha, Bruto, Neto, IVA, IGIC, Concepto, \
7 CIF_cliente, NIF_cliente"
8 lateinit var filter : CsvFilter
9 private val emptyDataFile = listOf(headerLine)
10 private val emptyField = ""
11
12 @Before
Test mantenibles 38
13 fun setup(){
14 filter = CsvFilter()
15 }
16
17 @Test
18 fun allow_for_correct_lines_only(){
19 val lines = fileWithOneInvoiceLineHaving(concept = "a correct line with irre\
20 levant data")
21 val result = filter.apply(lines)
22
23 assertThat(result).isEqualTo(lines)
24 }
25
26 @Test
27 fun exclude_lines_with_both_tax_fields_populated_as_they_are_exclusive(){
28 val result = filter.apply(
29 fileWithOneInvoiceLineHaving(ivaTax = "19", igicTax = "8"))
30
31 assertThat(result).isEqualTo(emptyDataFile)
32 }
33
34 @Test
35 fun exclude_lines_with_both_tax_fields_empty_as_one_is_required(){
36 val result = filter.apply(
37 fileWithOneInvoiceLineHaving(ivaTax = emptyField, igicTax = emptyField))
38
39 assertThat(result).isEqualTo(emptyDataFile)
40 }
41
42 @Test
43 fun exclude_lines_with_non_decimal_tax_fields(){
44 val result = filter.apply(
45 fileWithOneInvoiceLineHaving(ivaTax = "XYZ"))
46
47 assertThat(result).isEqualTo(emptyDataFile)
48 }
49
50 @Test
51 fun exclude_lines_with_both_tax_fields_populated_even_if_non_decimal(){
52 val result = filter.apply(
53 fileWithOneInvoiceLineHaving(ivaTax = "XYZ", igicTax = "12"))
54
55 assertThat(result).isEqualTo(emptyDataFile)
Test mantenibles 39
56 }
57
58 private fun fileWithOneInvoiceLineHaving(ivaTax: String = "19", igicTax: String \
59 = emptyField, concept: String = "irrelevant"): List<String> {
60 val invoiceId = "1"
61 val invoiceDate = "02/05/2019"
62 val grossAmount = "1000"
63 val netAmount = "810"
64 val cif = "B76430134"
65 val nif = emptyField
66 val formattedLine = listOf(
67 invoiceId,
68 invoiceDate,
69 grossAmount,
70 netAmount,
71 ivaTax,
72 igicTax,
73 concept,
74 cif,
75 nif
76 ).joinToString(",")
77 return listOf(headerLine, formattedLine)
78 }
79 }
Hemos reducido el conocimiento (acoplamiento) que los test tienen sobre la solución. No saben
cómo se forman las líneas, no saben que los campos están separados por comas ni en qué orden
van colocados. Tampoco les importa conocer que la primera línea es la cabecera. Si alguno de estos
detalles cambia en el futuro bastará con cambiar la función privada que construye la línea. En cada
test se puede leer claramente cuál es el dato relevante para provocar el comportamiento deseado.
Hemos movido la inicialización a un método decorado con la anotación @Before, que significa que
el framework ejecutará ese método justo antes de cada uno de los test. Si hay N test, se ejecutará
N veces. Está pensado así para garantizar que los test no se afecten unos a los otros. Para que la
memoria o la fuente de datos que sea, pueda ser reiniciada y un test no provoque el fallo en otro.
En este ejemplo estamos creando una instancia nueva de la clase CsvFilter para cada test, por si esta
clase guardase algún estado en variables de instancia, para evitar que sea compartido entre los test.
Los test deben ser independientes entre si y en la medida de los posible poderse ejecutar incluso
en paralelo, sin que se produzcan condiciones de carrera. Así se pueden ejecutar más rápido si la
máquina tiene varios núcleos.
Como efecto secundario de refactorizar los test, me dí cuenta que también debía refactorizar el
código de producción. El método de producción originalmente se llamaba filter pero entonces los
test quedaban como filter.filter(arguments) lo cual era muy redundante. Esa redundancia empezaría
Test mantenibles 40
a aparecer por todos aquellos puntos del código de producción que consuman la función, por lo que
he decidido llamarla apply.
44 "name": "Otitis",
45 "location": "Oídos",
46 "system": "Auditivo",
47 "origin": "Bacteria",
48 "specie": "Perro, Gato",
49 }
50 ];
51 renderComponentWith(cases, diagnoses);
52 });
53 /*
54 ...
55 some other tests around here
56 ...
57 */
58 it("filters cases when several diagnosis filters are applied together", async () => {
59 simulateClickOnFilterCheckbox("Cerebro");
60 simulateClickOnFilterCheckbox("Vías Respiratorias Altas");
61
62 let table = await waitForCasesTableToUpdateResults();
63 expect(table).not.toHaveTextContent("Dinwell");
64 expect(table).toHaveTextContent("Chupito");
65 expect(table).toHaveTextContent("Juliana");
66 });
En el ejemplo original la lista de casos tenía cinco elementos y la de diagnósticos otros cinco. El
número de test de la suite era de unos veinte. He omitido todo eso para hacer reducir el tamaño del
ejemplo en el libro. Para entender el test había que navegar hasta arriba del fichero, entender todos
los datos y sus relaciones y retener en la mente los nombres para ponerlos en el test. Un primer paso
hacia evitar este problema es utilizar un subconjunto de esos datos en cada test de manera que se
manejen solamente los elementos mínimos necesarios. Sin más complicaciones. En nuestro caso la
estructura del dato estaba todavía por cambiar bastante y no queríamos vernos obligados a cambiar
todos los test de la clase cuando esto pasara. Entonces decidimos construir fixtures en cada test, con
ayuda del patrón builder. Así quedaron los test tras aplicar refactoring:
Test mantenibles 43
1 it("filters cases when several dianosis filters are applied together", async () => {
2 let searchCriterion1 = "Cerebro";
3 let searchCriterion2 = "Vías Respiratorias Altas";
4 let discardedLocation = "irrelevant";
5 let fixtures = casesWithDiagnoses()
6 .havingDiagnosisWithLocation(searchCriterion1)
7 .havingDiagnosisWithLocation(searchCriterion2)
8 .havingDiagnosisWithLocation(discardedLocation)
9 .build();
10 renderComponentWith(fixtures.cases(), fixtures.diagnoses());
11
12 simulateClickOnFilterCheckbox(searchCriterion1);
13 simulateClickOnFilterCheckbox(searchCriterion2);
14
15 let table = await waitForCasesTableToUpdateResults();
16 expect(table)
17 .not.toHaveTextContent(
18 fixtures.patientNameGivenDiagnosisLocation(discardedLocation));
19 expect(table)
20 .toHaveTextContent(
21 fixtures.patientNameGivenDiagnosisLocation(searchCriterion1));
22 expect(table)
23 .toHaveTextContent(
24 fixtures.patientNameGivenDiagnosisLocation(searchCriterion2));
25 });
Desapareció por completo el bloque beforeEach de la suite. El test queda más grande porque genera
sus propios datos y porque es complejo. Está filtrando una lista de tres elementos quedándose con
dos de ellos. Pero es independiente, alimenta al sistema con toda la información que necesita y sólo
muestra los detalles relevantes de la misma.
El patrón builder es fácil de implementar en cualquier lenguaje, consiste en ir guardando datos por
el camino y generar la estructura de datos deseada cuando se invoca al método “build”. En el caso
de arriba, este es el código:
Test mantenibles 44
1 function casesWithDiagnoses() {
2 let id = 0;
3 let theDiagnoses = [];
4 let theCases = [];
5
6 function aDiagnosisWith(id: number, location: String) {
7 return {
8 "id": id,
9 "name": "Irrelevant-name",
10 "location": location,
11 "system": "Irrelevant-system",
12 "origin": "Irrelevant-origin",
13 "specie": "Irrelevant-specie"
14 };
15 }
16
17 function aCaseWithDiagnosis(patientName: string, diagnosis: any, id: number = 0)\
18 {
19 return {
20 "id": id,
21 "patientName": patientName,
22 "diagnosisId": diagnosis.id,
23 "diagnosisName": diagnosis.name,
24 "publicNotes": [],
25 "privateNotes": []
26 }
27 }
28
29 function add(locationName) {
30 ++id;
31 let aDiagnosis = aDiagnosisWithLocation(id, locationName);
32 let randomPatientName = "Patient" + Math.random().toString();
33 let aCase = aCaseWithDiagnosis(randomPatientName, aDiagnosis, id);
34 theDiagnoses.push(aDiagnosis);
35 theCases.push(aCase);
36 }
37
38 let builder = {
39 havingDiagnosisWithLocation(locationName: string) {
40 add(locationName);
41 return this;
42 },
43 build: () => {
Test mantenibles 45
44 return {
45 cases: () => {
46 return [...theCases];
47 },
48 diagnoses: () => {
49 return [...theDiagnoses];
50 },
51 patientNameGivenDiagnosisLocation: (name) => {
52 let diagnosisId = theDiagnoses.filter(d => d.location == name)[0\
53 ].id;
54 let theCase = theCases.filter(c => c.diagnosisId == diagnosisId)\
55 [0];
56 return theCase.patientName;
57 },
58 }
59 }
60 };
61 return builder;
62 }
Este código no es trivial realmente, sobre todo la parte en que realiza operaciones de filtrado en la
función patientNameGivenDiagnosisLocation. Generalmente evito todo lo que puedo la complejidad
ciclomática en los test. Es decir, evito bucles, condicionales, llamadas recursivas, etc, porque dispara
la probabilidad de introducir defectos en los test. Por tanto usar aquí la función filter de JavaScript va
en contra de mi propio principio de evitar complejidad ciclomática en los test. Lo hicimos porque en
este caso es lo menos propenso a errores que pudimos encontrar, teniendo en cuenta el procedimiento
que seguimos para extraer el builder, que fue refactoring. Si me pongo a escribir el builder antes de
tener el test funcionando, puede ser que haya fallos en el propio builder y entonces no sepa distinguir
si lo que está mal implementado es el código que estoy probando o son estas herramientas auxiliares
del test. Entonces puede que me vea obligado a depurar y me complique la vida más de la cuenta
implementando tales funciones auxiliares. Así que el proceso consiste en escribir el test tal como lo
vimos al principio, luego implementar la funcionalidad de producción y avanzar hasta tener tres o
cuatro test más en verde. A partir de ahí ya vimos que se convertía en un problema disponer de un
conjunto de datos compartido para todos los test y fuimos poco a poco extrayendo el builder. Un
pequeño cambio, lanzar los test, verlos en verde y así hasta tenerlo terminado. Los propios test sirven
de andamio para el desarrollo del builder, que una vez terminado ofrece un gran soporte a los test
existentes y a los que están por venir. Si la estructura de datos cambia, con suerte sólo tendremos
que cambiar nuestro builder, no los test.
Toda esta labor de ocultar en el test los detalles que son irrelevantes tiene sentido si cuando se
estamos trabajando en el mantenimiento de los test, no sentimos la necesidad de ir a ver cómo
están implementadas las funciones auxiliares. Si tenemos que ir constantemente a mirar el código
del builder o de cualquier otra función auxiliar, es preferible quitar ese nivel de indirección y
Test mantenibles 46
dejar el código dentro del test. Extraer funciones cuando hacemos refactor es importante pero más
importante todavía es eliminar aquellas funciones que demuestran no tener la abstracción adecuada
(aplicando el refactor inline method). El refactor inline consiste en sustituir una abstracción por su
contenido en todos los sitios donde se referencia.
Ejemplo de Inline variable. Código antes del refactor:
1 let a = 1;
2 let b = a + 1;
3 let c = a + 2;
1 let b = 1 + 1;
2 let c = 1 + 2;
1 let x = 1 + 1;
2 let y = 2 + 3;
Mi preferencia por los lenguajes de tipado estático se debe a los increíbles entornos de desarrollo
integrado que existen hoy en día como IntelliJ, Visual Studio + Resharper, Rider, Eclipse, Netbeans,…
en los que todas estas transformaciones clásicas son ejecutadas de forma automática y sin lugar para
el error humano. Toda herramienta que me ayuda a reducir la probabilidad de error humano, es
bienvenida.
Aserciones explícitas
La validación del comportamiento deseado debería ser muy explícita para que la intención de la
persona que programó el test quede lo más clara posible. Si para validar un único comportamiento
necesitamos varias líneas de aserciones, a veces es mejor crear un método propio, sobre todo si esas
líneas van juntas en varios test. Esto pasa con frecuencia cuando operamos con colecciones o con
campos de objetos. Por ejemplo para validar que una lista contiene dos números en un cierto orden
podríamos hacer lo siguiente:
Test mantenibles 47
1 assertThat(list.size).isEqualTo(2)
2 assertThat(list[0]).isEqualTo(10)
3 assertThat(list[1]).isEqualTo(20)
O bien ser más explícitos. Si la librería de aserciones hace comparaciones “inteligentes” basadas en
el contenido y no en las referencias, se pueden escribir expresiones como esta:
1 assertThat(list).isEqualTo(listOf(10, 20))
1 assertThatList(list).isExactly(10, 20)
13 if (archive.filename != actual.filename){
14 failWithMessage("Archive names are different. Expected %s but was %s",
15 archive.filename, actual.filename)
16 }
17 if (archive.content != actual.content){
18 failWithMessage("Archive content is different. Expected %s but was %s",
19 archive.content, actual.content)
20 }
21 return this
22 }
23 }
La clase AbstractAssert pertenece a la libreria AsserJ al igual que el método estático failWithMessage.
La ventaja de implementar nuestros propios métodos de aserción es que generalmente el código es
más sencillo que extendiendo una librería, como puede apreciarse en el ejemplo. Pero la desventaja
es que cuando falla el test, la línea donde se señala el error es dentro de la implementación de nuestro
método de aserción. Esto puede despistar un poco a quien se lo encuentre, que tendrá que seguir la
traza de excepción buscando qué línea del test es la que falla realmente. En cambio, si extendemos la
librería, el test fallido apuntará directamente a la línea dentro del test de una forma directa, sin traza
de excepción. Los matchers que vienen incluidos en las librerías hoy en día son muy completos y
cada vez hace menos falta escribirlos a medida, pero cuando corresponde aportan mucha legibilidad
al test.
Otro caso típico donde podemos ser más o menos explícitos al hacer la validación, es el lanzamiento
de excepciones. Al principio algunos frameworks ni siquiera soportaban la comprobación de
excepciones por lo que teníamos que recurrir a la gestión de excepciones en el test:
1 @Test(expected=IllegalFileException.class)
2 public void should_fail_if_the_file_is_empty(){
3 filter.apply(emptyFile)
4 }
Pero esta forma, a nivel estructural es muy diferente a un test donde existe una línea de aserción
al final, lo cual resulta chocante a la vista. Incluso me ha ocurrido alguna vez que al no ver a bote
pronto la línea de assert he creído que el test estaba mal escrito. El código simétrico suele ser más
fácil de entender. Por eso las librerías han incluido la posibilidad de usar las aserciones también para
excepciones:
Nótese que tanto en Java como en JavaScript y en tantos otros lenguajes, para hacer esta comproba-
ción de que la función va a lanzar la excepción, no la invocamos directamente dentro del test sino
que la envolvemos en una función anónima. Es decir, sin que se ejecute, se la pasamos a la librería.
Si ejecutásemos la función directamente y lanza excepción, la librería no tendría ningún mecanismo
para capturarla puesto que la ejecución se detendría antes de entrar al código de la librería. Esto
es porque en la mayoría de los lenguajes actuales que usan paréntesis, las expresiones se evalúan
de dentro hacia afuera. O sea que ni la función isThrownBy ni la función toThrow de los ejemplos
llegaría a ejecutarse, porque el test se detendría antes con un rojo. Internamente el código de esta
función toThrow podría ser algo como:
Test mantenibles 50
1 function toThrow(theFunctionUnderTest){
2 try {
3 theFunctionUnderTest();
4 fail("The function under test didn't throw the expected exception")
5 }
6 catch (){
7 // It's all good, exception was thrown.
8 }
9 }
Hablando de excepciones, ¿qué comportamiento debería tener nuestra función apply de CsvFilter si
resulta que llega un fichero sin cabecera?, ¿y un fichero completamente vacío?. ¿Debería devolver
una lista vacía?,¿lanzar una excepción quizás? Dejo estas preguntas abiertas como ejercicio de
investigación y reflexión.
Agrupación de test
Hay algo que sigue quedando por hacer en los test de CsvFilter. Es un cambio que yo haría un poco
más adelante cuando la funcionalidad haya crecido un poco más. Imaginemos por un momento que
ya he terminado la funcionalidad del filtro. Todos los tests que excluyen líneas empiezan igual:
• CsvFilterShould.exclude_lines_with_...
Si agrupamos los test por aquellas variantes funcionales que tienen en común (contexto), entonces
podríamos generar dos clases de test, una para cuando no se eliminan líneas y otra para cuando sí:
• CsvFilterCopyLinesBecause.correct_lines_are_not_filtered
• CsvFilterExcludeLinesBecause.tax_fields_must_be_decimals
• CsvFilterExcludeLinesBecause.tax_fields_are_mutually_exclusive
• CsvFilterExcludeLinesBecause.there_must_be_at_least_one_tax_for_the_invoice
Reorganizar los test acorde los nombres de la clase es muy sutil, no siempre funciona. Lo más común
es que los reorganicemos por repetición de contexto en la preparación del test. Cuando hay dos test en
una clase que comparten las mismas líneas de inicialización y otros dos que comparten a su vez otras
líneas de inicialización, entonces podemos separarlos en dos clases diferentes y mover esas líneas de
inicialización a un método anotado como @Before. Cuando hacemos este cambio también es más
fácil reflejar ese contexto común en el nombre de la clase de test. Veamos un ejemplo esquemático.
Estructura inicial:
Test mantenibles 51
1 class TestSuite {
2 @Test public void testA(){
3 arrangeBlock1();
4 actA();
5 assertA();
6 }
7 @Test public void testB(){
8 arrangeBlock1();
9 actB();
10 assertB();
11 }
12 @Test public void testC(){
13 arrangeBlock2();
14 actC();
15 assertC();
16 }
17 @Test public void testD(){
18 arrangeBlock2();
19 actD();
20 assertD();
21 }
22 }
1 class TestSuiteWhenContext1 {
2 @Before public void setup(){
3 arrangeBlock1();
4 }
5 @Test public void testA(){
6 actA();
7 assertA();
8 }
9 @Test public void testB(){
10 actB();
11 assertB();
12 }
13 }
14 class TestSuiteWhenContext2 {
15 @Before public void setup(){
16 arrangeBlock2();
17 }
18 @Test public void testC(){
Test mantenibles 52
19 actC();
20 assertC();
21 }
22 @Test public void testD(){
23 actD();
24 assertD();
25 }
26 }
Cuando practicamos TDD o cuando escribimos test a posteriori para código bien conocido, agrupar
los test por contexto contribuye a la documentación del sistema. Los frameworks tipo RSpec permiten
además anidar contexto:
Los bloques beforeEach se ejecutan en cadena. Primero el global y luego los anidados. Así que antes
de actB, se habrán ejecutado los dos bloques beforeEach. Mientras que antes de actA, sólo se habrá
ejecutado el beforeEach de arriba.
Inicialmente puede que una clase de producción se corresponda con una clase de test pero mediante
refactor y agrupación por contexto puede ser que una clase de producción esté relacionada con dos
o más clases de test. También puede suceder que un mismo conjunto de test ejecute varias clases de
producción.
Test mantenibles 53
En la web del libro xUnit Patterns se describen con detalles los diferentes patrones que existen para
preparar los datos de prueba (Fixture Setup Patterns²¹).
automática de test, técnica conocida como property based testing. Puede lanzar miles de casos que
ponen a prueba el código de forma que un humano escribiendo test automáticos no haría. Y a una
velocidad increíble. Es especialmente útil para probar código que gestiona cambios de estado porque
las transiciones en los diagramas de estados pueden crecer exponencialmente conforme al número de
nodos. Es ideal para testar código que resuelve problemas de explosión combinatoria y para código
complejo en general. En lenguajes como C, cuando tenemos que gestionar la memoria y trabajar
con punteros, la cantidad de elementos que pueden provocar errores es típicamente mayor que si
resolvemos el mismo problema con un lenguaje de alto nivel. La generación automática de pruebas
en estas situaciones revelan casos que difícilmente se nos van a ocurrir al programar.
En lugar de escribir test con datos concretos, la herramienta necesita conocer las propiedades que
esperamos que cumpla el código que va a probarse. Por ejemplo, si una función suma dos números
positivos, una de sus propiedades es que el resultado será mayor que cualquiera de esos dos números.
Definir las propiedades de las funciones es un ejercicio muy bueno para obligarnos a pensar en el
comportamiento del sistema antes de programarlo. Por eso este tipo de pruebas pueden escribirse
antes que el código, siguiendo el ciclo TDD. Pero también son útiles después, como complemento
para explorar casos límite. Y por supuesto pueden usarse para testar código legado siempre que
este sea testable. John Hughes, co-autor de QuickCheck explica en este asombroso vídeo²⁴ cómo
usaban la herramienta para validar la implementación de una parte del protocolo de mensajería de
la tecnología móvil 2G.
QuickCheck ha sido portado a una gran variedad de lenguajes. Además existen otras alternativas
como por ejemplo Hypothesis²⁵, la elegida para nuestro el siguiente ejemplo. Vamos a programar
una función hash criptográfica. Algunas propiedades de una buena función hash en criptografía
son:
Aquí están los dos primeros test y el código de la función bajo prueba, hash, que ahora mismo cumple
con los primeros requisitos:
²⁴https://www.youtube.com/watch?v=AfaNEebCDos&list=PLiM1poinndePbvNasgilrfPu8V4jju4DY
²⁵https://hypothesis.works/
Test mantenibles 56
1 #!/usr/bin/env python3
2 from hypothesis import given, example, assume
3 from hypothesis.strategies import text, integers
4
5 # Function under test:
6 def hash(text):
7 return text
8
9 # Tests:
10
11 @given(text())
12 def test_hash_is_always_the_same_given_the_same_input(text):
13 assert hash(text) == hash(text)
14
15 @given(text(), text())
16 def test_hash_is_different_for_each_input(text1, text2):
17 assume(text1 != text2)
18 assert hash(text1) != hash(text2)
Por defecto se están ejecutando con cien casos cada uno. El primero recibe un texto aleatorio que
utilizo para invocar a la función y comprobar que efectivamente el resultado debe ser el mismo.
El segundo es más complejo. Recibo dos textos aleatorios. Primero asumo que son textos diferentes
(esto se salta los casos en que los textos coincidan) y después me aseguro de que el resultado de la
función debe ser diferente puesto que son entradas diferentes.
Hasta ahora ha sido muy fácil conseguir que los dos test pasen pero viene la regla de la longitud fija:
1 @given(text())
2 def test_hash_has_always_the_same_fixed_length(text):
3 assert len(hash(text)) == 10
Ahora lo tengo que trabajar mucho más para que el test pase de rojo a verde. El código que me
parece más fácil es el siguiente:
Test mantenibles 57
1 def hash(text):
2 if len(text) < 10:
3 hash = text + str(random.random())
4 return hash[0:10]
5 return text
Estaba pensando en el caso en que el texto no fuese lo suficientemente largo. Pensé que con rellenarlo
de números pseudo-aleatorios conseguiría el verde. Pero entonces Hypothesis envió un texto de más
de 10 caracteres y así pude ver que mi código era incorrecto:
1 text = '00000000000'
2
3 @given(text())
4 def test_hash_has_the_same_fixed_length(text):
5 > assert len(hash(text)) == 10
6 E AssertionError: assert 11 == 10
7 E + where 11 = len('00000000000')
8 E + where '00000000000' = hash('00000000000')
9
10 hash.py:26: AssertionError
11 ----------------------------- Hypothesis ------------------------------
12 Falsifying example: test_hash_has_the_same_fixed_length(
13 text='00000000000',
14 )
15 ===================== 2 failed, 1 passed in 0.60s ====================
1 def hash(text):
2 if len(text) < 10:
3 hash = text + str(random.random())
4 return hash[0:10]
5 else:
6 return text[0:10]
7 return text
1 text = ''
2
3 @given(text())
4 def test_hash_is_always_the_same_given_the_same_input(text):
5 > assert hash(text) == hash(text)
6 E AssertionError: assert '0.84442185' == '0.75795440'
7 E - 0.84442185
8 E + 0.75795440
9
10 hash.py:19: AssertionError
11 ----------------------------- Hypothesis ------------------------------
12 Falsifying example: test_hash_is_always_the_same_given_the_same_input(
13 text='',
14 )
15 ===================== 1 failed, 2 passed in 0.71s ====================
¡Se me había olvidado que no va a ser tan fácil como usar números aleatorios!
Escribir una buena función hash no es precisamente trivial. Gracias a esta herramienta de pruebas,
podemos adquirir un gran nivel de confianza en el código, que no sería posible de otra forma. Dejo
el resto del ejercicio como propuesta para la lectora.
Para seguir practicando con test basados en propiedades, Karumi tiene publicado un ejercicio muy
interesante que se llama MaxibonKata²⁶, para Java y JUnit-QuickCheck.
²⁶https://github.com/Karumi/MaxibonKataJava
Premisa de la Prioridad de
Transformación
La premisa de la prioridad de transformación es un artículo²⁷ de Robert C. Martin (Transformation
Priority Premise - TPP) que explica que, cada nuevo test que convertimos en verde debe provocar
una transformación en el código de producción que lo haga un poco más genérico de lo que era antes
de añadir ese test.
Recordemos que para conseguir que un test pase de rojo a verde escribimos el código que menor
esfuerzo requiera, bien sea el más rápido o el más sencillo que se nos ocurra. Es como jugar a trucar
la implementación para que pase haciendo trampa. Por eso se suele decir “fake it until you make it”.
No estamos generando código incorrecto sino código incompleto. Así que para que los primeros test
pasen, nos vale con respuestas literales como “devolver dos”, sin hacer ningún cálculo. Escribimos un
código muy específico para que el test pase. Sin embargo, a medida que añadimos test los anteriores
deben seguir pasando también y esto nos obliga a ir escribiendo un código que es cada vez más
completo. Los beneficios del minimalismo son principalmente la simplicidad y la inspiración que
nos brinda el código a medio hackear para poder pensar en casos de uso que deberíamos contemplar.
Si vamos añadiendo test que se van poniendo en verde pero el código no se va generalizando un
poco más con cada uno, entonces estamos haciendo mal TDD. Es un camino que no va a llevarnos a
ninguna parte. Ni vamos a ser capaces de terminar la implementación en pasos cortos progresivos, ni
se nos van a ocurrir más test, ni ninguno de los beneficios que nos puede aportar TDD. No podemos
jugar indefinidamente a devolver valores literales (hardcoded). Ejemplo:
1 function getPrimeFactorsFor(number){
2 if (number == 2){
3 return [2];
4 }
5 if (number == 4){
6 return [2,2];
7 }
8 if (number == 6){
9 return [2,3];
10 }
11 }
Esta es una función que calcula los números primos que multiplicados devuelven el número de
entrada. Tiene tres condicionales porque se han escrito tres test para llegar hasta aquí. El nivel de
²⁷https://blog.cleancoder.com/uncle-bob/2013/05/27/TheTransformationPriorityPremise.html
Premisa de la Prioridad de Transformación 60
concreción del código era igual desde que se puso en verde el primer test, se está devolviendo una
respuesta literal. La forma actual del código no sugiere ninguna pista sobre su posible generalización.
No avanza en la dirección de la implementación final. No es correcto, hay que esforzarse un poco
más por generalizar el código antes de seguir añadiendo test.
Pensemos en las partes de una solución como si fueran las ramas de un árbol. Recordaremos que los
árboles se pueden explorar en profundidad o en anchura. Me gusta practicar TDD en profundidad
de manera que cuando me decido a implementar una funcionalidad, trabajo en ella hasta terminarla
antes de empezar otra. No abro ramas paralelas de la solución. Porque entonces me queda mucho
código concreto que no tiende a generalizarse hacia ninguna parte. En el caso de la función anterior,
para mí el problema de la descomposición en factores primos tiene dos ramas. Una es averiguar qué
número es primo. Dentro de esta rama está el problema de encontrar todos los primos menores a un
número dado. La otra rama es la de coleccionar todos los primos que al multiplicarse producen el
número original. Es decir, una rama consiste en buscar los primos inferiores y la otra en descomponer
el número en esos primos. Con este entendimiento del algoritmo que tengo en la cabeza puedo
orientar el rumbo de TDD.
Verde:
1 function getPrimeFactorsFor(number){
2 return [2];
3 }
Rojo:
Premisa de la Prioridad de Transformación 61
Verde:
1 function getPrimeFactorsFor(number){
2 let factors = [2];
3 if (number / 2 > 1){
4 factors.push(2);
5 }
6 return factors;
7 }
1 function getPrimeFactorsFor(number){
2 let factor = 2;
3 let factors = [factor];
4 let remainder = number / factor;
5 if (remainder > 1){
6 factors.push(factor);
7 }
8 return factors;
9 }
Rojo:
Verde:
Premisa de la Prioridad de Transformación 62
1 function getPrimeFactorsFor(number){
2 let factor = 2;
3 let factors = [factor];
4 let remainder = number / factor;
5 if (remainder > 1){
6 factors = factors.concat(getPrimeFactorsFor(remainder));
7 }
8 return factors;
9 }
¿Por qué me ha parecido natural y sencillo añadir una llamada recursiva para pasar de rojo a verde?
Porque había aclarado el código anteriormente con variables que explícitamente decían lo que el
código estaba haciendo. Para facilitar la generalización, busque nombres adecuados y código auto-
explicativo.
La rama de la funcionalidad que descompone los números divisibles por dos, está completada. Ahora
lo que me sugiere el código en base a las partes que han quedado menos genéricas, es trabajar en un
número divisible por tres, para obligarme a seguir avanzando en la generalización:
Rojo:
Verde:
1 function getPrimeFactorsFor(number){
2 let factor = 2;
3 if (number % factor != 0) {
4 factor = 3;
5 }
6 let factors = [factor];
7 let remainder = number / factor;
8 if (remainder > 1){
9 factors = factors.concat(getPrimeFactorsFor(remainder));
10 }
11 return factors;
12 }
Premisa de la Prioridad de Transformación 63
Este código no sólo hace que pase el último expect sino que sin querer, también funciona para los
resultados [3,3,3] y [2,3] por ejemplo. Si tengo la duda de que vaya a funcionar, a veces pongo los test
un momento para asegurarme pero luego generalmente no los dejo, los borro. Porque intento limitar
al máximo la redundancia de test para limitar su mantenimiento. Más test no es necesariamente
mejor. Lo que está claro es que cuando el número sea solamente divisible por cinco, no va a funcionar,
así que escribo el siguiente test:
Rojo:
Verde:
1 function getPrimeFactorsFor(number){
2 let factor = 2;
3 while (number % factor != 0) {
4 ++factor;
5 }
6 let factors = [factor];
7 let remainder = number / factor;
8 if (remainder > 1){
9 factors = factors.concat(getPrimeFactorsFor(remainder));
10 }
11 return factors;
12 }
Decido dejar el último expect a modo de redundancia porque me aporta seguridad probar con un
ejemplo muy complejo y comprobar que el resultado es correcto. Aquí sería muy conveniente añadir
test basados en propiedades.
A diferencia de otros ejemplos anteriores, aquí he dejado un solo bloque it con todas las variantes de
casos dentro, múltiples aserciones dentro del mismo test. Cuando el nombre del test es válido para
todas ellas, no tiene por qué ser un problema. Cuando el problema es algorítmico, como en este,
a veces prefiero terminar la funcionalidad y luego repensar el nombre de los test y su agrupación.
Se me ocurre que para explicar mejor el comportamiento del sistema, ahora los podemos partir en
varios test que sumen a la documentación:
De paso he aprovechado para quitar algunas aserciones que eran redundantes y me he quedado con
una combinación representativa de ejemplos básicos que me pueden ayudar a depurar y de ejemplos
más complejos que ejercitan las diferentes ramas de la solución.
A continuación estudio si puedo aplicar los últimos refactors al código de producción con objetivo
de simplificarlo y dejarlo más legible.
Refactor: inline variable (factors)
Premisa de la Prioridad de Transformación 65
1 function getPrimeFactorsFor(number){
2 let factor = 2;
3 while (number % factor != 0) {
4 ++factor;
5 }
6 let remainder = number / factor;
7 if (remainder > 1){
8 return [factor].concat(getPrimeFactorsFor(remainder));
9 }
10 return [factor];
11 }
Refactor: invert if condition. Para mejorar legibilidad, el código recursivo es más intuitivo cuando
la condición de parada está antes que la llamada recursiva.
1 function getPrimeFactorsFor(number){
2 let factor = 2;
3 while (number % factor != 0) {
4 ++factor;
5 }
6 let remainder = number / factor;
7 if (remainder <= 1) {
8 return [factor];
9 }
10 return [factor].concat(getPrimeFactorsFor(remainder));
11 }
Refactor: extract method. Hago explícitas las dos ramas de la solución usando funciones separadas.
1 function getPrimeFactorsFor(number){
2 let prime = findSmallestPrime(number);
3 let remainder = number / prime;
4 if (remainder <= 1) {
5 return [prime];
6 }
7 return [prime].concat(getPrimeFactorsFor(remainder));
8 }
9
10 function findSmallestPrime(number) {
11 let factor = 2;
12 while (number % factor != 0) {
13 ++factor;
Premisa de la Prioridad de Transformación 66
14 }
15 return factor;
16 }
Por último estudio el código en busca de casos límite que se me hayan podido olvidar. Reviso
complejidad ciclomática, reviso los condicionales pensando en qué tipo de casos podrían hacerlos
fallar. El bucle es sospechoso. Entonces me doy cuenta de que para números menores que el dos,
incluidos los números negativos, el bucle no termina nunca. Añado un test para ello. Rojo:
Verde:
1 function findSmallestPrime(number) {
2 if (number == 1) {
3 return 1;
4 }
5 let factor = 2;
6 while (number % factor != 0) {
7 ++factor;
8 }
9 return factor;
10 }
¿Cómo debería comportarse el código ante el cero o un número negativo? No se me ocurre ningún
comportamiento razonable más que el de lanzar una excepción. Si implementase algo como procesar
el número de entrada en valor absoluto, cualquier programadora se llevaría una sorpresa ante ese
comportamiento. Más aún si decidiese devolver una lista vacía o nula.
• Que la primera función me devolviese una lista de usuarios encontrados por nombre sin alterar
el estado del sistema. Que sea una función de tipo consulta (query).
• Que la segunda función cambie el estado del sistema, asignando como email el que recibe como
parámetro. Que sea una función de tipo comando.
• Que la tercera función devuelva el cálculo de la raíz cuadrada del parámetro de entrada.
Verde:
1 function getPrimeFactorsFor(number){
2 if (number < 1){
3 throw new Error('Only positive numbers are allowed');
4 }
5 let prime = findSmallestPrime(number);
6 let remainder = number / prime;
7 if (remainder <= 1) {
8 return [prime];
9 }
10 return [prime].concat(getPrimeFactorsFor(remainder));
11 }
Premisa de la Prioridad de Transformación 68
Refactor: igualar el nivel de abstracción de las funciones, separar responsabilidades, eliminar avisos
del linter, hacer inaccesibles las funciones privadas.
1 function getPrimeFactorsFor(number) {
2 checkForPositiveNumber(number);
3 return primeFactors(number);
4
5 function checkForPositiveNumber(number) {
6 if (number < 1) {
7 throw new Error('Only positive numbers are allowed');
8 }
9 }
10
11 function primeFactors(positiveNumber) {
12 let prime = findSmallestPrime(positiveNumber);
13 let remainder = positiveNumber / prime;
14 if (remainder <= 1) {
15 return [prime];
16 }
17 return [prime].concat(primeFactors(remainder));
18 }
19
20 function findSmallestPrime(number) {
21 if (number === 1) {
22 return 1;
23 }
24 let factor = 2;
25 while (number % factor !== 0) {
26 ++factor;
27 }
28 return factor;
29 }
30 }
Existen multitud de posibilidades de implementación de este ejercicio. Buscando por “prime factors
kata” en Internet, aparecen vídeos y artículos con soluciones a este problema.
TPP
Cuando introduje la generalización del código mediante una llamada recursiva, también podía haber
introducido un bucle while con poco esfuerzo, pero el código recursivo ha quedado muy simple.
Premisa de la Prioridad de Transformación 69
Esta escalera de tácticas de refactoring para la generalización no tiene por qué seguirse al pie de la
letra ni tienen por qué utilizarse los 12 pasos. En el ejercicio de los factores primos (propuesto también
por Robert C. Martin), sólo he usado algunos de ellos. Según el problema puede que algunas de estas
transformaciones no puedan aplicarse o no tenga sentido dar pasos tan pequeños. Pero resulta útil
considerar esta secuencia de transformaciones a la hora de elegir los test, pensando, ¿qué caso de
prueba elijo para poder aplicar la siguiente transformación de la lista? Como resultado puede que
encontremos más casos de prueba y seamos capaces de descomponer mejor el problema. O puede
que encontremos una forma más conveniente de ordenar los casos de prueba que teníamos pensados.
Tanto la elección de los casos de prueba como el orden en que los abordamos son determinantes para
maximizar los beneficios de TDD.
Como ejemplo tomemos el quinto elemento de la lista (statement -> statements), que consiste
en generalizar el código pasando de una sentencia a varias sentencias sin usar condicionales ni
ninguna de las otras transformaciones de la lista. Y tomemos también como ejemplo una función
que recibe una cadena y produce otra en formato CamelCase, traduciendo tanto espacios como otros
separadores. Estamos en medio de la sesión de TDD y el código está así:
1 function toCamelCase(text){
2 const words = text.split(/[ ,_-]/g)
3 return words.join("")
4 }
La función une las palabras pero todavía no convierte en mayúscula la primera letra de cada palabra
porque se han ido escogiendo los test de forma que aún no ha hecho falta. Ahora para poner en
práctica la quinta transformación puedo elegir un test con una sola palabra cuya primera letra es
minúscula.
Rojo:
Premisa de la Prioridad de Transformación 70
1 function toCamelCase(text){
2 const words = text.split(/[ ,_-]/g)
3 let word = words[0]
4 word = word.charAt(0).toUpperCase() + word.substr(1)
5 words[0] = word
6 return words.join("")
7 }
El esfuerzo por avanzar en la generalización sin aumentar en complejidad ciclomática (ni condicio-
nales, ni bucles ni llamadas recursivas), me permite dar un pasito pequeño pero rápido y directo
para resolver una parte del problema. Acto seguido resulta muy fácil seguir generalizando:
1 function toCamelCase(text){
2 return text.split(/[ ,_-]/g).map(word => {
3 return word.charAt(0).toUpperCase() + word.substr(1)
4 }).join("")
5 }
Puede resultar llamativo que haya aplicado una generalización al código sin haber escrito un test
primero. Pero es que hay veces que añadir un test más, implica elegir un caso más complejo y
esto dificulta la búsqueda de la generalización. Puede llevarnos a añadir más sentencias que van
en una dirección diferente a la generalización. Es decir, puede resultar más fácil generalizar con
un test en verde como parte del refactor. Escribir un test que falla es crítico para empezar a
implementar un comportamiento nuevo pero no tanto cuando se trata de triangular o encontrar una
generalización. Hay veces que ese nuevo test es incluso redundante. Cuando tenemos la sensación
de estar escribiendo test redundantes sólo por seguir la regla de “escribir el test primero”, podemos
plantearnos generalizar sin más test o incluso modificar el último test si fuera necesario para
demostrar la necesidad de esta transformación.
La primera vez que vi la escalera de transformaciones de TPP me resultó curioso que el autor prioriza
la recursividad por encima de la iteración con bucles. Esto es algo que muy pocos programadores
utilizan, lo más común es ver el uso de bucles. Sin embargo hay problemas que naturalmente son
recursivos y por tanto el código más simple se obtiene mediante recursividad. Para trabajar con la
TPP, el autor propuso un ejercicio llamado Word Wrap Kata, que es básicamente el algoritmo que
implementan muchos editores de texto sencillos como notepad o gedit, donde las líneas de texto
que no caben en el ancho de la ventana se parten en más líneas más cortas para que el texto pueda
leerse en el mismo ancho. Algunos editores llaman a esto word wrap o text wrap, incluidos editores
Premisa de la Prioridad de Transformación 71
de código fuente. Este es un ejercicio que utilizo en cursos de formación para developers que están
empezando con TDD. Hay varias soluciones propuestas por Robert C. Martin, una de ellas en el
propio artículo de la TPP. Otra la explica en uno de sus videos de la serie Clean Coders, llamado
Advanced TDD²⁸. La gran mayoría de las personas que he visto enfrentarse a este problema se
atascan porque se anticipan en la implementación, añadiendo un código mucho más complejo que
el requerido para hacer pasar cada uno de los test. Fallan eligiendo los ejemplos, o bien el orden en
que los abordan o fallan complicando el código más de lo necesario. Es un ejercicio muy interesante
que recomiendo realizar varias veces con diferentes enfoques. No lo voy a resolver en este libro sino
que lo dejo como propuesta. Mi amigo Peter Kofler publicó en su blog²⁹ todas las soluciones que
consiguió hacer para este ejercicio.
Son estas contraseñas de las que nunca me acuerdo. La firma de la función sería algo como esto:
Lo que propongo a los participantes cuando realizamos este ejercicio es que antes de programar
generen una lista completa de todos los casos representativos y que los ordenen por su complejidad.
Después les pido que hagan TDD. Antes de seguir leyendo, le propongo a usted que practique el
ejercicio aunque sea mentalmente (si es delante de la computadora mejor), para después comparar
y descubrir los errores cometidos.
³⁰https://agileforall.com/author/rmyers/
³¹https://groups.io/g/testdrivendevelopment/topics
Criterios de aceptación 74
Lo que confunde a la gente en este ejercicio es que las reglas de negocio no están aisladas sino que
van todas juntas, se deben cumplir a la vez para que el sistema responda que la contraseña es fuerte.
Típicamente la gente quiere probar sólo una de las reglas para ir poco a poco, se anticipan demasiado
a la posible implementación del código de producción y entonces algunos escriben funciones que
deberían ser privadas (containsNumber por ejemplo) y las ponen como públicas para poderlas testar.
No es un problema si luego las volviesen a poner privadas, aunque el camino más corto para
resolver este ejercicio con TDD es invocar directamente a la función que queremos que sea pública
y olvidarnos de los posibles bloques que tendrá internamente.
Suelo decir que llevan el sombrero de programadora puesto cuando deberían primero llevar el
sombrero de analista de negocio o de agile tester. Cuando pensamos en los criterios de aceptación
(reglas de negocio) y los ejemplos que las ilustran, es mejor que nos olvidemos de la posible
implementación. Es mejor que pensemos en el sistema como en una caja negra y nos limitemos
a tener claras cuáles son sus entradas y sus salidas, nada más.
Por defecto la mayoría de los lenguajes toman como falso el valor por defecto de una variable/función
booleana. Entonces si el primer test lo escribimos afirmando falso, nos quedaría directamente en
verde sin programar nada, solamente dejando la función vacía (en JavaScript por ejemplo). TDD
dice que el test debe estar en rojo para empezar, no en verde. Por tanto si no queremos un verde
directo, buscamos el rojo:
Verde:
1 function isStrongPassword(password){
2 return true;
3 }
¿Pensaba que para poder hacer pasar este test tenía que escribir un montón de código? Recuerde,
solamente el mínimo para que pase, nada más. Este código ya es correcto para todos los casos en
que la contraseña es fuerte.
A partir de aquí sabemos que todas las demás aserciones tendrán que comparar con falso, puesto
que el caso verdadero ya está cubierto.
Nótese que el ejemplo de contraseña que estoy usando en el test cumple todos los requisitos de una
contraseña fuerte excepto la longitud. Es muy importante que elijamos los ejemplos más sencillos
posibles para demostrar la regla de negocio y que sólo incumplan esa regla y no otras.
Verde:
1 function isStrongPassword(password){
2 return password.length >= 6;
3 }
Siguiente rojo:
De nuevo el ejemplo ilustra el criterio de aceptación concreto sin mezclar con los otros. Conseguirlo
pasar a verde y terminar el resto del ejercicio ya no tiene misterio.
Realizando este ejercicio es muy común que la gente ponga test con aserciones incorrectas con la
idea de cambiarlas después:
Lo primero es que no se piensan mucho los nombres de los test. Lo segundo que el ejemplo incumple
varias reglas a la vez. Y lo peor de todo es que la línea de expect está afirmando algo que va en
contra de los requisitos de negocio. Una contraseña corta no puede validarse como fuerte. ¿Cómo
pueden evitarse este tipo de errores? Pensando un poco más antes de programar, eligiendo mejores
ejemplos. Una forma de hacer la lista de ejemplos antes de programar podría ser la siguiente:
Las personas que empezaban a programar con una lista de ejemplos tan clara y bien ordenada, no
tenían problema en programar el ejercicio rápido y cumpliendo el ciclo rojo-verde-refactor. Los que
tenían prisa por programar y partían con un par de casos mal planteados, incumplieron la mayoría
de las reglas de TDD incluido modificar el test o testar funciones privadas directamente.
Criterios de aceptación 76
Los programadores tenemos cierta ansiedad por ver los programas terminados y funcionando desde
que nos plantean un problema. Parece que la silla quema hasta que por fin abrimos el editor de
código fuente y nos ponemos a escribir líneas de código a toda velocidad. A veces hay urgencias
reales, sobre todo cuando hay un fallo crítico en producción, pero en mi experiencia la sensación
de urgencia y la ansiedad por terminar es auto-impuesta la mayoría de las veces. Parece que si no
estamos programando estamos perdiendo el tiempo. Pensar es más importante que escribir código
y más barato. En los ochenta y los noventa se daba el problema contrario, durante muchos meses,
séis o más, se pensaba, se analizaba, se escribía documentación para luego programar. Se generaba
un gran tomo de documentación que prometía anticipar cualquier situación que pudiera aparecer
en el software para que las programadoras luego tuviesen todo el camino llano y escribir código
fuese una tarea trivial. Ni un extremo ni el otro funcionan bien para escribir código mantenible sin
desperdicio.
• 1ab_2331 ⇒ fuerte
• 1_xyz23a ⇒ débil
• 3xasdflk23 ⇒ fuerte
• abcd_1234 ⇒ débil
Dados estos ejemplos, ¿puede inferir cuáles son las reglas de fortaleza de la contraseña? Probable-
mente no. Las reglas para que este sistema de por fuerte una contraseña eran:
Por eso es tan importante que los nombres de los test contengan la regla de negocio y que las
aserciones sean las justas y sean certeras y claras. Es un buen principio intentar que sólo haya una
³²https://cucumber.io/blog/example-mapping-introduction/
Criterios de aceptación 77
aserción por test, aunque a veces se necesitan varias para afirmar un comportamiento esperado, por
ejemplo cuando se trabaja con colecciones o con objetos. También puede que una regla de negocio
requiera varios ejemplos para quedar bien ilustrada, tal como ocurrió con el ejemplo de los factores
primos del capítulo anterior. En estos casos no es un problema que existan varias aserciones. Lo
importante es que las personas que tienen que mantener los test entiendan claramente tanto los
criterios de aceptación como los ejemplos y los puedan distinguir.
Conocer y definir los criterios de aceptación es una parte fundamental del análisis del proyecto. En el
software empresarial los requisitos no cambian tanto como parece, porque el negocio normalmente
no está cambiando constantemente. El usuario no cambia su negocio cada vez que le entregamos
una nueva versión del software. Lo que sucede a menudo es que los requisitos son mal expresados y
mal entendidos o no se hacen explícitos ni siquiera. Aquí está uno de los grandes beneficios de BDD
y de TDD, mejorar la comunicación.
Mock Objects
Es muy difícil entender para qué y cómo usar los mocks cuando nunca se ha enfrentado al problema
que resuelven. Típicamente es porque no tiene costumbre de escribir test. Al principio parecen un
artefacto muy complejo pero cuando por fin los entienda, verá que no hay tanta variedad de mocks
ni tantas formas de usarlos.
Si está probando una función pura, es decir, que sólo depende de sí misma, no hay cabida para
los mocks. La necesidad surge cuando quiere probar una función o método que depende de otra
función o método. Supongamos que la clase A contiene a la función f, que al ejecutarse invoca a la
función g, que pertenece a la clase B. Es decir que clase A depende de clase B. Puede que testar el
comportamiento de la función f sea muy complicado dada su interacción con la función g.
Cuando hay varios artefactos que interaccionan, la primera pregunta que debemos responder es,
¿cuál de los dos quiero probar en este momento? Es decir, ¿estoy probando la clase A o la clase B?
Es una pregunta clave ya que seguramente queremos probar las dos clases pero quizás no a la vez.
Puede que sea más fácil probar una y luego la otra. Si mi objetivo es probar A, entonces no puedo
usar ningún tipo de mock para simular A, sino que debo ejercitar el código real de A. Y como mi
objetivo es probar A, puedo plantearme usar un mock para B. Es decir, como B no es el objetivo
directo de mi prueba en este momento, tengo dos opciones; utilizar el código real de B o simular el
comportamiento de B con un sucedáneo de B.
En lenguajes que utilizan clases como Java, Kotlin, C#, etc, podremos reemplazar la implementación
real de una dependencia por una simulación, si el código está preparado para inyección de
dependencias:
En este ejemplo puedo inyectar por constructor una instancia de cualquier clase que implemente la
interfaz Dependency.
Los mocks no son mas que objetos o funciones que suplantan partes del código de producción, para
simular los comportamientos que necesitamos en nuestros test.
Podría darse el caso de que la función que queremos probar dependa de otra función que está en
la misma clase, en cuyo caso, según el lenguaje de programación y las herramientas utilizadas,
Mock Objects 79
puede que sea más difícil de trabajar con mocks. Este caso se da habitualmente con código legado
y veremos más adelante en este capítulo opciones para resolver el problema. Pero si el código que
estamos escribiendo es nuevo, no debería ser complejo usar mocks. Si estamos usando alguno de
estos lenguajes que utilizan clases lo correcto es que, de tener que usar mocks, sea para suplantar
funciones que están en otras clases y no en la misma que estamos probando. Esto puede servir de
pista para orientarnos a la hora de diseñar software. Si estamos escribiendo código nuevo con Java
por ejemplo y tenemos una sola clase y nos vemos en la necesidad de suplantar una de sus funciones,
entonces lo más probable es que haya llegado el momento de descomponer esa clase en dos o más
clases que se relacionan mediante inyección de dependencias.
En el libro xUnit Patterns de Gerard Meszaros³³ se recogen los distintos tipos de mock que podemos
usar en los test. El nombre genérico que Meszaros usó para hablar de estos objetos usados para
simulaciones fue el de doble de prueba. Como los dobles de los actores en las películas de acción. Sin
embargo ni el nombre de doble ni los distintos tipos de doble se han estandarizado en la industria.
En su lugar se ha impuesto la terminología usada por los frameworks y librerías más populares
como Mockito, Sinon o Jest. Lo que según Mockito es un mock, para Meszaros es un spy. Y lo que
para Mockito es un spy, no tiene equivalencia en xUnit Patterns (yo le llamo proxy). En parte el
problema surge porque la palabra mock en inglés significa sucedáneo y por tanto parece ser aplicable
a cualquier doble de test. Pero resulta además que mock es también un tipo específico de doble,
diferente de otros como spy y stub. Entonces el mock (estricto) es un tipo de mock. Por eso algunas
librerías le llaman mock a todos los dobles, porque se refieren al significado de la palabra en inglés.
Existen algunas librerías como jMock, donde el tipo de doble creado por defecto es un mock estricto
pero las librerías más populares por defecto sirven espías (spy). No es casualidad que jMock tenga
este estilo ya que está escrito por Steve Freeman y Nat Pryce entre otros, los co-autores de los mock
objects. Los padres de la criatura. Sigo basándome en la terminología definida en xUnit Patterns
para explicar los mocks pero lo importante es conocer el comportamiento de cada objeto según la
herramienta que usemos, junto con sus ventajas e inconvenientes. En la web de xUnit Patterns³⁴
están muy bien explicados los distintos tipos de doble con diagramas y sus posibles usos. Por su
parte los frameworks y librerías también documentan el comportamiento de sus mocks, conviene
leer su documentación para evitar sorpresas.
Hasta ahora en los test que hemos visto, el código de producción era una caja negra con una entrada
directa (argumentos) y una salida directa (valor de retorno). Hay artefactos de código donde puede
que la entrada o la salida o ambas sean indirectas. Por ejemplo una función F que no devuelve
nada, no admite aserciones sobre su valor de retorno porque no lo tiene. Sin embargo esa función
F probablemente tiene un comportamiento observable, quizás su salida consiste en invocar a otra
función, G. Si podemos suplantar a G podremos comprobar que F interactúa con G de la manera
esperada.
Mock y Spy
³³http://xunitpatterns.com
³⁴http://xunitpatterns.com/TestDoublePatterns.html
Mock Objects 80
Esta función no devuelve nada, su trabajo consiste en interactuar con otras funciones. Primero pide
al objeto usuario que actualice la contraseña. Luego pide a su dependencia repository que guarde el
usuario. Para poder asegurarnos que la función se comporta como esperamos debemos suplantar a
su dependencia:
1 // Production code:
2 public class Service {
3 private Repository repository;
4 public Service(Repository repository){
5 this.repository = repository;
6 }
7 public void updatePassword(User user, Password password){
8 user.update(password);
9 repository.save(user);
10 }
11 }
12 // Tests:
13 public class ServiceShould {
14 @Test public void
15 save_user_through_the_repository(){
16 Repository repository = mock(Repository.class);
17 Service service = new Service(repository);
18 User user = new User();
19
20 service.updatePassword(user, new Password("1234"));
21
22 verify(repository).save(user);
23 }
24 }
En este ejemplo escrito en Java, el servicio admite la inyección de su dependencia por constructor.
Gracias a ello podemos inyectar una versión falsa de la misma. El código del test está usando las
funciones estáticas de Mockito, mock y verify. La primera genera una instancia de tipo Repository
con implementación falsa. La segunda interroga al objeto para preguntarle si se ha producido la
llamada al método save con el parámetro user. En caso positivo el test resulta verde, de lo contrario
el test resulta rojo.
En lenguajes para JVM como Java y Kotlin, al igual que pasa para .Net con C# y otros lenguajes,
hay librerías que generan clases nuevas en tiempo de ejecución. Escriben código intermedio que
Mock Objects 81
He llamado Spy al doble pero le podría haber llamado Mock. Tanto el espía como el mock en la
terminología de Meszaros son objetos que tienen memoria para registrar las llamadas que se les
hacen. Por otro lado el Stub no tiene memoria sino que simplemente devuelve los valores que le
digamos. Spy y Mock se usan para validar salida indirecta, mientras que Stub se utiliza para simular
entrada indirecta como veremos más adelante. La diferencia entre Spy y Mock es sutil porque ambos
tienen memoria. Hay un artículo muy bueno³⁵ del autor y programador J.B Rainsberger que explica
las ventajas y los inconvenientes de elegir uno u otro (los comentarios de su artículo son también
interesantes). Básicamente el mock estricto valida que sólo se hacen las llamadas que se le ha dicho
que van a ocurrir, mientras que el espía no se molesta si ocurren otras llamadas que no se le han
dicho. El espía se limita a responder a nuestras preguntas desde el test, es más discreto. Además las
librerías de mocks estrictos requieren que las llamadas que van a ocurrir se especifiquen antes de la
ejecución del código de producción, a lo cual denominan expectativas.
Escuché decir a alguien que un framework es aquel código que se encarga de invocar a tu código,
mientras que una librería es aquel código que debe ser invocado por tu código. JUnit se encarga de
buscar los test y ejecutarlos, por lo tanto es un framework. jMock me da mocks cuando los pido, por
tanto soy yo quien invoco al código, con lo cual es una librería. Algunas herramientas como Jest o
Mockito incluyen ambas cosas, parte de framework y parte de librería.
³⁵https://blog.thecodewhisperer.com/permalink/jmock-v-mockito-but-not-to-the-death
Mock Objects 82
Stubs
Cuando la entrada de la función que queremos probar no es directa, es decir que no depende
solamente de los argumentos, podemos simular la fuente de datos con un stub. ¿Cómo podemos
testar la siguiente función?
Mock Objects 83
Una opción que a veces prefiero es insertando los datos que necesito para el test en la base de datos y
escribiendo un test sin mocks, usando un repositorio real. Un test de integración. Sobre todo cuando
el código de la función que estoy probando es muy sencillo. Lo pienso dos veces antes de usar mocks
para probar funciones con una o dos líneas si va a quedar un test más complejo que el propio código
de producción. No obstante si es muy lento o costoso acceder a la base de datos o si hay más capas
en medio o si el código que quiero probar es algo más complejo, prefiero suplantar el repositorio con
un stub.
22
23 assertThat(result.size()).isEqualTo(1);
24 assertThat(result.get(0)).isEqualTo(user);
25 }
26
27 @Test public void
28 search_users_by_surname_when_nothing_is_found_by_name(){
29 RepositoryStub repository = new RepositoryStub();
30 Service service = new Service(repository);
31 String aName = "irrelevantName";
32 User user = new User();
33 repository.stubListOfUsersBySurname = Arrays.asList(user)
34
35 List<User> result = service.findUsers(aName);
36
37 assertThat(result.size()).isEqualTo(1);
38 assertThat(result.get(0)).isEqualTo(user);
39 }
40 }
Lo mismo podemos escribirlo con menos líneas usando una herramienta como Mockito. De paso
voy a mostrar los test después del refactor:
21
22 @Test public void
23 search_users_by_surname_when_nothing_is_found_by_name(){
24 when(repository.findUsersBySurname(aName))
25 .thenReturn(Arrays.asList(user));
26
27 assertThat(service.findUsers(aName)).containsOnly(user);
28 }
29 }
He utilizado la función estática de Mockito, when, que sirve para configurar la respuesta que debe dar
la función cuando se le llame con los argumentos especificados. Esta función devuelve un objeto con
una serie de métodos como thenReturn o thenThrow que permiten especificar el comportamiento
exacto de la función.
En lenguajes dinámicos como Python, Ruby o JavaScript es más sencillo suplantar funciones porque
no es necesario recurrir a mecanismos de herencia sino que directamente se puede recurrir al duck
typing y a sobreescribir funciones de objetos:
Inyectamos un objeto literal con la misma interfaz que el servicio espera. Otra opción es instanciar
el objeto real y luego reemplazar las funciones que necesitemos:
Mock Objects 86
Hace unos años estaba usando Python en un proyecto y no había ninguna librería de mocks que me
convenciera por lo que decidí implementar una API similar a Mockito para Python y la desarrollé
usando TDD. Fue un ejercicio muy divertido. En Python existen los Magic Methods, entre ellos hay
hooks para obtener el control de flujo cuando se produce una llamada a un método de un objeto
que no existe. Con este truco puede implementar buena parte de los dobles. Las herramientas de
metaprogramación de Python y Ruby son muy potentes. Respeté la terminología de Meszaros a la
hora de nombrar a los dobles. Tiempo después, mi amigo David Villa hizo un fork del proyecto
(Python Doublex) y lo mejoró considerablemente, añadiendo documentación clara que puede leerse
online³⁶. Muy útil para ayudar a entender los dobles en Python. A día de hoy es mi librería favorita
cuando programo con este lenguaje.
Combinaciones
Hay test que prueban métodos de objetos que dependen de otros dos colaboradores, por lo tanto
puede que haya dos dobles de prueba en el mismo test. Típicamente uno es para entrada indirecta
(stub) y el otro para salida indirecta (spy o mock). Si hay más de dos dependencias, sinceramente me
plantearía que quizás el diseño del código es mejorable. Al igual que es poco aconsejable que una
función tenga más de dos argumentos, también es poco aconsejable que una clase tenga más de dos
dependencias. Cuando se trabaja con código legado con múltiples dependencias, una estrategia para
reducirlas es crear fachadas o envolturas que agrupen estas dependencias y las oculten de la interfaz
pública que conecta con el objeto que queremos probar. Un ejemplo típico de test que combina stub
con spy, podría ser algo como esto:
³⁶https://python-doublex.readthedocs.io/en/latest/
Mock Objects 87
Y el código del servicio que estamos probando, para que el test estuviese en verde sería así:
Es importante señalar que no estamos verificando explícitamente que se realiza una llamada al
repositorio, sino que queda probado indirectamente mediante la verificación final de la salida
indirecta, cuando comprobamos que el servicio de backup recibe los ficheros que debería. En el libro
GOOS, los autores recomiendan utilizar stubs para simular consultas y mocks para las acciones. Justo
lo que estamos haciendo en este ejemplo (salvo que en realidad es un spy y no un mock estricto,
pero eso es menos relevante). Cuantas menos verificaciones explícitas hagamos sobre llamadas
producidas, mejor, porque estaremos reduciendo el acoplamiento entre el test y la implementación.
Como cada regla tienes sus excepciones, en ocasiones no queda más remedio que ser explícitos con
la comprobación de llamadas. Si existen dos colaboradores a los que se realiza llamadas y necesitan
Mock Objects 88
verificarse ambas, es aconsejable escribir dos test separados, uno para cada llamada. Aunque el
escenario sea el mismo, queda más claro escribir dos test cada uno con su verificación. Veamos
un ejemplo de un componente JavaScript que realiza un envío de datos al servidor mediante su
dependencia cliente, para después hacer una redirección de la página si la respuesta del servidor fue
satisfactoria:
38 ...
39 */
40 });
1 function onFormSubmission(){
2 client.createArchive(archive)
3 .then((response) => {
4 redirector.navigateTo(pages.dashboard);
5 })
6 }
Ventajas e Inconvenientes
Los tipos de doble o de mock más comunes en test unitarios son los que ya hemos visto. Tienen la
ventaja de que permiten aislar el código que queremos probar sin llegar a ejecutar sus dependencias.
Desde este punto de vista estaríamos dando menos motivos de fallo al test. Se romperá sólo si el
código que se está probando tiene un problema, no si las dependencias tienen algún problema.
Estamos acotando el ámbito de ejecución. Ganamos en velocidad y cuando el test falla tardamos
menos en encontrar el problema. Siempre que el diseño del código sea sencillo y el del test. Siempre
que haya un único mock por test porque si nos vamos a los extremos, a test con varios mocks,
entonces se hace un problema entender y mantener el test. Como siempre, depende del uso que le
demos a la herramienta. Otra ventaja es que podemos programar código que depende de artefactos
que todavía no han sido implementados. Por ejemplo, si el repositorio está sin implementar y es
Mock Objects 90
un trabajo que incluso va a realizar otra persona, podemos definir su interfaz y usar mocks para
ir avanzando en la implementación del servicio con TDD. Esta técnica es muy útil para simular
la comunicación con un API REST por ejemplo, cuando todavía no está disponible. Podemos ir
programando el código del cliente sin que el servidor esté hecho todavía.
El inconveniente que tienen estos dobles es que si en los test simulamos un comportamiento que no
se corresponde con el real, no vamos a conseguir reproducir las condiciones reales que luego van
a darse en el entorno de producción. Es decir estamos asumiendo que esas dependencias (también
llamadas colaboradores) tienen un comportamiento determinado y si luego tienen otro, el código
fallará de forma que no anticipamos. Corremos el riesgo de estar construyendo castillos en el aire. El
otro gran inconveniente es que los test quedan más acoplados a la implementación y pueden llegar
a impedir cambios en el diseño del software. Generalmente los test con mocks son más difíciles de
entender.
Personalmente intento restringir el uso de mocks a aquellos objetos de hacen de frontera de la capa
de negocio del sistema. Es decir para simular la interfaz de usuario, o simular API REST, o accesos
a base de datos. Cuando las herramientas me lo ponen fácil incluso prefiero test de integración que
se comunican con bases de datos reales y con interfaz de usuario real. Siempre y cuando no me vea
depurando test repetidas veces, me da más confianza y más flexibilidad que introduciendo mocks.
Además evito hacer mocks de artefactos que pertenecen a terceros, siguiendo el consejo de Steve
Freeman y Nat Pryce. Es decir, si necesito hacer un mock del API REST del servidor y estoy
escribiendo código cliente para el navegador con JavaScript, no hago un mock directo de la función
de comunicación nativa (fetch) sino que la envuelvo en un objeto cuya interfaz puedo controlar.
Este objeto es el que inyecto donde corresponde y el que mockeo en los test. Si mañana hay cambios
(que yo no puedo controlar) en ese código de terceros, mi envoltura protegerá de ellos al resto del
sistema.
Esta idea de que la capa de dominio es el corazón del sistema y se interconecta con el mundo exterior
mediante Puertos y Adaptadores, es del autor y programador Alistair Cockburn y se llama también
Arquitectura Hexagonal³⁷.
Otros tipos
Existen otras simulaciones posibles como por ejemplo un repositorio en memoria, una base de datos
en memoria, un servidor SMTP que no envía emails de verdad, un servidor web ligero para APIs
REST sin lógica real detrás… Este tipo de dobles se conocen como fakes y tienen la funcionalidad
parcial o total del artefacto real, pero abarata las pruebas porque simplifica la forma en que se hace
la validación o bien se ejecutan más rápido que si la pieza fuese la real. Los uso para conseguir que el
sistema sea lo más real posible pero que aún así pueda tener un buen control sobre las entradas y las
salidas indirectas del sistema. Normalmente un fake no es apto para sistemas de producción porque
su tecnología es limitada pero desde el punto de vista de los test, son funcionalmente completos. Los
test ni tienen por qué enterarse de que está ejecutándose un fake, lo cual los hace interesantes para
simplificar los test. La parte programática del test suele quedar más simple a cambio de añadir más
complejidad en la parte de configuración del ejecutor de los test (runner). JUnit soporta la inyección
de runners de terceros mediante la anotación @RunWith, de la cual se aprovechan frameworks como
SpringBoot para inyectar backends web de tipo fake. Gracias a esta evolución en las herramientas
de test y a la creciente potencia de cálculo de las máquinas, es cada vez más rentable usar fakes en
lugar de otros tipos de dobles.
³⁷https://alistair.cockburn.us/hexagonal-architecture/
Mock Objects 92
Código legado
Llegará ese día en que empiece a escribir test para ese código legado con clases de 20000 líneas de
código con funciones de cientos de líneas de código, que hasta ahora no tenían ni un solo test. Aquí
es más que probable que quiera reemplazar alguna de esas funciones con mocks, pero que sólo haya
una clase. Michael Feathers explica varias de las técnicas para hacerlo en su libro. Una de ellas es la
siguiente:
No cuesta mucho poner ejemplos de código feo y real como la vida misma. Nuestro objetivo aquí
es añadir test para el primer método de la clase, sin que se ejecute el segundo, porque no podemos
recrear las condiciones de producción en la base de datos o por cualquier otro motivo. Todo lo que
queremos hacer es comprobar que se llama al segundo método con los parámetros adecuados. Una
solución es crear una clase que hereda de la que queremos probar y reemplazar el método mediante
la herencia:
Ahora en los test, ya podemos instanciar esta clase que hemos creado, ejecutar el primer método y
después comprobar que en la variable de instancia savedData está el contenido que debería estar. Es
una forma de programar un espía manualmente.
En realidad lo más común es que el acceso a base de datos esté dentro del propio método que
queremos probar:
Como paso previo para poder usar mocks, habrá que extraer el bloque que accede a datos a un
método. Lo aconsejable es que el método sea protegido para no seguir ensuciando la interfaz pública
de la clase. Así podremos suplantarlo al heredar, pero los consumidores seguirán sin poder acceder
al método (al menos no por accidente).
17 customer.location + "," +
18 customer.signUpYear + ")");
19 conn.close();
20 }
21 }
Los frameworks de mocks son muy útiles para empezar a meterle mano al código legado. El código
sucio puede limpiarse. No se ensució en un sólo día sino que se hace difícil de mantener por la falta
de refactor con el paso del tiempo. Es el inadvertido empobrecimiento paulatino que sufre al añadir
más código sin test y añadir complejidad accidental.
Si llevase tiempo pidiendo a su jefe o a su cliente que se deshagan de todo el código existente para
escribirlo de nuevo completamente y un día le concedieran el deseo, ¿cómo garantizaría que dentro
de un año no se vería en la misma situación? ¿qué cambiaría en la forma de programar para evitar
llegar al mismo lugar?
Estilos y Errores
Outside-in TDD
Esta técnica aborda el diseño del sistema desde el exterior, considerando que en el interior está la
implementación de las reglas de negocio. El libro GOOS³⁸ fue el primer ejemplo real completo que
estudié de una aplicación desarrollada con TDD empezando por las capas externas y avanzando
progresivamente hacia el corazón. Cuando empiezo el desarrollo de una funcionalidad (una historia
de usuario por ejemplo), escribo un test de extremo a extremo que mira al sistema como una caja
negra. Estimulando al sistema desde la interfaz de usuario y validando la respuesta también en la
propia interfaz de usuario o bien en el otro extremo del sistema, que con frecuencia es la base de
datos. Aunque nada del sistema existe todavía. Si se trata de una aplicación web, uso herramientas
como WebDriver³⁹ para manipular el navegador programáticamente y una base de datos de pruebas
con la misma estructura que la real de producción. En la medida de lo posible intento que el entorno
sea una réplica del de producción para que el test sea lo más real posible. Aunque la granularidad
del test es la más grande, sigo buscando feedback rápido y todos los demás beneficios de los test
mantenibles, sobre todo intento que el test sea corto, claro y certero. Este trabajo de pensar en test
de extremo a extremo con el menor número de líneas posibles me ayuda a refinar el análisis del
sistema. A menudo sirve para que surjan nuevas dudas sobre el negocio y podamos resolverlas
antes de empezar a programar, lo cual es más barato que tener que cambiar el código a posteriori.
Para escribir test cortos y resilientes que atacan al sistema mediante la interfaz de usuario existen
patrones de abstracción de interfaz gráfica como el Page Object⁴⁰ y otras variantes. El propio test (su
configuración/preparación) es responsable de levantar el servidor y cualquier otra pieza necesaria.
También es responsable de dejarlo todo como estaba para que sea repetible. Estos test son altamente
dependientes del framework usado. Los frameworks web modernos están pensados para ser testados
de extremo a extremo integrándose con frameworks tipo xUnit o tipo RSpec. Con unas pocas líneas
de código es posible levantar un servidor, un frontend, una base de datos de pruebas, etc. Las
herramientas de virtualización como docker también facilitan cada vez más la tarea de recrear una
instancia del sistema para test.
En el ejemplo del CSV del primer capítulo el primer test podría:
1 @RunWith(SpringRunner::class)
2 @SpringBootTest(
3 webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
4 properties = ["server.port=8080"])
5 class CsvFilterAppShould {
6 @Value("\${chrome.path}")
7 var chromePath : String = "not-configured"
8 lateinit var driver: WebDriver
9 val filepath = System.getProperty("java.io.tmpdir") +
10 File.separator +
11 "invoices.csv"
12 lateinit var csvFile: File
13
14 @Before
15 fun setUp(){
16 driver = WebDriverProvider.getChromeDriver(chromePath)
17 csvFile = File(filepath)
18 }
19
20 @After
21 fun tearDown() {
22 csvFile.delete()
23 driver.close()
24 }
25
26 @Test
27 fun display_lines_after_filtering_csv_file() {
28 val lines = listOf(
29 "Num_factura, Fecha, Bruto, Neto, IVA, IGIC, Concepto, CIF_cliente, NIF_\
30 cliente",
31 "1,02/05/2019,1000,810,19,,ACER Laptop,B76430134,",
32 "2,03/12/2019,1000,2000,19,8,Lenovo Laptop,,78544372A")
33 createCsv(lines);
34 login()
35 selectFile()
36
37 submitForm()
38
39 assertThat(driver.pageSource).contains(lines[0])
40 assertThat(driver.pageSource).contains(lines[1])
41 assertThat(driver.pageSource).doesNotContain(lines[2])
42 }
43
Estilos y Errores 97
Este tipo de test requieren muchos detalles a tener en cuenta como por ejemplo que puedan correr
en distintas plataformas. Por eso a la hora de elegir la ruta del fichero he procurado que sea una ruta
absoluta que funcione en Linux, Windows, MacOS. La ruta debe ser absoluta para que el test pueda
escribir en el disco y también Webdriver pueda leer del disco.
Existen varias alternativas a este test de extremo a extremo. Por ejemplo si consideramos que
aporta poco valor enviar el fichero CSV desde la interfaz de usuario (mucha fragilidad frente a poca
seguridad añadida), podría atacar directamente al servidor haciendo un envío tipo POST con una
librería cliente HTTP desde el test, sin necesidad de WebDriver. De hecho SpringBoot por defecto
Estilos y Errores 98
provee esta opción y también por defecto usa una instancia más ligera del backend que no ocupa
ningún puerto de red real en la máquina y que arranca más rápido, esto es usando MockMvc:
1 @RunWith(SpringRunner::class)
2 @SpringBootTest
3 @AutoConfigureMockMvc()
4 @WithMockUser("spring")
5 class CsvFilterAppShould {
6 @Autowired
7 lateinit var mvc: MockMvc
8 val filepath = System.getProperty("java.io.tmpdir") +
9 File.separator +
10 "invoices.csv"
11 lateinit var csvFile: File
12
13 @Before
14 fun setUp() {
15 csvFile = File(filepath)
16 }
17
18 @After
19 fun tearDown() {
20 csvFile.delete()
21 }
22
23 @Test
24 fun display_lines_after_filtering_csv_file() {
25 val lines = listOf(
26 "Num_factura, Fecha, Bruto, Neto, IVA, IGIC, Concepto, CIF_cliente, NIF_\
27 cliente",
28 "1,02/05/2019,1000,810,19,,ACER Laptop,B76430134,",
29 "2,03/12/2019,1000,2000,19,8,Lenovo Laptop,,78544372A")
30 createCsv(lines);
31
32 val pageSource = mvc.perform(
33 MockMvcRequestBuilders.multipart(
34 Configuration.webUrl + "/postcsv")
35 .file(MockMultipartFile(
36 "file", filepath,
37 "text/plain",
38 csvFile.inputStream()))
39 ).andExpect(MockMvcResultMatchers.status().isOk)
40 .andReturn().response.contentAsString
Estilos y Errores 99
41 assertThat(pageSource).contains(lines[0])
42 assertThat(pageSource).contains(lines[1])
43 assertThat(pageSource).doesNotContain(lines[2])
44 }
45
46 private fun createCsv(lines: List<String>) {
47 csvFile.printWriter().use { out ->
48 lines.forEach {
49 out.println(it)
50 }
51 }
52 }
53 }
La ventaja de este test es que es más rápido y ligero, incluso evita tener que hacer login en la
aplicación porque inyecta un supuesto usuario autenticado. Ataca a un sólo método del controlador
a diferencia del anterior. La desventaja es que si el formulario de subir el fichero es incorrecto,
la aplicación en realidad está rota y no nos enteramos. Hay que sopesar bien las ventajas y los
inconvenientes para elegir la mejor estrategia.
Volviendo a Outside-in TDD, ahora que que el test está escrito y falla, lo siguiente es añadir otro
test de un ámbito más reducido que me permita practicar TDD con un componente más pequeño.
A diferencia de los ejemplos vistos anteriormente, aquí no se trata de hacer pasar el test con el
mínimo esfuerzo. Primero porque el paso al verde será demasiado grande, no podremos dar pasos
cortos. Y si acaso lo conseguimos, entonces seguramente el código es demasiado irreal como para
ayudar a la generalización con los test sucesivos. Estamos todavía demasiado lejos de las partes
del sistema que más se prestan a ser implementadas con TDD. Hay multitud de opciones para
el siguiente objetivo. Podríamos bajar a una capa del sistema bastante interna como hicimos en
el primer capítulo, lo cual sería combinar con el estilo Inside-out, el otro enfoque que veremos
a continuación. También podríamos trabajar en la capa del controlador web, es decir la primera
capa del servidor (asumiendo que estamos trabajando con algún framework MVC en backend como
el del ejemplo de arriba). Dentro de esta opción podríamos utilizar un mock de tipo Stub para el
componente que filtra el CSV, el del primer capítulo, para centrarnos en diseñar el controlador.
Podríamos hacer TDD con los diferentes casos que debe gestionar el controlador, desde un subida
de fichero correcto y una respuesta también correcta, hasta los casos en que el fichero no ha sido
adjuntado o los datos no pueden leerse, o quizás el formato del fichero no es CSV… Haríamos TDD
del controlador practicando rojo-verde-refactor, mientras que el primer test de extremo a extremo
que teníamos escrito sigue estando en rojo. Siguiendo el ejemplo anterior, los test para triangular
el controlador se apoyarían en MockMvc. Típicamente los frameworks permiten la inyección de
dependencias en el controlador, así que también podría utilizar un mock construído por Mockito en
el test:
Estilos y Errores 100
1 @SpringBootTest
2 @RunWith(SpringRunner::class)
3 @AutoConfigureMockMvc()
4 @WithMockUser("spring")
5 class CsvFilterAppShould_ {
6 @MockBean
7 lateinit var stubCsvFilter: CsvFilter
8
9 /* ... the same lines than before...*/
10
11 @Test
12 fun filters_csv_file() {
13 val lines = listOf(theSameList)
14 createCsv(lines);
15 // this is the new line:
16 given(stubCsvFilter.apply(lines))
17 .willReturn(listOf(lines[0], lines[1]))
18 /* ... same lines here ... */
19 }
20 }
SpringBoot se apoya en Mockito para la generación de dobles. En este caso, given invoca al when
the Mockito. Y con la anotación @MockBean indicamos al framework que esa dependencia va a ser
un doble. Aunque estoy utilizando esta tecnología para los ejemplos, la mayoría de los frameworks
web modernos incluyen soporte para este tipo de pruebas. Aquí una implementación del controlador
para convertir el test en verde:
1 @Controller
2 class CsvFilterController {
3 @GetMapping("/csvform")
4 fun form(): String {
5 return Views.CsvForm
6 }
7
8 @PostMapping("/postcsv")
9 fun filteredCsv(
10 @RequestParam("file") file: MultipartFile,
11 redirectAttributes: RedirectAttributes,
12 viewModel: Model): String {
13 val inputLines = file.inputStream
14 .reader(Charsets.UTF_8)
15 .readLines()
16 val lines = CsvFilter().apply(inputLines)
Estilos y Errores 101
17 viewModel.addAttribute("lines", lines)
18 return Views.CsvResult
19 }
20 }
21
22 @Service
23 class CsvFilter {
24 fun apply(lines: List<String>): List<String>{
25 return lines
26 }
27 }
Una vez tenemos terminado el controlador, podríamos hacer TDD con la función que filtra el CSV.
Al terminar, nuestro test de extremo a extremo, el primero que habíamos escrito, debería ponerse
en verde automáticamente si todo está bien.
En el enfoque Outside-in es bastante típico el uso de mocks porque se va penetrando en el sistema
trabajando en artefactos que deben comunicarse con la siguiente capa y esta todavía ni existe. Estos
test con mocks no necesariamente tienen que quedarse así después. Cuando implementamos la
siguiente capa y tenemos la opción de inyectar el componente real, es perfectamente válido y a
veces deseable refactorizar los test para reemplazar algunos mocks por sus alternativas reales. Los
mocks en estos casos sirven como andamio para poder estudiar el diseño del sistema y progresar
en su implementación. Incluso algunos test se pueden borrar cuando ya está todo implementado si
la redundancia no compensa el mantenimiento, teniendo en cuenta que hay un test de extremo a
extremo que cubre a nivel de seguridad.
Nuestro primer test de extremo a extremo no pretendía validar reglas de negocio del filtrado de
CSV. Por lo tanto sólo hemos necesitado uno de este tipo. La combinatoria de casos que implementa
la lógica de negocio, queda cubierta con test de ámbito más reducido, más cercanos al artefacto en
cuestión. De ahí lo de la pirámide de los test. Tenemos pocos de granularidad gruesa en comparación
con los de grano fino.
Se dice que el estilo Outside-in es de la escuela de Londres porque fue allí donde se popularizó.
Combina muy bien con BDD (también promovido por la comunidad de práctica londinense), porque
nos invita a practicar TDD a nivel global del sistema.
Inside-out TDD
Una de las principales críticas al estilo Outside-in es que podría hacernos incurrir en un diseño más
complejo de lo estrictamente necesario. Puesto que supone de antemano que el sistema va a dividirse
en diferentes capas desde fuera hacia adentro, podría ser que añadamos más artefactos de los
necesarios. El enfoque Inside-out propone empezar el desarrollo por la capa de negocio y poco a poco
agregarle más funcionalidad conforme nos acercamos a los límites del sistema, utilizando refactoring
Estilos y Errores 102
para partir el código en diferentes artefactos cuando estos adquieren suficiente responsabilidad. La
idea es que tanto el tamaño de los artefactos como la cantidad, sea la mínima necesaria para que el
sistema funcione en producción.
En un enfoque Inside-out clásico podríamos realizar el proceso de desarrollo sin mocks, porque
cuando se advierte que la pieza A necesita delegar en la pieza B, primero se implementa B. Cuando
se inventó TDD no existía el concepto de objeto mock, por lo que se dice que este es el enfoque
clásico de TDD.
El primer capítulo de este libro arrancó con este enfoque. Es el más adecuado para explicar TDD a
las personas que empiezan a estudiar la técnica porque no requiere conocer conceptos avanzados
como los mocks. Es también el enfoque que recomiendo a las programadoras que quieren empezar
a introducir TDD en su día a día, porque encaja bien en proyectos legados que requieren nuevas
funcionalidades. Seguramente esos proyectos no tienen una arquitectura testable y no será posible
añadir test sin hacer un buen puñado de cambios antes, pero sí que podremos desarrollar nuevos
métodos/funciones con TDD cuando no dependan de otras funciones existentes.
En la práctica recurrimos a esta técnica sobre todo por una cuestión de cadencia de desarrollo. Y
es que cuando nos atascamos con alguna capa más externa, ya sea por dudas en los requisitos no
funcionales, o por dudas sobre la tecnología o la estrategia, podemos volver a las reglas de negocio
y seguir avanzando en su implementación. Lo más habitual es combinar las dos técnicas, trabajar
desde afuera y también desde adentro.
Este enfoque se dice que es de la escuela de Chicago y parece ser el preferido por programadores
como Robert C. Martin.
Combinación
Es muy poco probable que podamos diseñar un sistema entero desde afuera utilizando mocks para
diseñar la colaboración con las capas internas, porque habrá ocasiones en que nos queden dudas
sobre cómo deberían comportarse esas capas internas. En lugar de jugar a la lotería y configurar
mocks con comportamientos poco probables, es preferible bajar al artefacto donde pensamos que
debería estar implementado cierto comportamiento y trabajar en él para comprender mejor su
responsabilidad y a su vez aprender cómo van a encajar las piezas. Hay veces que no podemos
ni estar seguros de la interfaz de un artefacto interno (sus métodos públicos por ejemplo) hasta que
no nos ponemos a programarlo. Outside-in es la punta de lanza mientras que Inside-out es la lupa
que me permite investigar los pequeños detalles. Es muy típico desarrollar una historia de usuario
alternando los dos estilos, trabajando en las dos direcciones hasta que los caminos se encuentran.
Aunque hay developers que tienden a usar más un estilo que el otro, la mayor parte de la comunidad
está de acuerdo en que es necesario combinar ambos estilos para un desarrollo eficaz. Esto es algo
que se ha discutido bastante en congresos internacionales y está claro que no hay un estilo ganador.
Para aquellos que nos sentimos cómodos dibujando pequeños diagramas de módulos/clases en la
pizarra (o en la cabeza) para analizar el diseño antes de empezar el desarrollo, Outside-in encaja
Estilos y Errores 103
como un guante. Cuando surge la duda sobre si el diseño será excesivo en cuanto a su complejidad,
Inside-out resuelve yendo directo al grano.
¿Qué sucedería si en el ejemplo de CSVFilter ahora hubiera un nuevo requisito que nos pide generar
un fichero con aquellas líneas que han sido descartadas y una pequeña explicación de por qué se
descartó cada una? ¿Podemos implementar esta funcionalidad desde fuera con un test que ataca al
controlador web? Ciertamente es posible pero para mí lo más natural sería buscar en el código la
función que realiza el filtrado y estudiar de qué manera puedo encajar ahora el nuevo requisito. Por
una cuestión de cadencia me resultaría más productivo bajar a ese nivel del sistema y añadir los test
pertinentes a ese nivel.
Errores típicos
Algunas de las contraindicaciones más habituales se han ido repasando a lo largo del libro,
probablemente la más habitual y más infravalorada sea nombrar los test de cualquier manera. Pero
hay más. En mi primer libro sobre TDD, escrito una década antes que este, había un buen puñado. No
hay nada como sufrir los errores propios para aprender de la experiencia. A continuación enumero
los errores más típicos cometidos por los programadores que están empezando con TDD y con test
automáticos en general.
Los motivos de evitar este tipo de test es que están demasiado acoplados a la implementación del
código sin necesidad, que no ayudan a implementar ninguna funcionalidad y que incluso pueden
distraernos de definir el verdadero comportamiento antes de ponernos a programar. Es decir, pueden
tirar por la borda muchos de los beneficios de TDD. Pueden provocar excesos de complejidad y puede
ser que incurran en YAGNI (You ain’t gonna need it).
permitan crear una mínima red de seguridad de test antes de empezar a hacer cambios en el código.
Sin embargo no son unos test que ayuden en el medio y largo plazo porque su complejidad es muy
grande. Son test que entorpecen, que encarecen el mantenimiento, a menudo provocando falsas
alertas. Por eso mi recomendación es que sean test de usar y tirar para transitar de un diseño que
no es nada testable hacia otro que admita mejores test.
Un test que necesita simulaciones complejas, como por ejemplo un mock que devuelve otro
mock, nos está indicando que el diseño del código de producción podría ser accidentalmente
complejo o bien que el test tiene un enfoque inadecuado. Probablemente está mezclando varios
comportamientos en un mismo test. Es una pista que nos da la oportunidad de mejorar el diseño del
código de producción o el diseño de la prueba.
El exceso de mocks dificulta significativamente la lectura de los test y provoca fricción a la hora
de intentar practicar refactoring del código de producción, ya que hay un fuerte acoplamiento entre
ambas partes. Cuando en un mismo test puedo elegir entre inyectar la dependencia real del artefacto
bajo prueba y un mock de dicha dependencia, suelo inyectar la versión real. Por supuesto haciendo
balance de los beneficios y los inconvenientes en cada caso, ya que si por ejemplo la dependencia
real va a ralentizar la ejecución del test en dos segundos, voy a preferir un mock.
Test parametrizados
Los frameworks de test tipo xUnit soportan la posibilidad de crear métodos y clases de test
parametrizados:
1 @ParameterizedTest
2 @ValueSource(ints = {1, 3, 5, 7, 11})
3 void recognizes_prime_numbers(int number) {
4 assertTrue(isPrime(number));
5 }
En este ejemplo el test será ejecutado cinco veces, uno para cada valor de los introducidos en
la anotación @ValueSource. En la práctica de TDD, pocas veces me he visto en la necesidad de
parametrizar mis test porque la triangulación se puede hacer con dos o tres casos, no mucho más. La
parametrización tiene sentido cuando estamos tratando con resultados tabulados, como por ejemplo
la traducción de números decimales a números romanos. Mi amigo Jose Juan Hernández lo utiliza
como ejemplo en sus clases en la escuela de informática de la ULPGC. También puede resultar útil
para probar código escrito a posteriori y sobre todo código que uno no conoce y explora de forma
Estilos y Errores 107
tabulada. Así que podría ser de utilidad para explorar código desconocido. Se trata de un artefacto
que puede introducir más complejidad en los test y reducir la legibilidad ya que estamos quitando
los nombres de los test y usando valores que pueden resultar mágicos en su lugar.
Cuando pienso en parametrizar los test lo que me planteo es si no sería más conveniente usar una
herramienta que me permita escribir test basados en propiedades como vimos en el segundo capítulo
y que sea la herramienta quien genere los valores.
Ausencia de documentación
Se tiende a confundir metodologías ágiles con ausencia de documentación, lo cual no es cierto.
Por más que los test sirvan como documentación, siempre necesitaremos otros documentos que
Estilos y Errores 109
nos expliquen cómo construir el software, como instalarlo, como ejecutar los test, cómo resolver
problemas frecuentes…
Es importante dibujar diagramas de arquitectura que expliquen cómo está diseñado el sistema y
que vayan acompañados de documentos que expliquen el por qué de las decisiones que se han
tomado. Explicar también las opciones que se descartaron y las conclusiones aprendidas de un
análisis o una experiencia ocurrida en producción. La cantidad de tiempo que le lleva a una nueva
desarrolladora, que se incorpora al equipo, empezar a realizar cambios en el código, dice mucho de
la calidad del proyecto. Si necesita varios días y que se sienten con ella varias personas veteranas
en el proyecto a solucionarle problemas de instalación, está claro que falta mucha documentación y
mucha automatización.
Implantación de TDD
Durante la pasada década, parte de mi trabajo consistió en dar a conocer las prácticas de TDD y de
automatización de pruebas a individuos y organizaciones de diverso tipo. Algunos de estos equipos
consiguieron incluso llegar a adoptar XP como método de trabajo aunque fue una minoría. En este
capítulo repasamos cuáles fueron los ingredientes que contribuyeron a que esta forma de trabajar
fuese adoptada en unos casos, así como los motivos por los que no tuvo aceptación en otros.
Para personas y equipos que no escriben test automáticos como parte de su trabajo, TDD se percibe
típicamente como una técnica disruptiva, casi utópica. Incluso como una amenaza para los plazos de
entrega de los proyectos. Este es el grupo que tiene un camino más largo que recorrer pero que sin
duda puede hacerlo. He vivido varias transformaciones con diferentes equipos y puedo confirmar
que es posible. Lo que requiere es tiempo y voluntad. En mi experiencia, el tiempo transcurrido para
que equipos que trabajaban sin ningún tipo de metodología ni proceso definido trabajasen con XP,
fue de al menos dos años. El problema no son las metodologías “waterfall” sino la ausencia total
de metodología y la falta de cultura de mejora y de aprendizaje. Allá donde se implantó, llegó un
momento en que ya no querían volver atrás. Ya no se planteaban escribir código sin test salvo en casos
muy excepcionales que sabían identificar perfectamente y que abordaban de manera estratégica,
porque habían adquirido suficiente criterio para llevar una gestión meticulosa de su deuda técnica.
deberá haber iniciado su propio cambio antes de pedírselo a los demás. Esto puede suponer que tenga
que invertir tiempo y esfuerzo más allá del horario laboral para adquirir un nivel de competencia
que le permita ganar credibilidad en su organización. La credibilidad se consigue dando ejemplo,
con resultados. No son muchas las personas que están dispuestas a reinventarse para cambiar
sus organizaciones. Lo más habitual es encontrar personas que se quejan sistemáticamente de sus
empresas por no introducir cambios pero que no hacen nada efectivo para contribuir.
Las organizaciones donde XP caló y se quedó, estaban formadas por personas con la voluntad de
entender a los demás, de cooperar y de esforzarse para cambiar. Que antes de pedir a los demás ya
estaban dando algo de su parte. Donde existía una cultura basada en la confianza. La empresa invertía
en recursos de formación, en contratar apoyo externo cuando hacía falta y ayudaba a los empleados
a poder organizarse y conciliar trabajo con formación. Y los empleados daban su mejor versión en
cada jornada laboral y además asistían a eventos de la comunidad (congresos/conferencias, charlas
y talleres), a veces en su tiempo libre. Leían libros técnicos y veían conferencias por Internet en casa.
La transformación funcionó porque todos pusieron de su parte, trabajando como un equipo.
Lo primero es probar
Leyendo un libro no podremos saber cuándo nos conviene usar TDD y cuándo no. Será cuando
practiquemos que podamos forjar un criterio propio propio. Podemos leer que “hay que escribir el
test primero”, y “ejecutarlo para verlo fallar”, pero hasta que no lo hagamos y descubramos que el
test que hemos escrito y que esperábamos que estuviese rojo está verde, porque tenemos un fallo en
el test, no entenderemos de verdad lo importante que es seguir el método.
Es importante que al principio realicemos los ejercicios siguiendo el método de manera muy rigurosa
durante un tiempo, hasta que seamos capaces de saber en qué momento podemos adaptar las reglas
a nuestro propio estilo.
La forma más rápida y divertida de experimentar TDD es una combinación de ejercicios cortos de
programación (code katas) y de pequeños proyectos de juguete. Una kata es, por ejemplo, encontrar
la descomposición en factores primos como vimos en un capítulo anterior. Nos permite ir directos
al método y practicar técnicas concretas según el problema, desde algoritmia hasta arquitectura de
software. Pero las katas no son suficientes para avanzar en la técnica porque les falta la fontanería
que requieren las aplicaciones reales. El entrenamiento se completa cuando hacemos alguna pequeña
aplicación que podemos usar nosotros mismos en el día a día o bien alguien de nuestro entorno
porque, al tener que mantener nuestro propio código, será cuando mejor entendamos el valor de
disponer de un código modular y bien testado.
No existe una recomendación sobre cuánto tiempo dedicar a estos ejercicios sino que depende del
contexto de cada persona. Ciertamente es mejor dedicar dos horas al mes a practicar con una kata
que no hacer nada en todo el año porque estamos esperando al momento ideal donde tengamos un
montón de tiempo libre para sentarnos a aprender. Cualquier práctica será mejor que no practicar.
Para quienes encuentran más resistencia al practicar en solitario, existen eventos de comunidad
llamados “coding dojo”, donde un grupo de personas se reúne para practicar una kata. Bien en
Implantación de TDD 112
parejas o bien estilo randori (practicando mob programming). Existen plataformas online como por
ejemplo meetup.com donde es posible encontrar estos grupos, a veces bajo el nombre de software
“craftsmanship”, “crafters” o simplemente “agile”. Puede que en su ciudad o pueblo haya algún
grupo al que se pueda unir. Hace poco en un dojo que facilité grabé la introducción y la subí a
Youtube⁴¹, explicando en qué consiste y qué se necesita para organizarlo.
Una vez adquirida cierta soltura con katas y proyectos de juguete, podemos empezar a introducirlo
progresivamente en nuestros proyectos reales del trabajo.
lenta y dolorosa de aprender algo que ya está más que dominado por otras personas. Es más barato
y más rápido pedir ayuda a gente que de verdad tenga habilidad en la técnica y que sepa transmitir
los conocimientos. En aquellos problemas que sean muy particulares del negocio de la empresa, será
muy difícil o imposible contratar ayuda externa cualificada pero, para técnicas tan extendidas y
antiguas como TDD, sí.
En todos los equipos a los que ayudé en el proceso de adopción de XP, se produjeron contrataciones
que aceleraron el cambio. Como consultor externo ayudé en el proceso de selección. Típicamente
las personas venían interesadas por la cultura de la empresa, la veían como un lugar en el que poder
seguir creciendo profesionalmente. Ambas partes ganaron.
La ayuda externa debe durar lo suficiente como para que las personas que se quedan puedan tomar el
relevo del liderazgo. En la mayoría de organizaciones que contrataron sólo una formación intensiva
de dos o tres días de TDD, la práctica no caló en el equipo. Al cabo de unos meses no quedaba
nada del entusiasmo post-curso. Es cierto que para unas pocas de esas personas que participaron en
los cursos, se abrió una nueva puerta y consiguieron sacarle partido a TDD en esa empresa o en la
siguiente, a nivel individual, pero a nivel organizacional el cambio requiere mucho más que dos días
intensivos. Requiere aplicarlo en proyectos reales durante meses. Un curso es una primera toma de
contacto que funciona muy bien si forma parte de un plan más grande.
Libros
En castellano
En inglés
Material en vídeo
En mi canal de Youtube tengo varias listas de reproducción con vídeos míos practicando así como
vídeos de clases grabadas en vivo sin edición.
• TDD y Refactoring⁵⁴
• Refactoring avanzado⁵⁵
• Clases grabadas⁵⁶
⁵⁴https://www.youtube.com/watch?v=D2gFmSUeA3w&list=PLiM1poinndeOGRx5BxAR7x1kqjzy-_pzd
⁵⁵https://www.youtube.com/watch?v=fNZf7jlVKVA&list=PLiM1poinndeOYDYU-jzKTJflpfGJxqqYA
⁵⁶https://www.youtube.com/watch?v=0WqAA6DOpJw&list=PLiM1poinndeMSR6ATToTWPWLmFqg50-Vb
⁵⁷https://codely.tv/
⁵⁸https://cleancoders.com/
⁵⁹https://keepcoding.io/es/
Recursos adicionales 117
Gracias por leer hasta aquí. Mucho ánimo con tus primeros tests. Nos vemos en algún coding dojo
;-)