Triangular en TDD
Hace unas semanas tuve la oportunidad de asistir a un Coding Dojo de Software Craftsmanship Madrid facilitado por Carlos Ble y Antonio de la Torre donde en una primera rotación hice pair programming con mi socio Miguel Ángel de Kirei Studio y cuando cambiamos de pareja me puse con Sergio León.
Con Sergio acabe charlando sobre TDD más que programando que también puede llegar a ser muy productivo. En concreto estuvimos hablando sobre la triangulación.
Este último fin de semana en twitter hemos vuelto a hablar del tema así que he pensado añadir mi granito de arena escribiendo sobre la triangulación.
Historia
La técnica se llama triangulación por analogía a la triangulación de radar donde se deben utilizar las salidas de al menos dos radares para determinar la posición de una unidad.
Además, en la triangulación de radar, la posición se mide de manera indirecta, mediante la combinación de los siguientes datos: rango entre dos radares, medición realizados por cada radar y las posiciones de los radares (que sabemos, porque somos los que poner los radares allí).
A partir de estos datos, podemos derivar un triángulo, así que podemos usar la trigonometría para calcular la posición del tercer vértice del triángulo, que es la posición deseada de la unidad (dos puntos restantes son las posiciones de los radares).
Dicha medición es de naturaleza indirecta, porque no medimos la posición directamente, pero se calcula a partir de otras medidas de ayuda.
Triangulación
La primera vez que oí hablar del concepto de triangulación fue en el libro Test Driven Development: By Example de Kent Beck.
La triangulación es descrita por Kent Beck como la técnica más conservadora de las posibles en TDD.
Estas técnicas son:
- Escribir la implementación obvia
- Falsificar el resultado de la implementación
- Triangular
Pasos de la triangulación
La triangulación se compone de los siguientes pasos:
- Piensa en un ejemplo más simple que debe resolver el método a probar.
- Escribe un test que cubra ese ejemplo, implementa la solución y refactoriza.
- Repite este proceso añadiendo cada vez ejemplos más complejos hasta que pienses que no existen más (baby steps).
Cuando debemos triangular
Como norma general triangular hay que hacerlo cuando el algoritmo a crear no lo tenemos claro.
En ocasiones si la solución parece muy simple lo más sencillo es escribir la implementación obvia.
El problema es que muchas veces los desarrolladores nos precipitamos pensando que la solución es sencilla y cuando tenemos un buen lío en el código nos damos cuenta que no era tan sencillo como parecía.
De ahí el lema de la imagen del artículo Keep Calm And Triangulate :) (mantén la calma y triangula).
Kata FizzBuzz
Para entender la triangulación vamos a hacer la versión simple de una kata bastante famoso para practicar TDD que es FizzBuzz.
Descripción del problema
Escribe un programa que imprima los números del 1 al 100, pero aplicando las siguientes normas:
Devuelve Fizz si el número es divisible por 3.
Devuelve Buzz si el número es divisible por 5.
Devuelve FizzBuzz si el número es divisible por 3 y por 5.
Salida de ejemplo:
1,2,Fizz,4,Buzz,Fizz,7,8,Fizz,Buzz,11,Fizz,13,14,FizzBuzz,16,17,Fizz,19,Buzz... etc hasta el 100
Primer test
El ejemplo más sencillo sería que nos devolviera el primer número.
[Fact]
public void Generate_First_Number_If_MaxNumber_Is_One()
{
Assert.Equal(new String[{"1"},FizzBuzz.GenerateOutput(1));
}
Falsificamos el resultado primero.
public static string[] GenerateOutput(int maxNumber)
{
return new string[] {"1"};
}
Segundo test
El siguiente ejemplo sería abordar el siguiente número.
[Fact]
public void Generating_The_First_Two_Numbers_If_MaxNumber_Is_Two()
{
Assert.Equal(new String[] { "1","2" }, FizzBuzz.GenerateOutput(2));
}
Esto ya nos obliga a introducir un bucle y empezamos a implementar una solución más genérica.
public static string[] GenerateOutput(int maxNumber)
{
String[] result = new String[maxNumber];
for (int i = 1; i <= maxNumber; i++)
{
result[i-1] = i.ToString();
}
return result;
}
Añadiendo el Fizz
El siguiente ejemplo sería abordar ya la implementación de la lógica para el Fizz.
[Fact]
public void Generating_The_First_Two_Numbers_And_Fizz_If_MaxNumber_Is_Three()
{
Assert.Equal(new String[] { "1","2","Fizz" }, FizzBuzz.GenerateOutput(3));
}
Tenemos que añadir un if para distinguir los dos comportamientos.
public static string[] GenerateOutput(int maxNumber)
{
String[] result = new String[maxNumber];
for (int i = 1; i <= maxNumber; i++)
{
if (i % 3 == 0)
{
result[i - 1] = "Fizz";
}
else {
result[i - 1] = i.ToString();
}
}
return result;
}
Una vez el test pasa podemos intentar simplificar el código eliminando la duplicación de asignar al array restado -1.
public static string[] GenerateOutput(int maxNumber)
{
String[] result = new String[maxNumber];
for (int i = 1; i <= maxNumber; i++)
{
result[i - 1] = GetValue(i);
}
return result;
}
private static String GetValue(int i)
{
if (i % 3 == 0)
{
return "Fizz";
}
return i.ToString();
}
Añadiendo el Buzz
Según tenemos el código, el ejemplo del 4 funcionaría así que para no extender el artículo demasiado pasamos al ejemplo del buzz.
[Fact]
public void Generating_The_First_Five_Numbers_With_Bizz_And_Buzz_If_MaxNumber_Is_Five()
{
Assert.Equal(new String[] { "1", "2", "Fizz", "4","Buzz" }, FizzBuzz.GenerateOutput(5));
}
Simplemente modificamos la función GetValue.
public static string[] GenerateOutput(int maxNumber)
{
String[] result = new String[maxNumber];
for (int i = 1; i <= maxNumber; i++)
{
result[i - 1] = GetValue(i);
}
return result;
}
private static String GetValue(int i)
{
if (i % 3 == 0)
return "Fizz";
else if (i % 5 == 0)
return "Buzz";
return i.ToString();
}
Añadiendo el FizzBuzz
Según tenemos el código los ejemplos hasta el número 14 funcionarían así que pasamos al ejemplo del FizzBuzz.
[Fact]
public void Generating_The_First_Fiveteen_Numbers_With_Bizz_And_Buzz_If_MaxNumber_Is_Fiveteen()
{
Assert.Equal(new String[] { "1", "2", "Fizz", "4", "Buzz", "Fizz", "7", "8", "Fizz", "Buzz", "11", "Fizz", "13", "14", "FizzBuzz" }, FizzBuzz.GenerateOutput(15));
}
Hay varias formas de conseguir que funcione este test, una de ellas es añadir una lógica para cuando es múltiplo de 15, que sería cuando lo es de 3 y de 5.
public static string[] GenerateOutput(int maxNumber)
{
String[] result = new String[maxNumber];
for (int i = 1; i <= maxNumber; i++)
{
result[i - 1] = GetValue(i);
}
return result;
}
private static String GetValue(int i)
{
if (i % 15 == 0)
return "FizzBuzz";
else if (i % 3 == 0)
return "Fizz";
else if (i % 5 == 0)
return "Buzz";
return i.ToString();
}
Con este último ejemplo ya hemos resuelto la versión simple del kata FizzBuzz utilizando triangulación.
Código del kata en GiHub.
Conclusiones
La triangulación es la técnica más conservadora de TDD y deberíamos utilizarla cuando no tenemos clara la implementación de la solución ya que utilizando ejemplos de más sencillos a más complejos vamos definiendo el algoritmo en Baby Steps.
¿Qué opinas tú sobre la triangulación en TDD? Déjame tus ideas en la sección de comentarios.