Pruebas Unitarias en .Net Core con xUnit – 6 – Moq y clases sin Interfaz

Pruebas Unitarias en .Net Core con xUnit – 6 – Moq y clases sin Interfaz

Como vimos en los posts anteriores, una vez que cambiamos la dependencia de una clase a una interfaz es muy fácil crear un Mock para simular el objeto que vamos a usar durante la inyección de dependencia con un Mock.

Pero muchas veces dependemos de clases de las cuales no tenemos el control, que no tienen esa interfaz, o cuya interfaz requiere una implementación demasiado complicada y que no hace sentido replicarla. Puedes tomar la opción de implementar Proxys para casos donde no tienes nada de control de la dependencia, pero puede ser tedioso, y además hay casos en los que no podemos hacerlo, o no queremos cambiar las referencias. Un ejemplo muy común es el uso de HttpContext en los controladores MVC.

Para estos casos podemos usar Moq, una librería que nos permite automatizar la creación de Mocks en base a interfaces o clases. Sin tener que hacer la implementación completa en el caso de las interfaces. Y de clases que ofrezcan la posibilidad de hacer override de sus propiedades o métodos.

Moq es lo que se conoce como una Mocking Framework, librerías hechas específicamente para crear Mocks. No es la única, pero es una de las más populares para .Net Core. En este post veremos un ejemplo muy rápido, pero completo, para que puedas comenzar a utilizar esta librería en tus pruebas.

Como todos los posts anteriores, poder utilizar Moq depende primero de que estés utilizando Inyección de Dependencia, si no lo tienes y no tienes claro que es, o cómo hacerlo, te recomiendo que leas primero el post donde explico para que sirve y como funciona:

El Problema:

En este caso vamos a utilizar otra vez al ejemplo de ClienteServices que usamos en posts pasados. En este caso queremos crear pruebas unitarias para ClienteServices, que depende únicamente de ClienteRepository. Imagina que en este caso no tenemos nada de control de ClienteRepository, esto podría ser porque es una librería creada por otro equipó, o porque es una librería externa que agregaste por NuGet. Como sea, o no somos dueños del código, o no tenemos sus fuentes, por lo que no lo podemos modificar para implementar una interfaz.

 
//ClienteService.cs
public class ClienteService
{
    ClienteRepository clienteRepo;

    public ClienteService(ClienteRepository clienteRepository)
    {
        clienteRepo = clienteRepository;
    }

    public Guid IdUltimoCliente 
    {
        get { return clienteRepo.IdUltimoCliente; }
    }

    public Cliente AddCliente(Cliente cliente) 
    {
        if(String.IsNullOrWhiteSpace(cliente.Nombre) || String.IsNullOrWhiteSpace(cliente.Apellido))
            throw new Exception("El nombre o apellido no puede estar vacio.");

        var resp = clienteRepo.Add(cliente);
        return resp;
    }

    public IQueryable<Cliente> GetAll() 
    {
        return clienteRepo.GetAll();
    }
}

Para explicar ClienteService:

  • El constructor recibe la inyección de ClienteRepository que se utiliza en todos los métodos de la clase para interactuar con la Base de Datos.
  • Agregamos una propiedad llamada IdUltimoCliente la cual almacena el Id del ultimo cliente que se agregó. Esto lo saca directamente del ClienteRepository. No pienses mucho en la utilidad de esta propiedad, es solo por motivos del ejemplo.
  • El método AddCliente recibe como parámetro un objeto Cliente, valida sus datos, y utiliza ClienteRepository para guardarlo en la base de datos.
  • El método GetAll simplemente pasa la llamada a ClienteRepository para regresar el listado de todos los clientes.

El problema es que, al momento de probar, los métodos de ClienteRepository arrojan siempre una Excepción, porque tiene dependencias externas, como archivos de configuración o conexión al SQL. Cosas que no tenemos en nuestro proyecto de pruebas unitarias, y que no deben de existir en dicho contexto. Esto evita que podamos probar ClienteService.

La Solución – Moq para Mocks

Moq te permite hacer una clase que encapsula los métodos y propiedades públicas de una clase, e intercepta las llamadas a estos para regresarte un resultado que tu configures, en lugar de llamar al objeto real. Esto lo hace en una de dos maneras.

  • Si lo alimentas con una Interfaz, Moq crea un objeto que la implementa. Esto es sencillo de entender. En vez de implementar tú la interfaz a mano para crear un Mock, lo cual requiere que pongas todas las propiedades y métodos de la interfaz, Moq la implementa de manera automática y responde únicamente a las propiedades y métodos que configures.
  • Si lo alimentas con una clase regular, Moq crea una nueva clase que hereda de la clase original, permitiéndole hacer override de las funciones marcadas como virtual, o abstract. Esto es importante mencionarlo, porque si las propiedades o métodos no son virtual o abstract, no tienes otra opción más que implementar un proxy, como lo hicimos en el ejemplo de las clases estáticas.

Instalación y Uso

Para instalarlo, simplemente lo buscamos en NuGet en nuestro proyecto de pruebas.

Usarlo es tan fácil como seguir estos 3 pasos:

  1. Inicializar el objeto Mock.
    • var mock = new Mock<ObjetoASumular>();
  2. Configurar las propiedades y métodos que interceptará Moq.
    • mock.Setup(x => x.MetodoOPropiedad)
  3. Configurar la respuesta que arrojara el objeto
    • .Return(responseObject);

Simular ClienteRepository.GetAll()

Y para entender cómo usarlo, veremos cómo lo aplicamos primero para probar el método GetAll():

 
[Fact]
public void GetAll_HappyPath()
{
    //Arrange
    var clnRepoMock = new Mock<ClienteRepository>();
    clnRepoMock.Setup(x => x.GetAll()).Returns(
        new List<Cliente>()
        {
            new Cliente()
            {
                IdCliente = Guid.NewGuid(),
                Nombre = "Juan",
                Apellido = "Rulfo",
                EMail = "juan.rulfo@juanrulfo.com",
                EnviarCorreos = true,
            },
            new Cliente()
            {
                IdCliente = Guid.NewGuid(),
                Nombre = "Pedro",
                Apellido = "Paramo",
                EMail = "pedro.paramo@juanrulfo.com",
                EnviarCorreos = true,
            },
        }.AsQueryable());

    var sut = new ClienteService(clnRepoMock.Object);

    //Act
    var resp = sut.GetAll();

    //Assert
    Assert.NotEmpty(resp);
}

Línea 05

Primero, inicializamos el objeto. Esto se hace de manera muy sencilla, simplemente creamos una variable y creamos un nuevo objeto Mock, enviando el objeto que deseamos simular. Al hacerlo se inicializa una copia del objeto real, pero que intercepta las llamadas para regresar lo que tú le configures. Si no configuras una llamada, Moq llamara el método en el objeto original.

Líneas 06 a 25

Para configurarlo, utilizamos la función Setup. En esta función, le indicamos qué función es la que vamos a configurar. En este caso GetAll(). Esto prepara la función para que Moq sepa que tiene que interceptar la llamada. Posteriormente, ya que GetAll regresa información, utilizamos la función Returns para configurar qué vamos a regresar. La función regresa un listado IQueryble de Clientes. Para simularlo, simplemente creamos en listado con objetos en memoria, este simulará el funcionamiento real.

Líneas 27 a 33

Lo demás es simple. Utilizamos este objeto (clnRepoMock) para inyectarlo a ClienteService, utilizando su propiedad Object, que es donde expone el objeto real. De esta manera, al momento que ClienteService haga la llamada a GetAll de ClienteRepository, en vez de ejecutar el método real, interceptara la llamada y regresara el listado que creamos, simulando así la operación de la base de datos. Esto nos permitirá que podamos ejecutar nuestra prueba. El resto es simplemente ejecutar la función a probar y asegurarnos que tenemos el resultado esperado.

Este ejemplo es muy sencillo, pero nos permite entender cómo funciona la configuración en Moq. Para ver un escenario más completo, donde el método tiene parámetros de entrada, un proceso interno, y además se utiliza una propiedad, vamos a ver el ejemplo de cómo configurar la función Add().

Simular ClienteRepository.Add()

Primero veamos la función de prueba:

 
[Fact] 
public void AgregarCliente_HappyPath()
{
    //Arrange
    var idCliente = Guid.NewGuid();
    var clnRepoMock = new Mock<ClienteRepository>();
    clnRepoMock.Setup(x => x.IdUltimoCliente).Returns(idCliente);
    clnRepoMock.Setup(x => x.Add(It.IsAny<Cliente>())).Returns(
        (Cliente cliente) =>
        {
            cliente.IdCliente = idCliente;
            return cliente;
        });

    var sut = new ClienteService(clnRepoMock.Object);
    var cliente = new Cliente() 
    {
        Nombre = "Juan",
        Apellido = "Rulfo",
        EMail = "juan.rulfo@seidor.com",
        EnviarCorreos = true,
    };

    //Act
    var resp = sut.AddCliente(cliente);

    //Assert
    Assert.NotEqual(Guid.Empty, resp.IdCliente);
    Assert.Equal(idCliente, resp.IdCliente);
}

Línea 05

Primero se inicializa un Guid que vamos a asignar al cliente creado. Ya que es ClienteRepository el que se encarga de hacer esto, tenemos que simular esta generación. Lo guardamos en una variable para controlar cual es el Guid asignado.

Línea 06

Lo siguiente es inicializar nuestro objeto Mock. Este lo hacemos exactamente igual que en la función anterior.

Línea 07

Luego configuramos la respuesta de la propiedad IdUltimoCliente, esto lo hacemos igual a la configuración de GetAll() en la prueba anterior. Simplemente indicamos que propiedad, y que valor va a regresar.

Líneas 08 a 13

Y aquí viene el principal cambio. El método Add(), además de recibir el cliente como parámetro de entrada, también regresa el objeto cliente creado en la Base de Datos, con su Guid asignado. Para poder configurar la función Setup, primero tenemos que indicarle que la función Add recibe un objeto tipo cliente, esto lo hacemos utilizando la clase It.IsAny().

Para la respuesta, queremos utilizar la información del objeto recibido, agregarle la propiedad IdCliente, y regresar ese mismo objeto. Esto es un proceso más complejo, por lo que en vez de solo indicar el objeto que vamos a regresar, utilizaremos una expresión Lambda. En esta expresión recibimos primero el cliente recibido, abrimos las llaves y agregamos el Guid al IdCliente, y regresamos el mismo objeto. De esta manera simulamos la inserción a la base de datos, y el regreso del Id creado durante la inserción, finalmente regresamos el objeto ya con toda su información.

Línea 15

Y listo, con esto configuramos la ejecución del código que se va a simular la interacción con ClienteRepository, y por medio de este la interacción con la base de datos. Lo siguiente es inicializar ClienteService, inyectando el Mock que acabamos de crear, crear el cliente, y ejecutar nuestra prueba.

Líneas 25 a 29

Para la prueba validamos que el IdCliente no este vacío (o con puros ceros), y que el idCliente sea el mismo que el Id del Cliente agregado. Esta última validación realmente la incluimos para aprovechar el ejemplo de la propiedad.

Notas Finales

Aunque esto parece ser un tutorial muy sencillo, con lo que acabas de aprender podrás simular casi cualquier comportamiento por parte de tu dependencia. Incluso lo puedes usar para simular clases que tu controlas, ahorrándote el trabajo de tener que crear la interfaz para cada clase. Esto ayuda a evitar el Dependency Injection Hell (ver anotación 1). Solo recuerda que tus métodos y propiedades deben de ser virtual o abstract, para que Moq pueda interceptar las llamadas.

Si estas usando interfaces no importa, también puedes usar Mock en vez de tener que implementar toda la interfaz en tu Mock. Esto puedes decidirlo dependiendo de qué tan complicada sea tu interfaz, o simplemente por tu preferencia personal. Son muchos programadores los que prefieren usar solo Moq.

Moq tiene muchas más funciones, y mucha más funcionalidad, pero con esto es más que suficiente para empezar. Como dato adicional, ya que es muy probable que lo utilices, si tu método es Async, puedes usar ReturnsAsync en vez de Return. Su uso y funcionalidad es exactamente igual. Si quieres dar una leída rápida del resto de las funcione y su uso, puedes revisar la siguiente liga:

Resumen

Muchas veces dependemos de clases de las cuales no tenemos control, ya sea porque es código afuera de nuestro dominio, o librerías externas, pero simular su comportamiento es indispensable para poder probar. Esto lo podemos hacer con una Mocking Framework, siempre que haya una interfaz, o los métodos y propiedades de la clase estén marcados como virtuales o abstractos. Moq es uno de los Mocking Frameworks más populares .Net Core.

Esto Post nos muestra las bases para usar Moq. Con esto tienes todo lo que necesitas para empezarlo a usar de manera efectiva, es cosa de entender los 3 pasos para crear tu Mock. Moq funciona simplemente interceptando las llamadas, regresando la información que tu configures, o ejecutando el código que tu configures. Esto es más que suficiente para empezar a usar Moq de manera efectiva.

El acordeón de este post:

  • Moq puede simular cualquier interfaz o clase. Si le provees una interfaz, crea un objeto en base a esta. Si le provees una clase, crea una nueva clase que hereda de la inicial.
  • Si quieres simular las propiedades o métodos de una clase, estos tienen que estar marcados como virtual o abstract para que la clase que crea Mock pueda hacer un override.
  • Moq funciona interceptando las llamadas a las clases o métodos configurados, y sustituyéndolos por código o respuestas configuradas durante la prueba.
  • Crear un objeto Mock con Moq es muy sencillo, solo tienes que seguir 3 pasos:
    1. Inicializar el objeto Mock.
      • var mock = new Mock();
    2. Configurar las propiedades y métodos que interceptará Moq.
      • mock.Setup(x => x.MetodoOPropiedad).
    3. Configurar la respuesta que arrojara el objeto
      • .Return(responseObject);
  • Cuando tienes parámetros de entrada, debes utilizar la clase It.IsAny() para definir el tipo de objeto a recibir.
  • Si necesitas ejecutar código para simular la operación de tu dependencia, puedes usar una función Lambda dentro de Return().

Anotaciones

  1. Dependency Injection Hell – Cuanto todas las clases en un programa tienen una interfaz es muy difícil navegar el código, ya que cuando usas “Go To Definition”, o F12 en Visual Studio, siempre navegas a la interfaz, por lo que es más complicado encontrar el código que se ejecuta.

Repositorio del proyecto:
https://github.com/dansuarmar/unitTestXunitBasics/tree/main/P5%20-%20API%20Controller%20y%20EF/unitTestXunitBasics

Deja un comentario

Close Menu