Los asserts nos permiten comprobar el resultado de las pruebas unitarias. Podemos destacar que existe un buen soporte para trabajar con genéricos, algo que nos puede facilitar mucho el trabajo a realizar a la hora de escribir nuestras pruebas. Existe un buen numero de asserts pero algunos son la negación o lo contrario de otro (por ejemplo: True-False, Null-NotNull...):

  • Equal<T>: Comprueba si dos objetos sean iguales.
  • NotEqual<T>: Comprueba que dos objetos no sean iguales.
  • NotSame: Comprueba que dos objetos no sean la misma instancia.
  • Same: Comrpueba que dos objetos sean la misma instancia.
  • Contains, Contains<T>: Verifica que una colección contiene un objeto dado.
  • DoesNotContain, DoesNotContain<T>: Verifica que una colección no contiene un objeto dado.
  • DoesNotThrow: Comprueba que un bloque de código no lanza ninguna excepción.
  • Throws, Throws<T>: Comprueba que un bloque de código lance una determinada excepción.
  • InRange<T>: Verifica que un objeto se encuentra dentro de un rango.
  • NotInRange<T>: Verifica que un objeto no se encuentra dentro de un rango.
  • IsAsignableFrom, IsAsignableFrom<T>: Comprueba que un objeto sea del tipo dado o de uno derivado.
  • Empty: Comprueba que una colección no contenga elementos.
  • False: Verifica si una condición es falsa.
  • IsNotType, IsNotType<T>: Verifica que un objeto no es exactamente del tipo dado.
  • IsType, IsType<T>: Verifica que un objeto es exactamente del tipo dado.
  • NotEmpty: Comprueba que una colección tenga algún elemento.
  • NotNull: Verifica que una referencia a un objeto no sea nula.
  • Null: Verifica que una referencia a un objeto no sea nula.
  • True: Verifica que una condición es verdadera.

Los asserts a los que podemos prestarles más atención puede que sean Equal, InRange y Throws. Para la aserción Equal podemos pensar que no tiene mayor misterio pero, ¿qué ocurre si comparamos dos objetos de una clase que hemos creado? Para verlo con un ejemplo he creado una clase FichaDomino que simplemente contiene dos valores enteros para representar una ficha de dominó.

[Fact]

public void TestFichasIguales()

{

    FichaDomino fich1 = new FichaDomino(6, 6);

    FichaDomino fich2 = new FichaDomino(6, 6);

    Assert.Equal(fich1, fich2);

}

Al jecutar este test esperamos que pase pero el resultado obtenido es el siguiente:

TestCase 'Tests.Test.TestFichasIguales' failed: Assert.Equal() Failure

Expected: 6:6

Actual:   6:6

       en Xunit.Assert.Equal[T](T expected, T actual, IComparer`1 comparer)

       en Xunit.Assert.Equal[T](T expected, T actual)

       E:\Documentos\Visual Studio 2008\Projects\DemoTDD\Tests\Test.cs(116,0): en Tests.Test.TestFichasIguales()

Esto ocurre así porque la comparación se realiza a través del método Equals que se hereda de la clase Object que lo único que hace es comparar si las referencias de los objetos a comparar son la misma. La solución es sobrescribir el método Equals o, implementar la interfaz IComparable y/o IComparable<T> (ya que podemos hacer uso de genéricos...). Indagando un poco en la forma que trabaja xUnit, primero se va comprobar si se implementa la interfaz Icomparable<T> para llamar a CompareTo de forma genérica (es decir, con un tipo concreto), si no es así se comprueba si se implementa la interfaz IComparable para llamar a CompareTo donde el argumento a comparar es un object. Si no se implementa alguna de estas interfaces entonces es necesario sobrescribir el método Equals.

La aserción InRange verifica que un objeto esté dentro de un rango de valores. En este caso pasa tres cuartos de lo mismo que ocurre con el assert Equal, para tipos simples (enteros, strings, fechas...) la comparación es trivial pero para objetos no. Aquí necesitamos implementar IComparable o IComparable<T> para que al comparar si el objeto dado esta dentro de un determinado rango de valores el resultado sea el correcto. Como alternativa podríamos pasarle a este assert un comparador que compare objetos de la clase FichaDomino, aunque pienso que podrían producirse fallos si olvidamos que siempre tenemos que utilizar un comparador personalizado al no haber implementado IComparable. La implementación es esta:

public int CompareTo(FichaDomino other)

{

    int result = 0;

 

    if (this.numIzq < other.numIzq)

    {

        if (this.numDer < other.numDer || this.numDer == other.numDer)

            result = -1;

        else

            result = 1;

    }

    else if (this.numIzq == other.numIzq)

    {

        if (this.numDer < other.numDer)

            result = -1;

        else if (this.numDer == other.numDer)

            result = 0;

        else

            result = 1;

    }

    else

        result = 1;

 

    return result;

}

Por último, Throws<T> nos permite verificar si se produce una excepción de un tipo determinado. El parámetro que toma este assert es un delegado a la función o al código que queremos testar para que el assert pueda recoger la excepción si se produce. Vamos a crear una ficha de dominó con los valores 8:8, si la implementación de constructor es correcta debería de lanzar un excepción del tipo ArgumentException por estar esos valores fuera de los valores que puede tener una ficha de dominó

public FichaDomino(int numIzq, int numDer)

{

    this.numIzq = numIzq;

    this.numDer = numDer;

}

el test para verificarlo es:

[Fact]

public void TestFichaNueva()

{

    Assert.Throws<ArgumentException>(

        delegate

        {

            FichaDomino fich1 = new FichaDomino(8, 8);

        });

}

y el resultado es:

TestCase 'Tests.Test.TestFichaNueva'

failed: Assert.Throws() Failure

Expected: System.ArgumentException

Actual:   (No exception was thrown)

       en Xunit.Assert.Throws(Type exceptionType, ThrowsDelegate testCode)

       en Xunit.Assert.Throws[T](ThrowsDelegate testCode)

       E:\Documentos\Visual Studio 2008\Projects\DemoTDD\Tests\Test.cs(122,0): en Tests.Test.TestFichaNueva()

Qué ocurre si tenemos un método que puede lanzar una misma excepción pero por diferentes causas, de esta forma no podemos determinar cuál es la causa por la que se ha producido la excepción. Para tener un control mayor sobre las excepciones que se pueden lanzar vamos a utilizar la clase Record para capturar una excepción y después poder operar con ella. Como ejemplo vamos a tomar la calculadora (utilizada en el post anterior) para obtener un número aleatorio dentro de un rango de valores [a,b]. Las causas para lanzar una excepción son que el rango no sea correcto (a > b) o que no exista rango (a = b). Teniendo la siguiente implementación para generar un número aleatorio dentro de un rango de valores:

public static double NumAleatorioEnRango(double min, double max)

{

    if (min > max)

        throw new ArgumentException("Rango no valido. El valor minimo > maximo");

    if (min == max)

        throw new ArgumentException("Rango no valido. El valor minimo = maximo");

 

    Random random = new Random(DateTime.Now.Millisecond);

 

    double aleatorio = random.NextDouble();

    double result = ((max - min) * aleatorio) + min;

    return result;

}

Vamos a lanzar la siguiente prueba donde queremos comprobar que se lanza una excepción cuando min > max:

[Fact]

public void TestNumAleatorio3()

{

    double min = 9.0;

    double max = 5.0;

 

    Exception recException = Record.Exception(

        delegate

        {

            Calc.NumAleatorioEnRango(min, max);

        });

 

    Assert.IsType<ArgumentException>(recException);

    Assert.Equal("Rango no valido. El valor minimo > maximo", recException.Message);

}

El resultado es que las dos aserciones son correctas y el test pasa como válido.

NOTA: adjunto va un zip con la demo de prueba.