Pruebas Unitarias en .Net Core con xUnit – 9 – Tips Adicionales, Mejores Prácticas

Pruebas Unitarias en .Net Core con xUnit – 9 – Tips Adicionales, Mejores Prácticas

Ya para finalizar esta serie de posts de pruebas unitarias (9 en total) pensé en hacer un compilado de detalles sencillos de entender, como una explicación corta de los principales Assert, como verificar que algo envía una Excepción, un tip adicional con Moq, y lo más importante, mi recomendación para el uso de pruebas unitarias en tus proyectos.

En el futuro cubriré mas temas acerca de pruebas unitarias, principalmente el uso de otras librerías mas avanzadas, pero lo que hemos visto en estos posts es lo mas importante que necesitas saber para empezar a hacer pruebas unitarias. Como todo, recuerda que lo más importante es practicar.

Asserts

Assert.Equal

La función más común que vas a utilizar. Te permite comparar 2 valores del mismo tipo, para asegurarte se sean iguales. De no serlo, arrojara una excepción. Puede validar valores tipo string, DateTime, int, decimal, double, etc.

Para los valores tipo Datetime, decimal o double, puedes agregar un tercer valor para ajustar la precisión que deseas comparar.

Assert.NotEqual

La función contraria a Equal, esta verifica que 2 valores del mismo tipo no sean iguales.

Boolean

Creo que nos podemos ahorrar las explicaciones de los siguientes 2.

  • True(bool)
  • False(bool)

Strings

  • Contains(expectedSubString, actualString)
    • Verifica que un string exista dentro de otro string.
    • También existe DoesNotContain.
  • Matches(expectedRegExPattern, actualString)
    • Verifica que un string cumpla con una expresión regular.
    • También existe DoesNotMatch.

Object Instances & Null

  • Null(object)
    • Verifica que el objeto sea null.
  • NotNull(object)
    • Verifica que el objeto no sea null.
  • NotSame(object1, object2);
    • Verifica que los 2 objetos no sean la misma instancia.
  • Same(obj1, obj2);
    • Verifica que los 2 objetos sean la misma instancia.

Lists

  • Contains(T, List<T>)
    • Contains también sirve para verificar que un elemento de cualquier tipo existe en una lista de objetos de dicho tipo.
  • DoesNotContain(T, List<T>).
    • Caso contrario a Contains, verifica que un objeto no exista dentro de una lista.
  • All(lList<T>, predicate).
    • Verifica que todos los elementos de una lista cumplan con una condición definida a partir de un predicado (o Lambda). Esta función es muy útil para verificar resultados en todos los elementos de un listado. Es equivalente a hacer un foreach.
    • Ya que es importante, veamos un ejemplo:
Assert.All(itemList, item =>
{ 
    var expected = Function(item);
    Assert.Equal(item, expected);
});

Types

  • IsType<type>(object);
    • Verifica que el objeto recibido sea de un tipo específico. Esto es muy útil cuando tienes múltiples objetos que heredan de un padre, o de la misma interfaz, pero necesitas asegurarte de que el resultado es uno en específico.
  • IsNotType<type>(object);
    • Función contraria a IsType. Verifica que el objeto no sea de un tipo específico.

Revisar por Excepciones

Muchas veces nuestro código lo programamos para que genere excepciones, ya sean personalizadas o usando excepciones ya existentes. Estos casos pueden ser lógica de negocio importante, por lo que también es importante probar caminos y escenarios que generan dichas excepciones. El problema es que cuando arrojamos una Excepción a XUnit, este asume que la prueba no paso, y nos arroja un resultado de fallido.

Para solucionar esto, XUnit nos provee la función Throws<T>. En esta función debemos de indicar el tipo de Excepción que esperamos, y recibe una función Lambda que ejecutara el método que arroja la excepción. Si no se genera la excepción, o la excepción generada es de otro tipo, la función no pasara la prueba.

De manera opcional, podemos asignar el resultado de esta función a una variable, para cachar la excepción devuelta, por si queremos validar algo más. Veamos el siguiente ejemplo:

[Fact]
public void AgregarMensaje_Excepcion()
{
    //Arrange
    var sut = new MensajesClientesService(msgRepoMock.Object);

    //Act and Assert
    var excp = Assert.Throws<Exception>(() => sut.AddMensaje(Guid.NewGuid(), null, null));
    Assert.Equal("Titulo no puede ser null", excp.Message);
}

Verificar que se llamó un Método con Moq

Siguiente Tip, este aplica a Moq, mas que a XUnit, pero igual es importante ya que te permite validar algo en específico.

Al crear los Mocks de tus dependencias, usando Moq, y realizar el Setup de los métodos que deseas simular, Moq automáticamente lleva un registro de que métodos han sido llamados, y cuantas veces. Esto nos permite ejecutar una validación para asegurarnos que un método fue llamado. Esto es muy útil cuando la lógica de negocio decide entre varias posibilidades de llamada.

Para verificar utilizamos la función Verify, esta aparece como parte de nuestro Mock, y nos permite verificar que la llamada se hizo. Si tu método recibe parámetros, tienes que poner los mismos exactamente ya que Moq guarda referencia a los parámetros recibidos, no basta con mandarle cualquiera. Si necesitas recibir cualquiera, puedes utilizar It.IsAny<T>(). Puedes ver su uso en el ejemplo.

[Fact]
public void AgregarMensaje_HappyPath()
{
    //Arrange
    var idCliente = Guid.NewGuid();
    var titulo = "Bienvenido a nuestro Servicio.";
    var contenido = "Hola. Muchas gracias por usar nuestro servicio.";

    var sut = new MensajesClientesService(msgRepoMock.Object);

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

    //Assert
    msgRepoMock.Verify(mock => mock.Add(It.IsAny<Mensaje>()), Times.Once);
}

Recomendaciones Generales

Hemos visto muchas técnicas y posibilidades para hacer las pruebas unitarias. Pero mi intención es que tengas una técnica muy clara que te puedas llevar y que puedas empezar a aplicar en cualquiera de tus proyectos .Net. Básicamente, un resumen de lo mas importante, de como te recomiendo atacar las pruebas unitarias para que no rompan mucho tu flujo de trabajo.

Lo primero y mas importante es la inyección de dependencias. Aquí no hay mucha opción, es una de las mejores prácticas de desarrollo, es un patrón que te vas a encontrar muchas veces. Domínalo, va a ser tu principal herramienta para crear código con dependencias reducidas.

Y finalmente, tomando en consideración que tengas control de casi todo el código, y casi todas las dependencias, que es la mayoría de los casos, mi recomendación es:

Mocks Manuales en Proyectos Chicos, Moq en más Grandes

No te vayas por el camino de crear toda la infraestructura de tu prueba manualmente. Específicamente, el crear interfaces para cada una de las dependencias, solo para poder crear Mocks manualmente. Esta práctica funciona para proyectos chicos.

En proyectos más grandes te lleva creará un Interface Hell. Exceso de interfases que se usan únicamente para poder probar. El Interface Hell se llama a los problemas de navegación cuando estas usando interfaces.

Normalmente en Visual Studio, cuando estas en tu clase, y quieres ver la dependencia, simplemente pones el cursor sobre un objeto y, das clic en el botón derecho y seleccionas “Go to definition” (o F12 en el teclado). Esto te lleva al archivo de dicha clase. Esto es muy útil para navegar. Pero cuando todo depende de una interfaz, cuando seleccionamos “Go to definition”, nos lleva a la definición de la interfaz, lo cual, en el 99% de los casos no es lo que queremos, esto es el Interface Hell. En general, si una interfaz no tiene más que una implementación, sin contar Mocks, no vale la pena crearla.

Para solucionarlo es mejor usar Moq. Como se menciono en posts anteriores, Moq puede crear un Mock en base a una interfaz, o en base a una clase. Es esta segunda posibilidad la que vamos a aprovechar.

Cuando Moq lo alimentas con una clase, crea una nueva heredando de la original, y luego sustituye los métodos. Para poderlos sustituir estos tienen que estar marcados como virtuales. Así que marca todos los métodos públicos como virtuales. Esto no tendrá efecto en tus funciones, pero te permitirá usarla como referencia para Moq.

Ejemplo

Regresando a los ejemplos anteriores de EnviarMensaje.

Esta vez solo tenemos la dependencia de MensajeRepository, con el fin de simplificar el ejemplo. Ya que vamos a necesitar un Mock de esta clase, marcamos su método Add como virtual.

//MensajeRepository.cs
public class MensajeRepository
{
    public virtual Mensaje Add(Mensaje mensaje)
    {
        //Operaciones a la Base de Datos
        return mensaje;
    }
}

Lo siguiente es simplemente crear un Mock de dicha clase utilizando Moq. Al marcar la función Add como virtual podemos hacer su Setup para simplemente regresar el mensaje que recibimos. Nuestra clase final de pruebas queda así:

//MensajesClientes_Should.cs
public class MensajesClientesService_Should
{
    Mock<MensajeRepository> msgRepoMock;

    public MensajesClientesService_Should() 
    {
        msgRepoMock.Setup(x => x.Add(It.IsAny<Mensaje>())).Returns(
            (Mensaje mensaje) =>
            {
                mensaje.IdMensaje = Guid.NewGuid();
                return mensaje;
            });
    }

    [Fact]
    public void AgregarMensaje_HappyPath()
    {
        //Arrange
        var idCliente = Guid.NewGuid();
        var titulo = "Bienvenido a nuestro Servicio.";
        var contenido = "Hola. Muchas gracias por usar nuestro servicio.";

        var sut = new MensajesClientesService(msgRepoMock.Object);

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

        //Assert
        msgRepoMock.Verify(mock => mock.Add(It.IsAny<Mensaje>()), Times.Once);
        Assert.NotEqual(Guid.Empty, respuesta.IdCliente);
        Assert.Equal(titulo, respuesta.Titulo);
        Assert.Equal(contenido, respuesta.Contenido);
        Assert.Equal(idCliente, respuesta.IdCliente);
        Assert.True(respuesta.Enviado);
    }

    [Fact]
    public void AgregarMensaje_Excepcion()
    {
        //Arrange
        var sut = new MensajesClientesService(msgRepoMock.Object);

        //Act and Assert
        var excp = Assert.Throws<Exception>(() => sut.AddMensaje(Guid.NewGuid(), null, null));
        Assert.Equal("Titulo no puede ser null", excp.Message);
    }
}

Y como puedes ver utilizamos muchos de los puntos vistos en este post. Revisamos que msgRepoMock.Add() haya sido llamado, y verificamos que nos regrese una excepción si el título es null.

Resumen

En este post dimos una explicación muy rápida acerca de los principales Asserts que utilizarás en tus pruebas unitarias. Adicionalmente vimos como validar, no solo los resultados correctos, sino también escenarios donde recibamos excepciones. Finalmente vimos la funcionalidad que nos da Moq de validar que una función fue llamada.

Vimos también lo que te aconsejo para seguir adelante con pruebas unitarias. Usar interfaces únicamente para proyectos muy chicos, y usar Moq y métodos virtuales para proyectos mas grandes. En general, dominar Moq para seguir este segundo camino es igual de importante que dominar xUnit. Recuerda que son librerías con mucho alcance, pero que solo necesitas conocer lo principal para sacarles provecho.

Lo más importante, practica, practica mucho. Practica hasta que las pruebas unitarias sean naturales para ti, y que siempre sean parte de tu proceso de desarrollo.

Con esto llegamos al final de esta serie dedicada a pruebas unitarias. Mi intención no era crear un curso tal cual, creo que cursos hay más que suficientes. Más bien quería enfocarme en exponer como solucionar puntos muy específicos, que casi todos los programadores se encuentran al empezar con pruebas unitarias. Espero te sea útil. Si te interesa que exploremos alguna otra tecnología, o si tienes dudas o comentarios deja un mensaje con confianza, todo lo tomo en cuenta para posts futuros.

Y como acordeón de este post:

  • Aprende tus Asserts.
  • Si quieres verificar que tu código arroja una excepción, puedes utilizar Assert.Throws.
    • Throws<Exception>(() => sut.AddMensaje(Guid.NewGuid(), null, null));
  • Al configurar Moq para simular un metodo, podemos verificar que efectivamente fue llamado:
    • msgRepoMock.Verify(mock => mock.Add(It.IsAny()), Times.Once);
  • Utiliza interfaces para crear Mocks manuales únicamente en proyectos chicos.
  • En proyectos grandes, evita el Interface Hell, mejor marca los métodos de tus dependencias como virtuales para que puedas usar Moq para simularlos.

Deja un comentario

Close Menu