Pruebas Unitarias en .Net Core con xUnit – 8 – Pruebas Basadas en Datos, MemberData y Custom Attribute

Pruebas Unitarias en .Net Core con xUnit – 8 – Pruebas Basadas en Datos, MemberData y Custom Attribute

En el post pasado vimos como utilizar el atributo [Theory] con [InlineData] para alimentar una prueba con diferentes sets de información, y poder ejecutar la prueba una vez con cada set. Sin embargo, muchas veces queremos usar ese mismo set de datos en múltiples pruebas, y también, no tener la limitación de solo poder recibir valores, sino que poder mandar lo que sea siempre que respetemos lo que XUnit espera.

Para eso Xunit nos ofrece 2 alternativas, el atributo MemberData, o crear nuestro propio Custom Attribute (atributo personalizado).

Estas técnicas crean clases completamente separadas de la prueba, lo que te permite utilizarlas en múltiples lugares. Adicionalmente no tenemos la limitación de poder alimentar únicamente valores, ya que podemos controlar lo que regresa.

El problema

Regresando al problema del post pasado, tenemos un método AgregarMensaje, el cual, dependiendo de la configuración del cliente, tiene las siguientes 4 posibilidades de ejecución:

  • Cliente con EnviarCorreos = true, y con EMail – Se debe de enviar correo.
  • Cliente con EnviarCorreos = false, y con EMail – No se debe de enviar correo.
  • Cliente con EnviarCorreos = true, sin Email – No se debe de enviar correo.
  • Cliente con EnviarCorreos = false, sin Email – No se debe de enviar correo.

Para probar los 4 escenarios, en el post pasado utilizamos el atributo [InlineData]. Debido a las limitaciones en cada uno de los atributos [InlineData], agregamos un entero que identifica el valor dentro del arreglo de prueba que se iba a utilizar, y usabamos ese entero para conocer el GUID del arreglo. Esto lo hicimos así por la limitación de [InlineData] de solo recibir valores, por lo que no podemos poner los GUID generados en la prueba, aunque lo correcto habría sido mandar esos GUID directamente.

Estas limitaciones nos las podemos solucionar utilizando [MemberData] o un Custom Attribute de la siguiente manera.

Usando [MemberData]

El atributo [MemberData] se utiliza para alimentar el set de datos de la prueba desde cualquier método o propiedad que entregue un listado IEnumerable, de arreglos de objetos. Siendo cada elemento de la lista un set de datos separados a ejecutar.

Para usarlo, primero vamos a crear una nueva clase. Para separar las clases que contienen los datos de las pruebas, creamos una carpeta llamada TestData. Después creamos la clase con el nombre MensajeClientesTestData. El final TestData se usa como estándar de nombramiento de este tipo de clases.

//MensajeClientesTestData.cs

public class MensajeClientesTestData
{
    public static readonly List<Cliente> Clientes = new List<Cliente>;
    {
        new Cliente()
        {
            IdCliente = Guid.NewGuid(),
            EMail = "juan.rulfo@juanrulfo.com",
            EnviarCorreos = true,
        },
        new Cliente()
        {
            IdCliente = Guid.NewGuid(),
            EMail = "pedro.paramo@juanrulfo.com",
            EnviarCorreos = false,
        },
        new Cliente()
        {
            IdCliente = Guid.NewGuid(),
            EnviarCorreos = true,
        },
        new Cliente()
        {
            IdCliente = Guid.NewGuid(),
            EnviarCorreos = false,
        },
    }; 

    public static readonly IEnumerable<object[]> TestData = new List<object>
    { 
        new object[] { Clientes[0].IdCliente, true },
        new object[] { Clientes[1].IdCliente, false },
        new object[] { Clientes[2].IdCliente, false },
        new object[] { Clientes[3].IdCliente, false }, 
    }; 
}

En esta clase vamos a poner primero el listado de los Clientes que vamos a usar en la prueba, recordemos que este listado se alimenta a ClienteServiceMock para simular las operaciones de la base de datos. Este lo agregamos como una propiedad pública.

Posteriormente agregamos otra propiedad tipo IEnumerable<object[]>, está la vamos a llamar TestData, aunque puede tener cualquier nombre. Dentro de esta creamos todos los elementos que se usaran en cada prueba, igual que en los atributos [InlineData]. En este caso, en el primer objeto lo asignamos cada uno de los IdClientes de los clientes del listado anterior, en el segundo elemento ponemos el resultado esperado para cada cliente.

Ambas propiedades son estáticas, de esa manera no tenemos que instanciar nuestra clase múltiples veces, algo que no hacer falta ya que toda la información que se incluyen en ambas propiedades es fija para todas las pruebas.

Para poderlo usar, también necesitamos hacer cambios en nuestra clase de prueba.

//MensajesClientes_Should_MemberData.cs
public class MensajesClientesService_Should_MemberData
{
    ClienteServiceMock clnSrvMock;
    CorreosServiceMock corSrvMock;
    MensajeRepositoryMock msgRepo;

    public MensajesClientesService_Should_MemberData()
    {
        clnSrvMock = new ClienteServiceMock(MensajeClientesTestData.Clientes);
        corSrvMock = new CorreosServiceMock(true);
        msgRepo = new MensajeRepositoryMock();
    }

    [Theory]
    [MemberData(nameof(MensajeClientesTestData.TestData), MemberType = typeof(MensajeClientesTestData))]
    public void AgregarMensaje_HappyPath(Guid clienteId, bool seEnvioCorreo)
    {
        //Arrange
        var titulo = "Bienvenido a nuestro Servicio.";
        var contenido = "Hola. Muchas gracias por usar nuestro servicio.";

        var sut = new MensajesClientesService(clnSrvMock, corSrvMock, msgRepo);

        //Act
        var respuesta = sut.AddMensaje(clienteId, titulo, contenido);

        //Assert
        Assert.Equal(titulo, respuesta.Titulo);
        Assert.Equal(contenido, respuesta.Contenido);
        Assert.Equal(clienteId, respuesta.IdCliente);
        Assert.Equal(seEnvioCorreo, respuesta.Enviado);
    }
}

Primero quitamos la creación del listado desde el constructor. Esta la inyectamos al constructor de ClienteServiceMock desde la nueva clase.

Lo segundo, es que quitamos todas las líneas del atributo [InlineData], y agregamos el atributo MemberData. Este funciona de la siguiente manera:

[MemberData(nameof(MensajeClientesTestData.TestData), MemberType = typeof(MensajeClientesTestData))]

El primer parámetro espera el nombre del método o propiedad que va a alimentar los datos. Podemos agregar este nombre por medio de un string, pero para hacerlo más automático utilizamos la función nameof(). Esta nos regresa el nombre como automáticamente. Te recomiendo usarla así ya que, si haces un Refactoring, y le cambias el nombre, se cambiará de manera automática.

El resto de los parámetros lo declaramos por nombre, ya que es uno de los parámetros opcionales. Por default XUnit buscara la propiedad o método dentro de la misma clase, lo cual es útil para cuando el set de datos se usa únicamente en pruebas dentro de la misma clase. Como en este caso estamos haciendo referencia a una clase externa, usamos el parámetro MemberType. Lo asignamos usando typeof() ya que recibe el tipo.

Y listo, con esto XUnit sabe que debemos de correr la prueba una vez por cada uno de los datos que encuentre en dicho listado.

A diferencia de [InlineData], el Test Explorer nos mostrará únicamente una prueba, pero si la seleccionamos, podemos ver que la caja resumen nos muestra el resultado para cada una de las ejecuciones.

Usando un Custom Atribute

La segunda opción es crear un Custom Attribute (atributo personalizado). Esta opción es muy similar al atributo MemberData, pero en este caso, crearemos una clase que utilizaremos como atributo en la función de pruebas. Para que XUnit la pueda utilizar, esta clase debe de heredar de DataAttribute, y esto te obliga a implementar la función GetData(), la cual debe de devolver, si adivinaste, un listado IEnumerable<object[]>, igual que en MemberData.

Implementarlos es muy fácil. Veamos el ejemplo:

//MensajeClientesTestDataAttribute.cs
public class MensajeClientesTestDataAttribute : DataAttribute
{
    public static readonly List<Cliente> Clientes = new List<Cliente>
    {
        new Cliente()
        {
            IdCliente = Guid.NewGuid(),
            EMail = "juan.rulfo@juanrulfo.com",
            EnviarCorreos = true,
        },
        new Cliente()
        {
            IdCliente = Guid.NewGuid(),
            EMail = "pedro.paramo@juanrulfo.com",
            EnviarCorreos = false,
        },
        new Cliente()
        {
            IdCliente = Guid.NewGuid(),
            EnviarCorreos = true,
        },
        new Cliente()
        {
            IdCliente = Guid.NewGuid(),
            EnviarCorreos = false,
        },
    };

    public override IEnumerable<object[]> GetData(MethodInfo testMethod)
    {
        return new List<object[]>{
            new object[] { Clientes[0].IdCliente, true },
            new object[] { Clientes[1].IdCliente, false },
            new object[] { Clientes[2].IdCliente, false },
            new object[] { Clientes[3].IdCliente, false },
        };
    }
}

Primero, creamos una nueva clase en la carpeta TestData, a esta la nombramos MensajeClientesTestDataAttribute.

Segundo, para tener acceso al objeto DataAttribute agregamos la referencia (using) al namespace Xunit.Sdk.

Tercero, heredamos de la clase DataAttribute. Al agregar la herencia el nombre nuestra clase nos mostrara un error, si abrimos el mensaje nos indica que debemos implementar el método GetData. Para hacerlo, con el cursor sobre el nombre de la clase y presionamos el icono del foquito (o la secuencia Ctrl +.) y seleccionamos “Implement abstract class”. Esto crea el método GetData automáticamente, lo completamos para enviar los datos de la prueba.

Cuarto, hacemos que GetData regrese el arreglo de objetos que utilizaremos como parámetros en función de prueba.

Con esto tenemos nuestra clase finalizada. Lo siguiente es modificar la prueba para utilizar el Custom Atribute, y esto es tan fácil como quitar el atributo [MemberData] y agregar uno nuevo con la clase que creamos:

//MensajesClientes_Should_CustomAttribute.cs
public class MensajesClientesService_Should_CustomAttribute
{
    ClienteServiceMock clnSrvMock;
    CorreosServiceMock corSrvMock;
    MensajeRepositoryMock msgRepo;

    public MensajesClientesService_Should_CustomAttribute()
    {
        clnSrvMock = new ClienteServiceMock(MensajeClientesTestDataAttribute.Clientes);
        corSrvMock = new CorreosServiceMock(true);
        msgRepo = new MensajeRepositoryMock();
    }

    [Theory]
    [MensajeClientesTestDataAttribute]
    public void AgregarMensaje_HappyPath(Guid clienteId, bool seEnvioCorreo)
    {
        //Arrange
        var titulo = "Bienvenido a nuestro Servicio.";
        var contenido = "Hola. Muchas gracias por usar nuestro servicio.";

        var sut = new MensajesClientesService(clnSrvMock, corSrvMock, msgRepo);

        //Act
        var respuesta = sut.AddMensaje(clienteId, titulo, contenido);

        //Assert
        Assert.Equal(titulo, respuesta.Titulo);
        Assert.Equal(contenido, respuesta.Contenido);
        Assert.Equal(clienteId, respuesta.IdCliente);
        Assert.Equal(seEnvioCorreo, respuesta.Enviado);
    }
}

Listo. Ahora, al encontrar la prueba, XUnit verifica el atributo, y buscara el método GetData para alimentar los datos a la prueba. Esto funciona muy similar a [MemberData], pero la inclusión de nuestro atributo personalizado hace que se vea más limpio el código.

Igual que con MemberData, al ejecutar esta prueba, el Test Explorer nos muestra todos los resultados agrupados debajo de la misma prueba.

Mas Posibilidades

Utilizando MemberData, o nuestro Custom Attribute, podemos hacer muchas cosas. La más importante es poder compartir el mismo set de datos con múltiples pruebas, y con múltiples clases. Simplemente implementamos el mismo atributo en cada prueba que lo necesitamos.

También podemos utilizar estos atributos para alimentar la prueba con objetos completos, no solamente variables o valores primitivos. Esto nos abre muchas posibilidades ya que la mayoría de nuestros métodos utilizan objetos completos para funcionar.

Adicional a esto, ya que estamos usando una clase que regresa un listado, no importa desde donde saquemos los datos de este listado. Lo normal es que utilices datos en memoria, pero tienes la posibilidad de leer estos datos desde una fuente externa, como un archivo de texto o una base de datos. Ten cuidado, esto rompe la idea de las pruebas unitarias, pero puede ser útil para pruebas de integración o escenarios muy específicos donde un tercero tiene el control de los datos que se utilizan para las pruebas.

Resumen

Utilizar el atributo [MemberData] o un Custom Attribute nos permite compartir nuestros sets de datos de prueba entre múltiples pruebas, y entre múltiples clases, y nos da la posibilidad de agregar estos datos desde variables, no solo valores concretos.

Para usar MemberData, simplemente necesitamos una función o método que regrese un listado tipo IEnumerable<object[]>, incluyendo en cada elementó el arreglo de objetos los valores que recibirá la función de pruebas. Esta propiedad o método puede venir desde la misma clase de las pruebas, o desde una clase externa.

Para crear un Custom Attribute, creamos una clase que herede de DataAttribute. Esto nos obliga a implementar el método abstracto GetData(), el cual regresa un IEnumerable<object[]>. Una vez que implementamos este método podemos llamarlo simplemente agregando nuestra clase como atributo.

Los puntos importantes que recordar:

  • Para usar MemberData:
    • Crear una clase con un método o propiedad que regrese un IEnumerable<object[]>.
    • Agregar el atributo [MemberData] a nuestra prueba con la siguiente forma:
      • [MemberData(nameof(ClaseOrigen.MetodoOrigen), MemberType = typeof(ClaseOrigen))]
    • Para crear un Custom Attribute:
      • Creamos una clase que herede de DataAttribute.
      • Implementamos el método GetData, el cual debe de regresar un IEnumerable<object[]>.
      • Agregar nuestra clase como atributo.
        • [ClaseOrigen]

Deja un comentario

Close Menu