Diseño ágil con TDD

8.4. Implementando la operación de la resta

Vamos a por la resta. ¡Uy, ya nos íbamos directos a escribir el código de la función que resta! Tenemos que estar bien atentos para dirigirnos primero al test unitario. Al comienzo es muy común encontrarse inmerso en la implementación de código sin haber escrito un test que falla antes y hay que tener ésto en cuenta para no desanimarse. La frustración no nos ayudará.

Si se da cuenta de que ha olvidado escribir el test o de que está escribiendo más código del necesario para la correcta ejecución del test, deténgase y vuelva a empezar.

[Test]
public void Substract()
{
    int result = calculator.Substract(5, 3);
    Assert.AreEqual(2, result);
}

Aquí acabamos de hacer otra decisión de diseño sin advertirlo.Estamos definiendo una API donde el orden de los parámetros de los métodos es importante. En el test que acabamos de escribir asumimos que a 5 se le resta 3 y no al revés. Esto es probablemente tan obvio que no nos lo hemos planteado tal que así pero si por un momento le hemos dado la vuelta a los parámetros mentalmente, ha tenido que llegarnos la pregunta de si se aceptan números negativos como resultado de la operación. Entonces la apuntamos en la libreta para no interrumpir lo que estamos haciendo y que no se nos olvide:

* ¿Puede ser negativo el resultado de una resta en nuestra calculadora?
* Confirmar que efectivamente el orden de los parámetros produce resultados diferentes
# Aceptación - "2 + 2", devuelve 4
* Sumar 2 al número 2, devuelve 4
* Restar 3 al número 5, devuelve 2
* La cadena "2 + 2" tiene dos números y un operador que son '2', '2' y '+'
# Aceptación - "5 + 4 * 2 / 2", devuelve 9
# Aceptación - "3 / 2", produce ERROR
# Aceptación - "* *4 - 2": produce ERROR
# Aceptación - "*4 5 - 2": produce ERROR
# Aceptación - "*4 5 - 2 : produce ERROR
# Aceptación - "*45-2-": produce ERROR

Como estamos en rojo vamos a ponernos en verde lo antes posible:

public int Substract(int arg1, int arg2)
{
    return 2;
}

De acuerdo, funciona. Ahora necesitamos otro test que nos permita probar los otros casos de uso de la función y miramos a la libreta. Como existe una duda, nos reunimos con el equipo y lo comentamos, preguntamos al cliente y .... decide que es aceptable devolver números negativos porque al fin y al cabo es como si hubiese un signo menos delante del número. Para aclararlo con un ejemplo se añade un test de aceptación a la libreta:

# Aceptación - "2 + 2", devuelve 4
* Sumar 2 al número 2, devuelve 4
* Restar 3 al número 5, devuelve 2
* La cadena "2 + 2" tiene dos números y un operador que son '2', '2' y '+'
# Aceptación - "5 + 4 * 2 / 2", devuelve 9
# Aceptación - "3 / 2", produce ERROR
# Aceptación - "* *4 - 2": produce ERROR
# Aceptación - "*4 5 - 2": produce ERROR
# Aceptación - "*4 5 - 2 : produce ERROR
# Aceptación - "*45-2-": produce ERROR
# Aceptación - "2 + -2"devuelve 0

Y triangulamos. Escribimos el mínimo código necesario para pedirle a la resta que sea capaz de manejar resultados negativos:

[Test]
public void SubstractReturningNegative()
{
    int result = calculator.Substract(3, 5);
    Assert.AreEqual(-2, result);
}

Busquemos el color verde:

public int Substract(int arg1,int arg2)
{
    return arg1 - arg2;
}

Y justo al escribir esto se nos viene otra serie de preguntas.

# Aceptación - "2 + 2", devuelve 4
La cadena "2 + 2" tiene dos números y un operador que son '2', '2' y '+'
# Aceptación - "5 + 4*2 / 2", devuelve 9
# Aceptación - "3 / 2", produce ERROR
# Aceptación - "* *4 - 2": produce ERROR
# Aceptación - "*4 5 - 2": produce ERROR
# Aceptación - "*4 5 - 2 : produce ERROR
# Aceptación - "*45-2-": produce ERROR
# Aceptación - "2 + -2" devuelve 0
* ¿Cual es el número más pequeño que se permite como parámetro?
* ¿Y el más grande?
* ¿Qué pasa cuando el resultado es menor que el número más pequeño permitido?
* ¿Qué pasa cuando el resultado es mayor que el número más grande permitido?

Como puede apreciar hemos eliminado las cuestiones resueltas de la libreta. Las nuevas cuestiones atañen a todas las operaciones de la calculadora. Ciertamente no sabemos si la calculadora correrá en un ordenador con altas prestaciones o en un teléfono móvil. Quizás no tenga sentido permitir más dígitos de los que un determinado dispositivo puede mostrar en pantalla aunque el framework subyacente lo permita. Transformarmos las preguntas en nuevos tests de aceptación:

# Aceptación - "2 + 2", devuelve 4
* La cadena "2 + 2" tiene dos números y un operador que son '2', '2' y '+'
# Aceptación - "5 + 4*2 / 2", devuelve 9
# Aceptación - "3 / 2", produce ERROR
# Aceptación - "* *4 - 2": produce ERROR
# Aceptación - "*4 5 - 2": produce ERROR
# Aceptación - "*4 5 - 2 : produce ERROR
# Aceptación - "*45-2-": produce ERROR
# Aceptación - "2 + -2"devuelve 0
# Aceptación - Configurar el número más grande que puede usarse como argumento y como resultado
# Aceptación - Configurar el número más pequeño que puede usarse como argumento y como resultado
# Aceptación - Si el límite superior es 100 y alguno de los parámetros o el resultado es mayor que 100, ERROR
# Aceptación - Si el límite inferior es -100 y alguno de los parámetros o el resultado es menor que -100, ERROR

Simplifiquemos las frases para tener unos tests de aceptación más claros:

# Aceptación - "2 + 2", devuelve 4
La cadena "2 + 2" tiene dos números y un operador que son ’2’, ’2’ y ’+’
# Aceptación - "5 + 4*2 / 2", devuelve 9
# Aceptación - "3 / 2", produce ERROR
# Aceptación - "* *4 - 2": produce ERROR
# Aceptación - "*4 5 - 2": produce ERROR
# Aceptación - "*4 5 - 2 : produce ERROR
# Aceptación - "*45-2-": produce ERROR
# Aceptación - "2 + -2"devuelve 0
# Aceptación - Límite Superior =100
# Aceptación - Límite Superior =500
# Aceptación - Límite Inferior = -1000
# Aceptación - Límite Inferior = -10
# Aceptación - Limite Superior=100 y parámetro mayor que 100, produce ERROR
# Aceptación - Limite Superior=100 y resultado mayor que 100, produce ERROR
# Aceptación - Limite Inferior=10 y parámetro menor que 10, produce ERROR
# Aceptación - Limite Inferior=10 y resultado menor que 10, produce ERROR

Ahora tenemos dos caminos. Podemos seguir adelante afrontando la segunda línea de la libreta, el analizador o podemos resolver la cuestión de los límites. En TDD resolvemos el problema como si de un árbol se tratase. La raíz es el test de aceptación y los nodos hijos son tests de desarrollo, que unas veces serán unitarios y otras veces quizás de integración. Un árbol puede recorrerse de dos maneras; en profundidad y en amplitud.

La decisión la tomamos en función de nuestra experiencia, buscando lo que creemos que nos va a dar más beneficio, bien en tiempo o bien en prestaciones. No hay ninguna regla que diga que primero se hace a lo ancho y luego a lo largo.

En este caso decidimos poder configurar la calculadora con el número más grande permitido y el más pequeño. Vamos a explorar a lo ancho. Es una funcionalidad que invocaríamos de esta manera:

[Test]
public void SubstractSettingLimitValues()
{
    Calculator calculator = new Calculator(-100, 100);
    int result = calculator.Substract(5, 10);
    Assert.AreEqual(-5, result);
}

Estamos en rojo y ni siquiera es posible compilar porque el constructor de la clase Calculator no estaba preparado para recibir parámetros. Hay que tomar una decisión inmediata que no necesitamos apuntar en la libreta, ya que sin decidir no podemos ni compilar: ¿El constructor recibe parámetros? ¿Creamos dos versiones del constructor, con parámetros y sin ellos?

Por un lado si cambiamos el constructor para que acepte argumentos, necesitamos modificar el SetUp porque usa la versión anterior, lo cual nos recuerda que los tests requieren mantenimiento. Sin embargo, el costo de este cambio es mímino. Por otro lado podemos sobrecargar el constructor para tener ambas variantes pero, ¿qué pasa entonces cuando no indicamos los valores límite y se usan argumentos que superan el límite impuesto por el framework subyacente (en este caso .Net)?

No conviene decidir sin saber qué ocurrirá, mejor hacemos la prueba de sumar números muy grandes cuyo resultado excede dicho límite y observamos qué hace el runtime de .Net (Int32.MaxValue + 1 por ejemplo). El resultado es el número más pequeño posible, un número negativo. Es como si un contador diese la vuelta. Es un comportamiento muy raro para una calculadora. Nos interesa más que sea obligatorio definir los límites. Bien pues ya podemos modificar el constructor para que admita los límites y los tests existentes que tras el cambio no compilen, con vistas a conseguir luz verde.

public classCalculator
{
    public Calculator(int minValue, int maxValue) { }

    //...
}

El último test que habíamos escrito (SubstractSettingLimitValues) no tiene nada diferente a los demás porque ya todos definen los límites; vamos a modificarlo escogiendo uno de los casos de uso de la lista. Tomamos el caso en que se excede el límite inferior y decidimos que en tal situación queremos lanzar una excepción de tipo OverflowException.

[Test]
public void SubstractExcedingLowerLimit()
{
    Calculator calculator = new Calculator(-100, 100);
    try 
    {
        int result = calculator.Substract(10, 150);
        Assert.Fail("Exception is not being thrown when" +
            "exceedinglowerlimit");
    }
    catch(OverflowException)
    {
        // Ok, the SUT works as expected
    }
}

Efectivamente el test falla. Si el método Substract hubiese lanzado la excepción, ésta hubiese sido capturada por el bloque catch que silenciosamente hubiese concluido la ejecución del test. Un test que concluye calladito como este es un test con luz verde.

No es obligatorio que exista una sentencia Assert, aunque es conveniente usarlas para aumentar la legibilidad del código. Para conseguir luz verde ya no vale lanzar la excepción sin hacer ninguna comprobación porque los otros tests fallarían. Necesitamos poder consultar los límites definidos y actuar en consecuencia.

Esto nos recuerda que hay líneas en nuestra lista que tenemos que resolver antes que la que nos ocupa. Por tanto dejamos aparcado éste (como el test falla no se nos olvidará retomarlo, de lo contrario deberíamos apuntarlo en la libreta) y nos encargamos de los casos de definición y consulta de límites:

[Test]
public void SetAndGetUpperLimit()
{
    Calculator calculator = new Calculator(-100, 100);
    Assert.AreEqual(100, calculator.UpperLimit);
}

No compila, hay que definir la propiedad UpperLimit en Calculator. Puesto que la propiedad LowerLimit es exactamente del mismo tipo que UpperLimit, aquí podemos atrevernos a escribir el código que asigna y recupera ambas.

public classCalculator
{
    private int _upperLimit;
    private int _lowerLimit;

    public int LowerLimit
    {
        get { return_ lowerLimit; }
        set { _lowerLimit = value; }
    }

    public int UpperLimit
    {
        get { return _upperLimit; }
        set { _upperLimit = value; }
    }

    publicCalculator(int minValue, int maxValue)
    {
        _upperLimit = maxValue;
        _lowerLimit = minValue;
    }
}

Así, tiene sentido añadir otro Assert al test en que estamos trabajando y cambiarle el nombre ... ¿No habíamos dicho que era conveniente que un test tuviera un único Assert y probase una sola cosa? Es que semánticamente o funcionalmente ambas propiedades de la clase son para lo mismo, desde el punto de vista del test: asignar valores y recuperar valores de variables de instancia. O sea que no estamos infringiendo ninguna norma.

Reconocer qué es lo que el test está probando es importantísimo para separar adecuadamente la funcionalidad en sus respectivos métodos o clases. Cuando se escribe un test sin tener claro lo que se pretende, se obtiene un resultado doblemente negativo: código de negocio problemático y un test difícil de mantener.

[Test]
public void SetAndGetLimits()
{
    Calculator calculator = new Calculator(-100, 100);
    Assert.AreEqual(100, calculator.UpperLimit);
    Assert.AreEqual(-100, calculator.LowerLimit);
}

El valor de los tests es que nos obligan a pensar y a descubrir el sentido de lo que estamos haciendo. Escribir tests no debe convertirse en una cuestión de copiar y pegar, sino en una toma de decisiones. Es por eso que en algunos casos es permisible incluir varios Assert dentro de un mismo test y en otros no; depende de si estamos probando la misma casuística aplicada a varios elementos o no.

Ejecutamos los tests y pasan todos menos SubstractExcedingLowerLimit por lo que nos ponemos manos a la obra y escribimos el mínimo código posible que le haga funcionar y no rompa los demás.

public int Substract(int arg1,int arg2)
{
    int result = arg1 - arg2;
    if(result < _lowerLimit)
    {
        throw new OverflowException("Lower limit exceeded");
    }
    return result;
}

Nos queda probar el caso en el que el resultado excede el límite superior y los casos en que los argumentos también exceden los límites. Vamos paso a paso:

[Test]
public void AddExcedingUpperLimit()
{
    Calculator calculator = new Calculator(-100, 100);
    try
    {
        int result = calculator.Add(10, 150);
        Assert.Fail("This should fail: we’re exceding upper limit");
    }
    catch(OverflowException)
    {
        // Ok, the SUT works as expected
    }
}

He tomado el método Add en lugar de restar para no olvidar que estas comprobaciones se aplican a todas las operaciones de la calculadora. Implementación mínima:

public int Add(int arg1,int arg2)
{
    int result = arg1 + arg2;
    if(result > _upperLimit)
    {
        throw new OverflowException("Upper limit exceeded");
    }
    return result;
}

Funciona pero se ve claramente que este método de suma no hace la comprobación del límite inferior. ¿Es posible que el resultado de una suma sea un número menor que el límite inferior? Si uno de sus argumentos es un número más pequeño que el propio límite inferior, entonces es posible. Entonces es el momento de atacar los casos en que los parámetros que se pasan superan ya de por sí los límites establecidos.

[Test]
public void ArgumentsExceedLimits()
{
    Calculator calculator = new Calculator(-100, 100);
    try
    {
        calculator.Add(
            calculator.UpperLimit + 1, calculator.LowerLimit - 1);
        Assert.Fail("This should fail: arguments exceed limits");
    }
    catch(OverflowException)
    {
        // Ok, this works
    }
}

Este test se asegura de no caer en el caso anterior (el de que el resultado de la suma es inferior al límite) y aprovecha para probar ambos límites. Dos comprobaciones en el mismo test, lo cual es válido porque son realmente la misma característica. A por el verde:

public int Add(int arg1,int arg2)
{
    if(arg1 > _upperLimit)
        throw new OverflowException(
            "First argument exceeds upper limit");
    if(arg2 < _lowerLimit)
        throw new OverflowException(
            "Second argument exceeds lower limit");
    int result = arg1 + arg2;
    if(result > _upperLimit)
    {
        throw new OverflowException("Upper limit exceeded");
    }
    return result;
}

¿Y qué tal a la inversa?

[Test]
public void ArgumentsExceedLimitsInverse()
{
    Calculator calculator = new Calculator(-100, 100);
    try
    {
        calculator.Add(
            calculator.LowerLimit -1, calculator.UpperLimit + 1);
        Assert.Fail("This should fail: arguments exceed limits");
    }
    catch(OverflowException)
    {
        // Ok, this works
    }
}

Pintémoslo de verde!:

public int Add(int arg1,int arg2)
{
    if(arg1 > _upperLimit)
        throw new OverflowException(
            "First argument exceeds upper limit");
    if(arg2 < _lowerLimit)
        throw new OverflowException(
            "First argument exceeds lower limit");
    if (arg1 < _lowerLimit)
        throw new OverflowException(
            "Second argument exceeds lower limit");
    if (arg2 > _upperLimit)
        throw new OverflowException(
            "Second argument exceeds upper limit");

    int result = arg1 + arg2;
    if(result > _upperLimit)
    {
        throw new OverflowException("Upper limit exceeded");
    }
    return result;
}

La resta debería comportarse igual:

[Test]
public void ArgumentsExceedLimitsOnSubstract()
{
    Calculator calculator = new Calculator(-100, 100);
    try
    {
        calculator.Substract(
            calculator.UpperLimit + 1, calculator.LowerLimit - 1);
        Assert.Fail("This should fail: arguments exceed limits");
    }
    catch(OverflowException)
    {
        // Ok, this works
    }
}

El test no pasa. Lo más rápido sería copiar las líneas de validación de la suma y pegarlas en la resta. Efectivamente podemos hacerlo, luego ver que los tests pasan y después observar que existe duplicidad y exige refactorizar. Esto es lo aconsejable para lo programadores menos experimentados. Sin embargo, algo tan evidente puede ser abreviado en un solo paso por el desarrollador experto. Estamos ante un caso perfecto para refactorizar extrayendo un método:

public bool ValidateArgs(int arg1,int arg2)
{
    if(arg1 > _upperLimit)
        throw new OverflowException(
            "First argument exceeds upper limit");
    if(arg2 < _lowerLimit)
        throw new OverflowException(
            "First argument exceeds lower limit");
    if(arg1 < _lowerLimit)
        throw new OverflowException(
            "Second argument exceeds lower limit");
    if(arg2 > _upperLimit)
        throw new OverflowException(
            "Second argument exceeds upper limit");
    return true;
}

public int Add(int arg1,int arg2)
{
    ValidateArgs(arg1, arg2);

    int result = arg1 + arg2;
    if(result > _upperLimit)
    {
        throw new OverflowException("Upperlimitexceeded");
    }
    return result;
}

public int Substract(int arg1,int arg2)
{
    ValidateArgs(arg1, arg2);

    int result = arg1 - arg2;
    if(result < _lowerLimit)
    {
        throw new OverflowException("Lowerlimitexceeded");
    }
    return result;
}

Los tests pasan. ¿Queda más código duplicado?. Sí, todavía queda algo en el SUT y es la línea que llama al método de validación pero de eso nos encargamos después. Tener una sola línea duplicada no es muy malo... ¿lo es? (la duda es buena querido lector; y va a ser que sí que es malo). ¿Están todos los casos de uso probados?. La libreta dice:

# Aceptación - "2 + 2", devuelve 4
* La cadena "2 + 2" tiene dos números y un operador que son '2', '2' y '+'
# Aceptación - "5 + 4*2 / 2", devuelve 9
# Aceptación - "3 / 2", produce ERROR
# Aceptación - "* *4 - 2": produce ERROR
# Aceptación - "*4 5 - 2": produce ERROR
# Aceptación - "*4 5 - 2 : produce ERROR
# Aceptación - "*45-2-": produce ERROR
# Aceptación - "2 + -2"devuelve 0
# Aceptación - Límite Superior =100
# Aceptación - Límite Superior =500
# Aceptación - Límite Inferior = -1000
# Aceptación - Límite Inferior = -10
# Aceptación - Limite Superior=100 y parámetro mayor que 100, produce ERROR
# Aceptación - Limite Superior=100 y resultado mayor que 100, produce ERROR
# Aceptación - Limite Inferior=10 y parámetro menor que 10, produce ERROR
# Aceptación - Limite Inferior=10 y resultado menor que 10, produce ERROR

Las últimas líneas albergan múltiples ejemplos y retenerlos todos mentalmente es peligroso, es fácil que dejemos algunos atrás por lo que expandimos la lista:

# Aceptación - "2 + 2", devuelve 4
* La cadena "2 + 2" tiene dos números y un operador que son '2', '2' y '+'
# Aceptación - "5 + 4*2 / 2", devuelve 9
# Aceptación - "3 / 2", produce ERROR
# Aceptación - "* *4 - 2": produce ERROR
# Aceptación - "*4 5 - 2": produce ERROR
# Aceptación - "*4 5 - 2 : produce ERROR
# Aceptación - "*45-2-": produce ERROR
# Aceptación - "2 + -2" devuelve 0
# Aceptación - Límite Superior = 100
# Aceptación - Límite Superior = 500
# Aceptación - Límite Inferior = -1000
# Aceptación - Límite Inferior = -10
* A: El primer argumento sobrepasa el límite superior
* B: El primer argumento sobrepasa el límite inferior
* C: El segundo argumento sobrepasa el límite superior
* D: El segundo argumento sobrepasa el límite inferior
* E: El resultado de una operación sobrepasa el límite superior
* F: El resultado de una operación sobrepasa el límite inferior
* Todos los casos de uso anteriores se aplican a todas las operaciones aritméticas

No hemos probado por completo que la resta valida sus dos argumentos, sólo hemos probado los casos A y D restando. Necesitaríamos otro test más. Si escribimos dos tests para la validación en cada operación aritmética, vamos a terminar con una cantidad de tests muy grande e inútil (porque en verdad están todos probando la misma cosa) a base de copiar y pegar. Esto empieza a oler mal.


Copyright (c) 2010-2013 Carlos Ble. La copia y redistribución de esta página se permite bajo los términos de la licencia Creative Commons Atribución SinDerivadas 3.0 Unported siempre que se conserve esta nota de copyright.