Las leyes de TDD
Desde la introducción de la metodología TDD por Kent Beck se ha intentado definir un framework sencillo que proporcione una guía para aplicarla en la práctica.
Inicialmente, Kent Beck propuso dos reglas muy básicas:
- No escribir una línea de código sin antes tener un test automático que falle.
- Eliminar la duplicación.
Es decir, para poder escribir código de producción, primero debemos tener un test que no pase y que requiera que escribamos ese código, precisamente porque eso es lo necesario para que el test pase.
Una vez que lo hemos escrito y viendo que el test pasa, nuestro esfuerzo se centra en revisar el código escrito y eliminar en lo posible la duplicación. Esto es muy genérico, porque, por una parte, se refiere al refactoring y, por otra parte, al acoplamiento entre el test y el código de producción. Y al ser tan genérico resulta difícil bajarlo en acciones prácticas.
Además, estas reglas no nos dicen nada acerca de cuan grandes son los saltos de código implicados en cada ciclo. Beck sugiere en su libro que los pasos o baby steps pueden ser tan pequeños o tan grandes como nos resulten útiles. En general, recomienda usar pasos pequeños cuando tenemos inseguridad o poco conocimiento del algoritmo, mientras que permite pasos más grandes si por experiencia y conocimientos tenemos claro qué hacer a continuación.
Con el tiempo, y a partir de la metodología aprendida del propio Beck, Robert C. Martin estableció las “tres leyes”, que no solo definen el ciclo de acciones en TDD, sino que también proporcionan criterios sobre cómo de grandes deberían ser los pasos en cada ciclo:
- No se permite escribir ningún código de producción a menos que haga pasar un test unitario que falle
- No se permite escribir más de un test unitario que sea suficiente para fallar; y los errores de compilación son fallos.
- No se permite escribir más código de producción del que sea necesario para hacer pasar un test unitario que falle
Las tres leyes son lo que hace diferente TDD de simplemente escribir tests antes que el código.
Estas leyes imponen una serie de restricciones cuyo objetivo es forzarnos a seguir un determinado orden y ritmo de trabajo. Definen una serie de condiciones que, si se cumplen, generan un ciclo y guían nuestra toma de decisiones. Entender cómo funcionan, nos ayudará a aprovechar al máximo la capacidad de TDD para ayudarnos a generar código de calidad y que podamos mantener.
Estas leyes se tienen que cumplir todas a la vez porque funcionan juntas.
Las leyes en detalle
No se permite escribir ningún código de producción a menos que haga pasar un test unitario que falle
La primera ley nos dice que no podemos escribir código de producción si no hace pasar un test unitario existente que actualmente está fallando. Esto implica lo siguiente:
- Tiene que existir un test que describa un aspecto nuevo del comportamiento de la unidad que estamos desarrollando.
- Este test tiene que fallar porque en el código de producción no existe nada que lo haga pasar.
En resumen, la primera ley nos fuerza a escribir un test que defina el comportamiento que vamos a implementar en la unidad de software que estamos desarrollando antes de plantearnos cómo hacerlo.
Ahora bien, ¿cómo tiene que ser el test que escribamos?
No se permite escribir más de un test unitario que sea suficiente para fallar; y los errores de compilación son fallos.
La segunda ley nos dice que el test debe ser suficiente para fallar y que tenemos que considerar fallos los errores de compilación o su equivalente en lenguajes interpretados. Por ejemplo, entre estos errores estarían algunos tan obvios como que la clase o función no existe o no ha sido definida.
Debemos evitar la tentación de escribir un esqueleto de la clase o la función antes de escribir el primer test. Recuerda que estamos hablando de Test Driven Development. Por tanto, son los tests los que nos dicen qué código de producción escribir y cuándo y no al revés.
Que el test sea suficiente para fallar quiere decir que el test ha de ser muy pequeño en diversos sentidos y es algo que al principio resulta bastante difícil de definir. Con frecuencia se habla del test “más sencillo”, del caso más simple, pero no es exactamente así.
¿Qué condiciones tendría que reunir un test en TDD, particularmente el primero?
Pues básicamente forzarnos a escribir el mínimo código posible que se pueda ejecutar. Lo mínimo en OOP sería instanciar la clase que queremos desarrollar sin preocuparnos de más detalles, de momento. El test concreto variará un poco en función del lenguaje y framework de testing que estemos utilizando.
Veamos este ejemplo. Se trata de la kata Leap Year en la se busca crear una función para averiguar si un año dado es bisiesto (leap year) o no. Para el ejemplo, mi intención es crear un objeto Year, al que le pueda preguntar si es bisiesto enviándole el mensaje isLeap. He encontrado este ejercicio en varias recopilaciones de katas sin mención de autoría. Para este capítulo, los ejemplos están escritos en C#.
Las reglas son:
- Los años no divisibles por 4 no son bisiestos (como 1997).
- Los años divisibles por 4 son bisiestos (como 1996), excepto:
- Si son divisibles por 100 no son bisiestos (como 1900).
- Si son divisibles por 400 serán bisiestos (como 2000).
Nuestro objetivo sería poder usar objetos Year de esta forma:
1 var year = new Year(1996);
2
3 if (year.IsLeap()) {
4 // do something
5 }
La tentación habitual es tratar de empezar de la siguiente forma porque parece que es el ejemplo del caso más sencillo posible.
1 using System;
2 using System.Runtime.Remoting.Metadata.W3cXsd2001;
3 using NUnit.Framework;
4
5 namespace LeapYear
6 {
7 public class Tests
8 {
9 [Test]
10 public void ShouldNotBeLeapYear()
11 {
12 var year = new Year(1997);
13 Assert.False(year.IsLeap())
14 }
15 }
16 }
Sin embargo, no es el test más sencillo que pueda fallar por una única razón. En realidad puede fallar por cinco razones, al menos:
- La clase
Yearno existe todavía. - Tampoco aceptaría parámetros pasados por la constructora.
- No responde al mensaje
IsLeap - Podría no retornar nada.
- Podría devolver una respuesta incorrecta.
Es decir, podemos esperar que el test falle por estas cinco causas y solo la última es la que el test realmente describe. Tenemos que reducirlas a solo una.
En este caso, es muy fácil ver que hay una dependencia entre los diversos motivos de fallo, de tal manera que para que se pueda producir uno, tiene que haberse solucionado el anterior. Evidentemente, es necesario que exista una clase que poder instanciar. Por tanto, nuestro primer test debería ser mucho más modesto y esperar únicamente que la clase se pueda instanciar:
1 using System;
2 using System.Runtime.Remoting.Metadata.W3cXsd2001;
3 using NUnit.Framework;
4
5 namespace LeapYear
6 {
7 public class Tests
8 {
9 [Test]
10 public void CanInstantiate()
11 {
12 new Year();
13 }
14 }
15 }
Si lanzásemos este test veríamos que falla por razones obvias: no existe la clase que se pretende instanciar en ninguna parte. El test está fallando por un problema de compilación o equivalente. Por tanto, podría ser un test suficiente para fallar.
A lo largo del proceso veremos que este test es redundante y que podemos prescindir de él, pero no nos adelantemos. Todavía tenemos que conseguir que pase.
No se permite escribir más código de producción del que sea necesario para hacer pasar un test unitario que falle
La primera y la segunda leyes nos dicen que tenemos que escribir un test y cómo debería ser ese test. La tercera ley nos dice cómo tiene que ser el código de producción. Y la condición que nos pone es que haga pasar el test que hemos escrito.
Es muy importante entender que es el test el que nos dice qué código necesitamos implementar y, por tanto, aunque tengamos la certeza de que va a fallar porque ni siquiera tenemos un archivo con el código necesario para definir la clase, debemos ejecutar el test y esperar su mensaje de error.
Es decir: tenemos que ver que el test, efectivamente, falla.
Lo primero que nos dirá al tratar de ejecutarlo es que la clase no existe. En TDD eso no es un problema, sino una indicación de lo que debemos hacer: añadir un archivo con la definición de la clase. Seguramente con las herramientas del IDE podamos generar ese código de manera automática, y es aconsejable hacerlo así.
En nuestro ejemplo, el mensaje del test dice:
1 The type or namespace name 'Year' could not be found
2 (are you missing a using directive or an assembly reference?)
Y simplemente tendremos que crear la clase Year.
1 using System;
2 using System.Runtime.Remoting.Metadata.W3cXsd2001;
3 using NUnit.Framework;
4
5 namespace LeapYear
6 {
7 public class Tests
8 {
9 [Test]
10 public void CanInstantiate()
11 {
12 new Year();
13 }
14 }
15
16 public class Year
17 {
18 }
19 }
En este punto volvemos a ejecutar el test para comprobar si pasa de rojo a verde. En muchos lenguajes este código será suficiente. En algunos casos puedes necesitar algo más.
Si es así, y el test pasa, el primer ciclo está completo y podremos pasar al siguiente comportamiento, a no ser que consideremos que tenemos posibilidades de hacer un refactor del código existente. Por ejemplo, lo habitual aquí sería mover la clase Year a su propio archivo.
1 namespace LeapYear
2 {
3 public class Year
4 {
5 }
6 }
Si el test no ha pasado, nos fijaremos en el mensaje mostrado por el test fallido y actuaremos en consecuencia, añadiendo el código mínimo necesario para que, finalmente, pase y se ponga en verde.
El segundo test y las tres leyes
Cuando hemos logrado hacer pasar el primer test aplicando las tres leyes podríamos pensar que no hemos conseguido realmente nada. Ni siquiera hemos abordado los posibles parámetros que podría necesitar la clase para ser construida, ya sean datos o colaboradores en el caso de servicios o use cases. Incluso el IDE se estará quejando de que no estamos asignando el objeto instanciado a ninguna variable.
Sin embargo, es importante ceñirse a la metodología, sobre todo en estas primeras fases. Con la práctica y la ayuda de un buen IDE el primer ciclo nos habrá llevado apenas unos pocos segundos. En esos pocos segundos hemos escrito un código, ciertamente muy pequeño, pero totalmente respaldado por un test.
Nuestro objetivo sigue siendo que los tests nos dicten qué código tenemos que escribir para implementar cada nuevo comportamiento. Como nuestro primer test ya pasa, tendríamos que escribir el segundo.
Aplicando las tres leyes, lo que viene a continuación es:
- Escribir un nuevo test que defina un comportamiento
- Que ese test sea el mínimo posible para obligarnos a hacer un cambio en el código de producción
- Escribir el código de producción mínimo y suficiente que hace pasar el test
¿Cuál podría ser el próximo comportamiento que necesitamos definir? Si en el primer test nos hemos forzado a escribir el código mínimo necesario para instanciar la clase, el segundo test puede llevarnos por dos caminos:
- Forzarnos a escribir el código necesario para validar parámetros del constructor y, por tanto, poder instanciar un objeto con todo lo necesario.
- Forzarnos a introducir el método que ejecuta el comportamiento deseado.
Así, en nuestro ejemplo, podríamos simplemente asegurarnos de que Year es capaz de responder al mensaje IsLeap.
1 using System;
2 using System.Runtime.Remoting.Metadata.W3cXsd2001;
3 using NUnit.Framework;
4
5 namespace LeapYear
6 {
7 public class Tests
8 {
9 [Test]
10 public void CanInstantiate()
11 {
12 new Year();
13 }
14
15 [Test]
16 public void CanRespondToIsLeapMessage()
17 {
18 var year = new Year();
19 year.IsLeap();
20 }
21 }
22 }
El test arrojará este mensaje de error:
1 'Year' does not contain a definition for 'IsLeap' and no accessible ext\
2 ension...
Que nos indica que el siguiente paso debería ser introducir el método que responde a ese mensaje:
1 namespace LeapYear
2 {
3 public class Year
4 {
5 public void IsLeap()
6 {
7 }
8 }
9 }
El test pasa, indicando que ahora los objetos de tipo Year, pueden atender al mensaje IsLeap.
Habiendo llegado a este punto, nos podríamos preguntar: ¿qué pasa si no cumplimos las tres leyes de TDD?
Violaciones de las tres leyes y sus consecuencias
Obviando la broma fácil de que no acabaremos en la cárcel o con una multa por incumplir las leyes de TDD, lo cierto es que sí tendríamos que apechugar con algunas consecuencias.
Primera ley: escribir código de producción sin tener un test
La consecuencia más inmediata es que rompemos el ciclo red-green. El código que escribimos ya no está guiado ni cubierto por tests. De hecho, si queremos tener esa parte testeada, tendremos que hacer un test a posteriori (un test de QA).
Imagina que hacemos esto:
1 using System;
2
3 namespace LeapYear
4 {
5 public class Year
6 {
7 private readonly int _aYear;
8
9 public Year(int aYear)
10 {
11 _aYear = aYear;
12 }
13
14 public bool? IsLeap()
15 {
16 if (_aYear % 4 == 0)
17 {
18 return true;
19 }
20
21 return false;
22 }
23 }
24 }
Los tests existentes fallarán porque hay que pasar un parámetro a la constructora, además no tenemos ningún test que se haga cargo de verificar el comportamiento que hemos introducido. Tendríamos que añadir tests para cubrir la funcionalidad que hemos incorporado, pero ya no estamos dirigiendo el desarrollo.
Segunda ley: escribir más que un test que falle
Esto podemos interpretarlo de dos formas: escribir varios tests o escribir un test que supone un salto de comportamiento demasiado grande.
Escribir más de un test provocaría varios problemas. Para hacerlos pasar todos necesitaríamos implementar una gran cantidad de código y la guía que nos podrían proporcionar esos mismos tests se desdibuja tanto que es como no tenerla. No contaríamos con una indicación concreta que podamos resolver implementando nuevo código.
1 using System;
2 using System.Runtime.Remoting.Metadata.W3cXsd2001;
3 using NUnit.Framework;
4
5 namespace LeapYear
6 {
7 public class Tests
8 {
9 [Test]
10 public void CanInstantiate()
11 {
12 new Year();
13 }
14
15 [Test]
16 public void CanRespondToIsLeapMessage()
17 {
18 var year = new Year();
19 year.IsLeap();
20 }
21
22 [Test]
23 public void CommonYearsShouldNotBeLeap()
24 {
25 var year = new Year(1997);
26 Assert.False(year.IsLeap());
27 }
28
29 [Test]
30 public void YearDivisibleBy4ShouldBeLeap()
31 {
32 var year = new Year(1996);
33 Assert.True(year.IsLeap());
34 }
35 }
36 }
Aquí hemos añadido dos tests. Para hacerlos pasar tendríamos que definir dos comportamientos. Además, son tests demasiado grandes. Todavía no hemos establecido, por ejemplo, que se pasa un parámetro a la constructora, ni que la respuesta será del tipo bool. Estos tests mezclan diversas responsabilidades y tratan de probar demasiadas cosas a la vez. Tendríamos que escribir demasiado código de producción de una sola vez, con lo que conlleva de inseguridad y espacio para que se produzcan errores.
En lugar de eso, necesitamos hacer tests para incrementos de funcionalidad más pequeños. Podemos ver varias posibilidades:
Para introducir que la respuesta es bool podemos asumir que, por defecto, los años no son bisiestos, por lo que esperaremos una respuesta false:
1 using System;
2 using System.Runtime.Remoting.Metadata.W3cXsd2001;
3 using NUnit.Framework;
4
5 namespace LeapYear
6 {
7 public class Tests
8 {
9 [Test]
10 public void CanInstantiate()
11 {
12 new Year();
13 }
14
15 [Test]
16 public void CanRespondToIsLeapMessage()
17 {
18 var year = new Year();
19 year.IsLeap();
20 }
21
22 [Test]
23 public void ByDefaultYearsAreNotLeapYears()
24 {
25 var year = new Year();
26 Assert.False(year.IsLeap());
27 }
28 }
29 }
El error es:
1 Argument 1: cannot convert from 'void' to 'bool?'
Se puede resolver con:
1 using System;
2
3 namespace LeapYear
4 {
5 public class Year
6 {
7 public Year()
8 {
9
10 }
11
12 public bool IsLeap()
13 {
14 return false;
15 }
16 }
17 }
Sin embargo, tenemos otra forma de hacerlo. Puesto que el lenguaje tiene tipado fuerte, podemos usar el sistema de tipos como test. Así en lugar de crear un test nuevo:
1 using System;
2 using System.Runtime.Remoting.Metadata.W3cXsd2001;
3 using NUnit.Framework;
4
5 namespace LeapYear
6 {
7 public class Tests
8 {
9 [Test]
10 public void CanInstantiate()
11 {
12 new Year();
13 }
14
15 [Test]
16 public void CanRespondToIsLeapMessage()
17 {
18 var year = new Year();
19 year.IsLeap();
20 }
21 }
22 }
Cambiamos el tipo de retorno de IsLeap:
1 using System;
2
3 namespace LeapYear
4 {
5 public class Year
6 {
7 public Year()
8 {
9
10 }
11
12 public bool IsLeap()
13 {
14
15 }
16 }
17 }
Al ejecutar el test nos indicará que hay un problema, pues no devolvemos nada en la función:
1 'Year.IsLeap()': not all code paths return a value
Y finalmente, no tenemos más que añadir una respuesta por defecto, que será false:
1 using System;
2
3 namespace LeapYear
4 {
5 public class Year
6 {
7 public Year()
8 {
9
10 }
11
12 public bool IsLeap()
13 {
14 return false;
15 }
16 }
17 }
Para introducir el parámetro de construcción podríamos recurrir a un refactor. Pero para esto el lenguaje de programación nos puede condicionar, llevándonos a distintas soluciones.
La vía del refactor es sencilla. Tan solo tenemos que incorporar el parámetro, aunque de momento no lo usaremos. En C# y otros lenguajes podemos hacerlo por la vía de introducir un constructor alternativo, de este modo los tests seguirán pasando. En otros lenguajes, podríamos marcar el parámetro como opcional.
1 using System;
2
3 namespace LeapYear
4 {
5 public class Year
6 {
7 private readonly int _aYear;
8
9 public Year()
10 {
11
12 }
13
14 public Year(int aYear)
15 {
16 _aYear = aYear;
17 }
18
19 public bool IsLeap()
20 {
21 return false;
22 }
23 }
24 }
Como para nosotras no tiene sentido un constructor sin parámetros, ahora podríamos eliminarlo, pero antes tendríamos que refactorizar los tests, de modo que usemos la versión con parámetro:
1 using System;
2 using System.Runtime.Remoting.Metadata.W3cXsd2001;
3 using NUnit.Framework;
4
5 namespace LeapYear
6 {
7 public class Tests
8 {
9 [Test]
10 public void CanInstantiate()
11 {
12 new Year(1997);
13 }
14
15 [Test]
16 public void CanRespondToIsLeapMessage()
17 {
18 var year = new Year(1997);
19 year.IsLeap();
20 }
21 }
22 }
Lo cierto es que el primer test nos sobra, porque está implícito en el otro.
1 using System;
2 using System.Runtime.Remoting.Metadata.W3cXsd2001;
3 using NUnit.Framework;
4
5 namespace LeapYear
6 {
7 public class Tests
8 {
9 [Test]
10 public void CanRespondToIsLeapMessage()
11 {
12 var year = new Year(1997);
13 year.IsLeap();
14 }
15
16 }
17 }
Y ahora podemos eliminar el constructor sin parámetros, ya que no se volverá a usar en ningún caso:
1 using System;
2
3 namespace LeapYear
4 {
5 public class Year
6 {
7 private readonly int _aYear;
8
9 public Year(int aYear)
10 {
11 _aYear = aYear;
12 }
13
14 public bool IsLeap()
15 {
16 return false;
17 }
18 }
19 }
Tercera ley: escribir más del código de producción necesario para que pase el test
Se trata quizá de la violación más frecuente de todas. Llega un momento en que “vemos” el algoritmo con tanta claridad que nuestro impulso es escribirlo ya y terminar el proceso. Sin embargo, esto nos puede llevar a obviar algunas situaciones. Por ejemplo, en una aplicación podríamos “ver” el algoritmo general e implementarlo. Sin embargo, eso podría habernos distraído de uno o varios casos particulares y no contemplarlos, lo que una vez incorporado a la aplicación y desplegado posiblemente aparecerían errores en producción, incluso con pérdidas económicas.
Por ejemplo, si añadimos un test para probar que controlamos los años no bisiestos:
1 using System;
2 using System.Runtime.Remoting.Metadata.W3cXsd2001;
3 using NUnit.Framework;
4
5 namespace LeapYear
6 {
7 public class Tests
8 {
9 [Test]
10 public void CanRespondToIsLeapMessage()
11 {
12 var year = new Year(1997);
13 year.IsLeap();
14 }
15
16 [Test]
17 public void CommonYearsAreNoLeapYears()
18 {
19 var year = new Year(1997);
20 Assert.False(year.IsLeap());
21 }
22 }
23 }
En el estado actual de nuestro ejercicio, un exceso de código sería este:
1 using System;
2
3 namespace LeapYear
4 {
5 public class Year
6 {
7 private readonly int _aYear;
8
9 public Year(int aYear)
10 {
11 _aYear = aYear;
12 }
13
14 public bool IsLeap()
15 {
16 if (_aYear % 4 == 0)
17 {
18 if (_aYear % 400 == 0)
19 {
20 return false;
21 }
22
23 if (_aYear % 100 == 0)
24 {
25 return true;
26 }
27
28 return true;
29 }
30 return false;
31 }
32 }
33 }
El test pasa con este código, pero como puedes ver se ha introducido mucho más del necesario para tener el comportamiento definido por el test, añadiendo código para controlar los años bisiestos y los casos especiales. Así que aparentemente todo está bien.
Si probamos un año bisiesto, veremos que el código funciona, lo que refuerza nuestra impresión de que todo es correcto.
1 using System;
2 using System.Runtime.Remoting.Metadata.W3cXsd2001;
3 using NUnit.Framework;
4
5 namespace LeapYear
6 {
7 public class Tests
8 {
9 [Test]
10 public void CanRespondToIsLeapMessage()
11 {
12 var year = new Year(1997);
13 year.IsLeap();
14 }
15
16 [Test]
17 public void CommonYearsAreNoLeapYears()
18 {
19 var year = new Year(1997);
20 Assert.False(year.IsLeap());
21 }
22
23 [Test]
24 public void ShouldIdentifyStandardLeapYears()
25 {
26 var year = new Year(1996);
27 Assert.True(year.IsLeap());
28 }
29 }
30 }
Pero, un nuevo test falla. Los años divisibles por 100 no deben ser bisiestos (salvo que sean divisibles por 400), y este error lleva un buen rato en nuestro código, pero hasta ahora no teníamos un test que ejecutase esa parte del código.
1 using System;
2 using System.Runtime.Remoting.Metadata.W3cXsd2001;
3 using NUnit.Framework;
4
5 namespace LeapYear
6 {
7 public class Tests
8 {
9 [Test]
10 public void CanRespondToIsLeapMessage()
11 {
12 var year = new Year(1997);
13 year.IsLeap();
14 }
15
16 [Test]
17 public void CommonYearsAreNoLeapYears()
18 {
19 var year = new Year(1997);
20 Assert.False(year.IsLeap());
21 }
22
23 [Test]
24 public void ShouldIdentifyStandardLeapYears()
25 {
26 var year = new Year(1996);
27 Assert.True(year.IsLeap());
28 }
29
30 [Test]
31 public void ShouldIdentifyCenturyExceptionOfLeapYears()
32 {
33 var year = new Year(1900);
34 Assert.False(year.IsLeap());
35 }
36 }
37 }
Este es el tipo de problemas que pueden pasar desapercibidos cuando añadimos demasiado código para hacer pasar un test. El exceso de código posiblemente no afecta al test que tenemos entre manos, por lo que no sabremos si esconde algún tipo de problema y no lo sabremos si no llegamos a construir un test que lo ponga de manifiesto. O peor: no lo sabremos hasta que el bug explota en producción.
La solución es bastante simple: añade solo el código estrictamente necesario para que el test pase, aunque solo sea devolver el valor esperado por el propio test. No introduzcas comportamiento si no existe antes un test que te obligue a ello porque está fallando.
En nuestro caso era el test que verificaba el tratamiento de los años no bisiestos. De hecho, el siguiente test, que pretendía introducir el comportamiento de detectar los años bisiestos estándar (años divisibles por 4) pasaba sin necesidad de añadir nuevo código. Esto nos lleva al siguiente punto.
Qué significa que un test pase nada más escribirlo
Cuando escribimos un test y pasa sin añadir código de producción puede ser por alguno de estos motivos:
- El algoritmo que hemos escrito es lo bastante general como para cubrir todos los casos posibles: hemos terminado nuestro desarrollo.
- El ejemplo que hemos elegido no es cualitativamente diferente de otros que ya hemos usado y, por lo tanto, no nos fuerza a escribir código de producción. tenemos que encontrar otro ejemplo.
- Hemos añadido demasiado código, que es lo que acabamos de contar en el apartado anterior.
En esta kata Leap Year, por ejemplo, llegará un momento en que no hay forma de escribir un test que falle porque el algoritmo cubre todos los casos: años no bisiestos, bisiestos, años no bisiestos cada 100 años y bisiestos cada 400 años.
La otra posibilidad es que el ejemplo escogido no sea representativo de un nuevo comportamiento, lo que puede venir dado por una mala definición de la tarea o por no haber analizado bien los posibles escenarios.
El ciclo red-green-refactor
Las tres leyes establecen un framework que podríamos llamar “de bajo nivel”. Martin Fowler, por su parte, define el ciclo TDD en estas tres fases que estarían en un nivel superior de abstracción:
- Escribe un test para el siguiente fragmento de funcionalidad que deseas añadir.
- Escribe el código de producción necesario para que el test pase.
- Refactoriza el código, tanto el nuevo como el anterior, para que esté bien estructurado.
Estas tres fases definen lo que se suele conocer como el ciclo “red-green-refactor”, nombrado así por el estado de los tests en cada una de las fases del ciclo:
- Red: la creación de un test que falla (está en rojo) y que describe la funcionalidad o comportamiento que queremos introducir en el software de producción.
- Green: la escritura del código de producción necesario para hacer pasar el test (ponerlo en verde) con lo cual se verifica que se ha añadido el comportamiento especificado.
- Refactor: manteniendo los tests en verde, reorganizar el código para estructurarlo mejor, haciéndolo más legible y sostenible sin perder la funcionalidad desarrollada hasta el momento.
En la práctica los ciclos de refactor surgen después de un cierto número de ciclos de las tres leyes. Los pequeños cambios impulsados por estas se acumulan hasta llegar a un punto en el que comienzan a aparecer smells de código que requieren el refactor.