sábado, 5 de abril de 2014

Introducción a NUnit (IV): Tests parametrizados

Ya en un post anterior de la serie se habló por encima del concepto de tests parametrizados y como podíamos ejecutar un test n veces con la ayuda del atributo [TextFixture] con distintos parámetros y un constructor personalizado.

En cualquier caso, como la ejecución de tests parametrizados es una característica muy importante, bien merece su propio post en la serie.

Un test parametrizado permite ejecutar un mismo test con un conjunto de valores distintos.

 

class Pruebas

{

    [Test]

    public void Test([Values(1,2,3)] int num1)

    {

        Assert.Pass();

    }

}

image

Los atributos que permiten la ejecución de tests parametrizados son:

  • Values
  • ValuesSource
  • Combinatorial
  • Sequential
  • Random
  • Range
  • TestCase
  • TestCaseSource

El atributo [Values] sirve para indicar por cada parámetro un conjunto de valores. Al ser un atributo, los valores deben ser un tipo por valor, una constante o un typeof (en definitiva un valor interpretable en tiempo de compilación). Esto no es una limitación de NUnit, es simplemente que los atributos en .NET funcionan así.

Si el método de test tiene varios parámetros decorados con el atributo [Value], por defecto NUnit combina todos los valores (es como si hubiéramos puesto el atributo [Combinatorial] al método de test):

 

[Test]

public void Test([Values(1, 2, 3)] int num1, [Values(4, 5)] int num2)

{

    Assert.Pass();

}

image

Si por el contrario ponemos al método de test el atributo [Sequential], ahora iterará cogiendo un valor a la vez por cada parámetro y cuando no hay coincidencia pondrá el valor por defecto:

 

[Test]

[Sequential]

public void Test([Values(1, 2, 3)] int num1, [Values(4, 5)] int num2, [Values(5, 7, 3)] int resultado)

{

    var calculadora = new Calculadora();

    Assert.AreEqual(resultado, calculadora.Sumar(num1, num2));

}

image

Aunque ReSharper nos muestra null en el tercer test, realmente lo que se le pasa es un 0 que es el default del tipo int.

Si lo que queremos es un conjunto de datos aleatorio tenemos [Random]

 

[Test]

public void Test([Random(1, 100, 10)] int num1)

{

    Assert.Pass();

}

Lógicamente, ReSharper no muestra la lista de los tests y sus valores hasta que no se ejecutan.

image

Otra opción es [Range]

 

[Test]

public void Test([Range(1, 10, 2)] int num1)

{

    Assert.Pass();

}

image

Tanto con [Random] como con [Range], si hay varios parámetros “cuidadito” con [Combinatorial] (por defecto), casi mejor poner [Sequential].

Entrando ya en modo lista (IEnumerable) podemos trabajar con [ValuesSource]. [ValuesSource] permite especificar un campo, propiedad o método (de la misma clase del test o de otro clase) que sea o devuelva un tipo IEnumerable.

 

class Pruebas

{

    private IEnumerable<int> _valores = new List<int>() {1, 2, 3, 4, 5};

    [Test]

    public void Test([ValueSource("_valores")] int num1)

    {

        Assert.Pass();

    }

}

o también

class ValoresSource

{

    private IEnumerable<int> _valores = new List<int>() { 1, 2, 3, 4, 5 };

    public IEnumerable<int> Valores { get { return _valores; } }

}

class Pruebas

{

    [Test]

    public void Test([ValueSource(typeof(ValoresSource), "Valores")] int num1)

    {

        Assert.Pass();

    }

}

Si en vez de asignar valores a parámetros y combinarlos con [Combinatorial] o [Sequential], queremos especificar de forma explícita que valores tomará cada parámetro, podemos utilizar [TestCase] (con este atributo además podemos ahorrarnos decorar el método también con [Test]).

 

[TestCase(1, 2)]

[TestCase(3, 4)]

[TestCase(5, 6)]

public void Test(int num1, int num2)

{

    Assert.Pass();

}

image

Además [TestCase] tiene un montón de sobrecargas que permiten especificar:

  • Category
  • Description
  • ExpectedException
  • Explicit
  • Ignore
  • TestName, que puede ser útil para dar un nombre a un TestCase (recuerda que lo podemos repetir para ejecutar n veces un test, y con este atributo podemos diferenciarlo)

[TestCase(1, 2,TestName = "1_and_2_are_the_first_two_numbers")]

[TestCase(3, 4)]

[TestCase(5, 6)]

public void Test(int num1, int num2)

{

    Assert.Pass();

}

image

Como podemos ver [TestCase] casi reemplaza/aúna en un solo atributo a los principales atributos de NUnit

Además con [TestCase] también podemos especificar un valor esperado de retorno del método de test (con lo que ahora un test no tendría ya que devolver void)

 

[TestCase(3, 4, ExpectedResult = 7, TestName = "sum_3_4_should_be_7")]

[TestCase(5, 6, ExpectedResult = 11, TestName = "sum_5_6_should_be_11")]

public int Test(int num1, int num2)

{

    return num1 + num2;

}

Como último atributo en lo relativo a tests parametrizados, encontramos [TestCaseSource]. Con [TestCaseSource] podemos:

  • Especificar un tipo que implementa IEnumerable
  • Especificar un tipo y un miembro que implementa IEnumerable
  • Especificar un miembro que implementa IEnumerable

Fijarse que aunque esto suena muy parecido a [ValuesSource], pero hay diferencias. [TestCaseSource] no se aplica a un parámetro sino a todos los parámetros en una llamada (al igual que [Values] vs [TestCase]) y además [TestCaseSource] permite especificar un tipo que, directamente, ya implemente IEnumerable (para evitar harcodear un miembro en una string).

 

class ValoresTestDataSource

{

    private IEnumerable<int> _valores = new List<int>() { 1, 2, 3, 4, 5 };

    public IEnumerable<int> Valores { get { return _valores; } }

}

 

class Pruebas

{

    [TestCaseSource(typeof(ValoresTestDataSource), "Valores")]

    public void Test(int num1)

    {

        Assert.Pass();

    }

}

image

Si ahora cambiamos el test y pedimos 2 argumentos, también tenemos que cambiar el origen de datos porque si no tendremos un error como el siguiente:

 

[TestCaseSource(typeof(ValoresTestDataSource), "Valores")]

public void Test(int num1, int num2)

{

    Assert.Pass();

}

image

Cambiamos el origen pues:

    class ValoresTestDataSource

    {

        private IEnumerable<int[]> _valores = new List<int[]>

        {

            new int[]{1,2},

            new int[]{3,4}

        };

        public IEnumerable<int[]> Valores { get { return _valores; } }

    }

 

    class Pruebas

    {

        [TestCaseSource(typeof(ValoresTestDataSource), "Valores")]

        public void Test(int num1, int num2)

        {

            Assert.Pass();

        }

    }

Este estilo de pasar parámetros está bien pero empieza a ser un poco complicado de leer (bajo mi punto de vista). Para solucionar esto, NUnit permite devolver instancias del tipo [TestCaseData] que representan todo lo que podíamos hacer con el atributo [TestCase]. Además [TestCaseData] implementa una interface fluida y es bastante cómodo configurar el caso.

class ValoresTestDataSource

{

    public IEnumerable<TestCaseData> GetValores()

    {

        yield return new TestCaseData(1, 2)

            .SetName("1_2_should_be_3")

            .Returns(3);

        yield return new TestCaseData(3, 4)

            .SetName("3_4_should_be_7")

            .Returns(7);

    }

}

 

class Pruebas

{

    [TestCaseSource(typeof(ValoresTestDataSource), "GetValores")]

    public int Test(int num1, int num2)

    {

        return num1 + num2;

    }

}

image

A partir de aquí, crear una clase que lea los valores desde un CSV o una bd (como ya hace de serie MSUnit) no debería ser problema.

Mas posts de esta serie:

Un saludo!

No hay comentarios:

Publicar un comentario