Mock en TET UNITARIOS C# - Trabajando con dependencias

1 - Qué es mock en C#

En este post vamos a ver una particularidad algo ya más avanzada, dentro de los test,  y esta va a ser hacer un mock de las interfaces que acceden a datos o a terceros,para así poder testear nuestros procesos correctamente. 

El término mock, técnicamente en castellano significa burlarse, y en este contexto no tiene sentido, pero uno de sus sinónimos es “mimic” el cual siginfica mímica, que es lo mismo que imitar.

Por lo tanto, en este post, vamos a tratar el cómo imitar esas interfaces que nos dan acceso a los datos, para así poder testear el flujo del programa. 

 

2 - Definición del sistema

Para este ejemplo he reciclado parte de una entrevista que hice hace un tiempo, en la cual se me pedía crear una aplicación la cual fuera una api, que tuviera dos endpoints, uno para insertar valores en una base de datos y el otro para leer. 

Aquí podemos ver un diagrama como va a ser la aplicación al final. 

Si vamos más cerca del código tenemos diferentes partes:

  • API
  • Capa de Dominio
  • Capa de acceso a datos
  • Dtos
  • Tests

Nuestro Mock va a estar simulando la interfaz que tenemos en la capa de acceso a datos. 

Esto quiere decir, que todo el código del dominio, osea el que contiene la lógica, a excepción de la propia inserción en la base de datos, va a ser testeado. 

La parte del código principal que disponemos es la siguiente:

  • Primero el DTO el cual contiene el objeto Articulo
public class Articulo
{
    public int Id { get; set; }
    public string Contenido { get; set; }
    public string Titulo { get; set; }
    public DateTime Fecha { get; set; }
    public int AutorId { get; set; }
}
  • Disponemos de nuestra API con dos endpoints los cuales únicamente llaman al dominio (service) que vamos a testear.  
public class ArticulosController : ApiController
{
    private readonly ArticulosServicio _articulosServicio;
    public ArticulosController(ArticulosServicio articulosServicio)
    {
        _articulosServicio = articulosServicio;
    }

    [HttpPost]
    [Route("Articulo")]
    public int InsertarArticulo(Articulo articulo)
    {
        var resultado = _articulosServicio.InsertarArticulo(contenido, titulo, autor);

        return resultado;
    }

    [HttpGet]
    [Route("Articulo/{id}")]
    public Articulo ConsultarArticulo(int id)
    {
        var resultado = _articulosServicio.ConsultarArticulo(id);

        return resultado;
    }
}

Como vemos disponemos de un endpoint para insertar artículos y otro para leer un artículo en concreto, pero esta funcionalidad NO esta dentro de nuestro unit test.  

Nota: todo el código es síncrono, para una mejor eficiencia del mismo, deberemos utilizar las llamadas asíncronas, pero ese apartado lo veremos más adelante. 

  • La capa de dominio, la cual contiene la lógica y es la parte que vamos a testear: 
namespace Dominio.Service
{
    public class ArticulosServicio
    {

        private readonly IAutorRepository autorRepository;
        private readonly IArticulosRepository articuloRepository;

        public Articulo InsertarArticulo(string contenido, string titulo, int autorId)
        {
            if (!autorRepository.AutorValido(autorId))
            {
                throw new Exception("Autor not valido");
            }
            
            
            var articuloId = articuloRepository.InsertarArticulo(contenido, titulo, autorId);

            return ConsultarArticulo(articuloId);
        }

        public Articulo ConsultarArticulo(int id)
        {
            return articuloRepository.GetArticulo(id);
        }

    }
}
  • La capa de acceso a datos con las interfaces y los repositorios:
public interface IAutorRepository
{
    Autor GetAutor(int id);
    bool AutorValido(int id);
}
public interface IArticulosRepository
{
    int InsertarArticulo(string contenido, string titulo, int autorId);
    Articulo GetArticulo(int id);
}
public class AutorRepository : IAutorRepository
{

    public bool AutorValido(int id)
    {
        throw new NotImplementedException();
    }

    public Autor GetAutor(int id)
    {
        throw new NotImplementedException();
    }
}
public class ArticulosRepository : IArticulosRepository
{
    public Articulo GetArticulo(int id)
    {
        throw new NotImplementedException();
    }

    public int InsertarArticulo(string contenido, string titulo, int autorId)
    {
        throw new NotImplementedException();
    }
}

Como vemos, los repositorios NO estan implementados, por lo que si intentamos acceder a ellos, nos saltará una excepción. 

 

3  - Por qué Simular código utilizando Mock en unit test

Este punto es muy importante a tenerlo claro, y es que la definición de unit test, indica que debemos testear los métodos o procesos individualmente. 

Pero claro, es posible que, esos procesos llamen a servicios externos o a la propia base de datos. 

En caso de que queramos probar también la inserción en la base de datos deberemos realizar test de integración, que como recordamos del post anterior son mucho más complicados de hacer y largos de ejecutar. 

 

4 - Instalación de una librería mock

Para implementar mock en nuestro código de test, debemos utilizar cualquiera de las librerías ya existentes, ya que no viene ninguna por defecto con el sistema. 

Para ello, vamos a los nuget package e instalamos Moq

o en la consola ejecutamos Install-Package Moq .

Con este sencillo paso ya podremos simular nuestra llamada al repositorio. 

 

5 - Uso de la librería mock

Primero de todo debemos tener claro cómo simular nuestras llamadas a los repositorios. Por lo que debemos declarar las interfaces que vamos a simular con la libreria moq.

Mock<IArticulosRepository> articuloRepo = new Mock<IArticulosRepository>();
Mock<IAutorRepository> autorRepo = new Mock<IAutorRepository>();

Para ello utilizaremos el método .Setup() dentro de nuestro mock.

autorRepo.Setup(a => a.AutorValido(autorId)).

Como vemos debemos indicar que método  son los que vamos a simular, debemos indicar también los parámetros que va a recibir el método. en este caso tenemos múltiples opciones. 

  1. Podemos utilizar la clase It.IsAny<T>()  el cual significa que acepta cualquier objeto de ese tipo.
    • autorRepo.Setup(a => a.AutorValido(It.IsAny<int>()))
  2. Podemos especificar un objeto en concreto, lo que quiere decir que el objeto que llegue a ese método deberá contener esos valores.
    • autorRepo.Setup(a => a.AutorValido(autorId)).

Finalmente indicamos con el método .Returns el resultado de la llamada, este resultado es un objeto, del tipo el cual debe devolver la función, y debemos crearlo en nuestra clase test. 

autorRepo.Setup(a => a.AutorValido(It.IsAny<int>())).Returns(true);

articuloRepo.Setup(a => a.InsertarArticulo(contenido, titulo, autorId)).Returns(1);
articuloRepo.Setup(a => a.GetArticulo(1)).Returns(new Articulo()
{
    Autor = new Autor()
    {
        AutorId = autorId,
        Nombre = "test"
    },
    Contenido = contenido,
    Fecha = DateTime.UtcNow,
    Id = 1,
    Titulo = titulo
});

Para pasar el objeto simulado que hemos creado debemos enviar el mock a la clase que vamos a utilizar, para ello utilizaremos la propiedad mock.Object.

ArticulosServicio servicio = new ArticulosServicio(articuloRepo.Object, autorRepo.Object);

Como podemos observar, mock.Object es del tipo del cual estamos creando la simulación. 

Si ejecutamos el test, y hacemos debug, vemos que si paramos el código, después de la llamada al repositorio este nos devuelve el valor que hemos indicado

FInalmente debemos verificar que nuestra llamada ha sido ejecutada, ya que se supone que solo hacemos mock de lo que necesitamos.

Para ello ejecutamos el método .Verify() el cual comprueba, con los parámetros que incluimos dentro del delegado, si estamos llamando de la forma adecuada. 

Assert.AreEqual(autorId, articulo.Autor.AutorId);
articuloRepo.Verify(a => a.GetArticulo(1));
articuloRepo.Setup(a => a.InsertarArticulo(contenido, titulo, autorId));
autorRepo.Setup(a => a.AutorValido(It.IsAny<int>()));

Conclusión

  • En este post hemos visto que es y cómo utilizar mock/moq. 
  • El uso de mock es esencial en nuestro dia a dia laboral, ya que necesitaremos de esta funcionalidad para simular las llamadas a nuestras dependencias. 
  • Los métodos y propiedades clave son:
    1. El método .Setup() con su .Return()
    2. La propiedad .Object
    3. El método .Verify() que comprobara si la llamada ha sido ejecutada.