Rompiendo los limites: Mocking en las Pruebas Unitarias .Net

Tiempo de lectura: 7 minutos

MockingHoy por fin os traigo la unión entre las Pruebas Unitarias y la Inyección de Dependencias, el «Mocking«. ¿Que es el «Mocking» te preguntarás?, pues es la técnica utilizada para simular objetos en memoria con la finalidad de poder ejecutar pruebas unitarias.

Esto, es especialmente útil cuando utilizamos recursos externos como bases de datos o servicios de mensajería, o cualquier cosa en general que no queramos o no podemos ejecutar durante las pruebas unitarias.

Sin mas preámbulos, ¡vamos con ello! En primer lugar, he reutilizado el proyecto Entity Framework Core «Code First» para partir de tener el contexto de datos creado. Ademas, he añadido una clase «GeneradorInformes» (la cual cumple el patrón de Inyección de Dependencias en el constructor) y una clase «EmailSender» que implementa la interfaz «IEmailSender»:

Proyecto

 
//IEmailSender.cs
namespace PostMocking.Model
{
    public interface IEmailSender
    {
        bool Enviar(string Destinatario, string Mensaje);
    }
}

//EmailSender.cs
namespace PostMocking.Model
{
    public class EmailSender : IEmailSender
    {
        public bool Enviar(string Destinatario, string Mensaje)
        {
            //{...}
            return true;
        }
    }
}

//GeneradorInformes.cs
using Microsoft.EntityFrameworkCore;
using PostMocking.Data;
using System.Linq;
using System.Text;

namespace PostMocking.Model
{
    public class GeneradorInformes
    {
        //Propiedad con la dependencia
        private IEmailSender emailSender { get; set; }
        private PostMockingDbContext context { get; set; }

        public GeneradorInformes(PostMockingDbContext context, IEmailSender emailSender)
        {
            this.context = context;
            this.emailSender = emailSender;
        }

        public bool GenerarInforme(string NombreProfesor, string Email)
        {
            //Obtenemos mediante LinQ los datos del profesor
            var Profesor = context.Profesores.Where(x => string.Compare(x.Nombre, NombreProfesor, true) == 0)
                                                .Include(x => x.Cursos)
                                                .ThenInclude(x => x.Alumnos)
                                                .FirstOrDefault();
            //En casode no encontrar nada, salimos
            if (Profesor is null)
                return false;

            //Generamos el informe de alumnos y cursos
            StringBuilder sb = new StringBuilder();
            sb.AppendLine($"El profesor {Profesor.Nombre} imparte los siguientes cursos:");
            foreach (var curso in Profesor.Cursos)
            {
                sb.AppendLine($"\t*{curso.Nombre} con los alumnos:");
                foreach (var alumno in curso.Alumnos)
                {
                    sb.AppendLine($"\t\t*{alumno.Nombre}");
                }
            }

            emailSender.Enviar(Email, sb.ToString());
            return true;
        }
    }
}

//Program.cs
using PostMocking.Data;
using PostMocking.Model;
using System;

namespace PostMocking
{
    class Program
    {
        static void Main(string[] args)
        {
            using (PostMockingDbContext context = new PostMockingDbContext())
            {
                EmailSender emailSender = new EmailSender();
                GeneradorInformes generador = new GeneradorInformes(context, emailSender);
                if (generador.GenerarInforme("FixedBuffer", "[email protected]"))
                    Console.WriteLine("Informe enviado con éxito");
                else
                    Console.WriteLine("Problema al enviar el informe");
            }
        }
    }
}

El resumen del funcionamiento básico, es que GeneradorInformes recibe las dependencias del contexto de datos y el servicio de correo, y al llamar al método «GenerarInforme(string,string)» se obtiene el informe del cursos y alumnos del profesor indicado, y se envía al correo indicado. En caso de que el informe se genere correctamente y se envíe, retornamos un true, en caso contrario un false.

Pruebas Unitarias y Mocking

Generaremos un proyecto de pruebas unitarias en nuestra solución y añadimos a nuestro proyecto de pruebas el siguiente paquete:

Moq.Net

O por consola:

PM-> Install-Package Moq -ProjectName «NombreProyectoPruebas»

Este paquete esta alineado con .NetStandard, en concreto .NetStandard 1.3, por lo que se puede utilizar indistintamente en .Net Framework o en .Net Core. Una vez que lo tenemos todo listo, vamos a crear nuestra prueba unitaria para GeneradorInformes.

 
using Microsoft.EntityFrameworkCore;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using PostMocking.Data;
using PostMocking.Model;
using System;
using System.Collections.Generic;
using System.Linq;

namespace PruebasUnitarias
{
    [TestClass]
    public class GeneradorInformesTests
    {
        // Como queremos reutilizar los Mock, los declaramos a nivel de clase
        Mock emailSender;
        Mock DbContext;

        // Creamos los objetos de Mock en el constructor de la clase, para reutilizarlos
        public GeneradorInformesTests()
        {
            // Creamos el mock sobre nuestra interfazde envio de mensajes
            emailSender = new Mock();
            // Ante la llamada a su metodo enviar, retornamos un true, pero ademas serializamos al output del test el informe
            emailSender.Setup(m => m.Enviar(It.IsAny(), It.IsAny()))
                       .Returns((string destinatario, string mensaje) =>
                       {
                           Console.WriteLine(mensaje);
                           return true;
                       });

            // Creamos la coleccion que devolverá nuestra base de datos Mockeada
            var profesor = new Profesor { Nombre = "Jorge Turrado", IdProfesor = 1 };
            var curso = new Curso { IdProfesor = profesor.IdProfesor, Ciudad = "Vitoria", Nombre = "Mocking", Profesor = profesor };
            var alumno = new Alumno { IdCurso = curso.IdCurso, Curso = curso, Nombre = "Andres Garcia" };
            curso.Alumnos.Add(alumno);
            profesor.Cursos.Add(curso);
            var Profesores = new List()
            {
                 profesor
            }.AsQueryable();

            // Creamos el mock para la base de datos
            var mockSet = new Mock>();
            mockSet.As>().Setup(m => m.Provider).Returns(Profesores.Provider);
            mockSet.As>().Setup(m => m.Expression).Returns(Profesores.Expression);
            mockSet.As>().Setup(m => m.ElementType).Returns(Profesores.ElementType);
            mockSet.As>().Setup(m => m.GetEnumerator()).Returns(Profesores.GetEnumerator());

            // Asignamos el mock de la base de datos al contexto
            DbContext = new Mock();
            DbContext.Setup(c => c.Profesores).Returns(mockSet.Object);
        }

        [TestMethod]
        public void GenerarInformeValido()
        {
            // Creamos nuestra clase a testear y le pasamos los objetos mock
            GeneradorInformes generador = new GeneradorInformes(DbContext.Object, emailSender.Object);
            var result = generador.GenerarInforme("Jorge Turrado", "");

            //Comprobamos el resultado
            Assert.AreEqual(true, result, "No se ha podido generar el informe");
        }

        [TestMethod]
        public void GenerarInformeInvalido()
        {
            // Creamos nuestra clase a testear y le pasamos los objetos mock
            GeneradorInformes generador = new GeneradorInformes(DbContext.Object, emailSender.Object);
            var result = generador.GenerarInforme("Pedro Mayo", "");

            //Comprobamos el resultado
            Assert.AreEqual(false, result, "Se ha podido generar el informe");
        }
    }
}

Analicemos la clase de pruebas. En primer lugar, tenemos 2 objetos de tipo "Mock<T>" , declarados a nivel de clase, esto es debido a que vamos a utilizarlos en las 2 pruebas, y así nos ahorramos tener que construirlos 2 veces, aligerando así la carga.

Mock<IEmailSender>

Lo siguiente que tenemos, es el constructor. En él, se inicializan los objetos Mock, vamos de uno en uno:

 
// Creamos el mock sobre nuestra interfazde envio de mensajes
emailSender = new Mock();
// Ante la llamada a su metodo enviar, retornamos un true, pero ademas serializamos al output del test el informe
emailSender.Setup(m => m.Enviar(It.IsAny(), It.IsAny()))
     .Returns((string destinatario, string mensaje) =>
     {
         Console.WriteLine(mensaje);
         return true;
     });

Mediante el método "Setup", le estamos indicando el comportamiento que debe tener cuando llamemos el método Enviar(string,string) de la interfaz. Cabe destacar que "It.IsAny<string>()" esta indicando que este Mock se aplicara ante cualquier entrada de tipo string, pero podríamos indicarle un string concreto, por ejemplo:

 
// Creamos el mock sobre nuestra interfazde envio de mensajes
emailSender = new Mock();
// Ante la llamada a su metodo enviar, retornamos un true, pero ademas serializamos al output del test el informe
emailSender.Setup(m => m.Enviar("FixedBuffer", It.IsAny()))
     .Returns((string destinatario, string mensaje) =>
     {
         Console.WriteLine(mensaje);
         return true;
     });

Con este segundo código, solo aplicaría el Mock si el primer string es "FixedBuffer", pudiendo definir así diferentes comportamientos ante diferentes entradas ya que podemos llamar tantas veces al método Setup como queramos.

Mock<PostMockingDbContext>

En este caso, no vamos a generar generar un comportamiento en concreto, sino que vamos a generar un contexto de datos falso. Lo primero para eso, es crear una colección de datos:

 
var profesor = new Profesor { Nombre = "Jorge Turrado", IdProfesor = 1 };
var curso = new Curso { IdProfesor = profesor.IdProfesor, Ciudad = "Vitoria", Nombre = "Mocking", Profesor = profesor };
var alumno = new Alumno { IdCurso = curso.IdCurso, Curso = curso, Nombre = "Andres Garcia" };
curso.Alumnos.Add(alumno);
profesor.Cursos.Add(curso);
var Profesores = new List()
{
    profesor
}.AsQueryable();

Como se puede ver, simplemente estamos creando la colección que luego convertiremos en el contexto de datos. Una vez que tenemos los datos, vamos a crear el Mocking del DbSet, que como se puede ver, simplemente consiste en relacionar la colección que acabamos de crear con el objeto Mock<DbSet<Profesor>>.

 
var mockSet = new Mock>();
mockSet.As>().Setup(m => m.Provider).Returns(Profesores.Provider);
mockSet.As>().Setup(m => m.Expression).Returns(Profesores.Expression);
mockSet.As>().Setup(m => m.ElementType).Returns(Profesores.ElementType);
mockSet.As>().Setup(m => m.GetEnumerator()).Returns(Profesores.GetEnumerator());

En el caso de necesitar mockear más tablas, solo tendríamos que repetir el proceso con todas las tablas que nos interese. Una vez que hemos acabado de generar todos los datos que tendrá nuestro contexto, asignamos los DbSet mokeados, vamos a crear por fin nuestro "Mock<PostMockingDbContext>":

 
DbContext = new Mock();
DbContext.Setup(c => c.Profesores).Returns(mockSet.Object);

Esto lo conseguimos diciéndole en el Setup que ante un acceso a la tabla "Profesores", devuelva el objeto mock que acabamos de crear.

Una vez que tenemos creados nuestros dos objetos "Mock<T>" para las pruebas, veamos las pruebas:

 
[TestMethod]
public void GenerarInformeValido()
{
    // Creamos nuestra clase a testear y le pasamos los objetos mock
    GeneradorInformes generador = new GeneradorInformes(DbContext.Object, emailSender.Object);
    var result = generador.GenerarInforme("Jorge Turrado", "");

    //Comprobamos el resultado
    Assert.AreEqual(true, result, "No se ha podido generar el informe");
}

[TestMethod]
public void GenerarInformeInvalido()
{
    // Creamos nuestra clase a testear y le pasamos los objetos mock
    GeneradorInformes generador = new GeneradorInformes(DbContext.Object, emailSender.Object);
    var result = generador.GenerarInforme("Pedro Mayo", "");

    //Comprobamos el resultado
    Assert.AreEqual(false, result, "Se ha podido generar el informe");
}

Como se puede ver, el funcionamiento es exactamente igual que sería en nuestro proyecto en producción, pero en vez de pasarle el PostMockingDbContext y EmailSender, las cuales pueden no estar disponibles para las pruebas, le pasamos sus respectivos Mock, de modo que siempre podemos prever el comportamiento, pudiendo hacer pruebas que sean fiables, sin necesidad de que se tenga acceso a recursos externos. Esto es especialmente útil si se emplean herramientas de CI como Travis o AppVeyor, ya que no van a tener acceso a esos recursos. De hecho, os dejo el enlace a una colaboración que hice hace unos meses hablando sobre AppVeyor y la integración continua.

Ademas, como dato adicional, podemos ver la salida de consola del test unitario, donde ademas de saber que se ha ejecutado correctamente, podríamos ver el reporte:

report

Como habitualmente, dejo el enlace de GitHub para descargar el proyecto y poder probarlo, en este caso, desarrollado en .Net Core. Para ampliar información, dejo también la documentación de Moq.Net.

Deja un comentario