Pruebas unitarias en .Net Core con xUnit – 3 – Probando Código Legado

Pruebas unitarias en .Net Core con xUnit – 3 – Probando Código Legado

En los posts anteriores vimos como comenzar a hacer Pruebas Unitarias, y como implementar Inyección de Dependencia para desacoplar clases y poder probarlas.  Implementar pruebas en código nuevo funciona muy bien, y es muy rápido ya que tienes la experiencia y la práctica de hacerlo, por lo cual es importante que lo aprendas, practiques, y te acostumbres a hacerlas.

Si no leíste los posts anteriores, y principalmente, si no te queda claro como implementar Inyección de Dependencia en una clase, primero lee el post anterior:

Pero la realidad es que no siempre estamos haciendo código nuevo. Una gran parte de nuestro trabajo es dar mantenimiento y agregar funcionalidad a código ya existente, ya sea código que escribimos nosotros anteriormente, o código que hizo alguien más pero ahora somos responsables de lo que hace.

Es en estos casos donde las pruebas unitarias parecen no ser tan fáciles de implementar, sin embargo, es también en estos casos en los que son más importantes porque:

  • ¿Como podemos hacer cambios en el código sin saber que lo rompimos?
  • ¿Como podemos agregar nueva funcionalidad y estar seguros de que lo demás sigue funcionando?

Cuando tenemos código legado que necesitamos modificar, es aún más importante tener pruebas unitarias.

En estos casos las pruebas unitarias aumentan su valor y se vuelven indispensables. Y veras que implementarlas no es tan difícil como parece, únicamente utilizando lo que aprendimos en los Posts anteriores. Específicamente, veremos el caso donde no tenemos acceso a todo el código, por lo que existe la posibilidad de que haya código afuera de nuestro control que dependa de la clase que queremos probar. Con esto aprenderemos a implementar las pruebas unitarias, sin modificar la firma de nuestra clase (ver anotación 1).

Esta técnica también te ayuda, aunque tengas control completo del código. Es más fácil ir agregando pruebas unitarias de una clase en una clase, sin tener que modificar el resto del código. Con esta técnica en poco tiempo podrás estar probado todo tu código, sin arriesgarte a romper algo.

El Problema

Esta vez tenemos una clase con el nombre MensajeClienteService la cual se utiliza para enviar un mensaje a un cliente, y luego guardarlo en un historial. Esta clase es un ejemplo típico de una arquitectura sencilla, donde la lógica de negocio está contenida en clases tipo Servicio, que utilizan el resto de las capas para ejecutar la funcionalidad. La clase es la como sigue:

//MensajesClientesService.cs
public Mensaje AddMensaje(Guid IdCliente, string Titulo, string Contenido) 
{
    var msgRes = new Mensaje()
    {
        IdCliente = IdCliente,
        Titulo = Titulo,
        Contenido = Contenido,
        IdMensaje = Guid.NewGuid(),
        FechaMensaje = DateTime.Now,
    };

    var clienteService = new ClienteService();
    var cliente = clienteService.GetCliente(IdCliente);
    msgRes.EMailCliente = cliente.EMail;

    if (cliente.EnviarCorreos)
    {
        try
        {
            var servicioCorreos = new CorreosService();
            servicioCorreos.Send(msgRes.EMailCliente, msgRes.Titulo, msgRes.Contenido);
            msgRes.Enviado = true;
        }
        catch
        {
            msgRes.Enviado = false;
        }
    }
    else
        msgRes.Enviado = false;

    var mensajesRepository = new MensajeRepository();
    mensajesRepository.Add(msgRes);

    return msgRes;
}

Para explicarla un poco el código:

  1. La clase recibe el Id de Cliente al que se le va a mandar el mensaje, el título del mensaje y su contenido.
  2. Lo primero que hace la clase es crear el objeto Mensaje con la información recibida. Esta es la entidad que se guardará en la Base de Datos. Se asignan ciertas cosas en código, como el Id del mensaje y la fecha de creación.
  3. Ya que se necesita el email del cliente, y si este tiene configurado recibir correos, se inicializa ClienteService, y se recupera la entidad completa de cliente. Posteriormente se asigna el correo del cliente.
  4. En base a la configuración del cliente. Se decide si se le envía el correo o no.
    1. Si se va a mandar el correo. Se inicializa la clase CorreosService y se envía el correo.
    2. Si hay una excepción al enviar el correo, o el cliente tiene configurado que no se le envíen correos, la propiedad Enviado del mensaje se pone como false.
  5. Se inicializa MensajeRepository, esta clase se utiliza para acceder a las funciones de la base de datos utilizando el patrón Repositorio (Repository Pattern). Se agrega el mensaje a la Base de Datos con la función Add.
  6. Finalmente se regresa el mensaje completo.

En si es una clase muy sencilla, aunque no respeta la idea de la unidad de código. Es una clase que definitivamente requiere un Refactoring, pero para hacerlo necesitamos primero pruebas unitarias que nos aseguren que nuestros cambios no rompen el funcionamiento de la clase. Adicionalmente, no queremos mover la firma de la clase para no tener que modificar ninguna de las clases que dependen de MensajesClientesService. Entonces; ¿Como podemos agregarle pruebas unitarias?

Identificar Dependencias y su Funcionamiento

El primer paso es identificar porque tenemos estas dependencias y para que se usan. Recuerda que toda clase externa es una dependencia. Básicamente cada vez que ves la palabra new, estas creando una dependencia. La intención es detectar todas estas clases, y todas las funciones o propiedades que se usan de dichas clases. En este caso tenemos:

  • ClienteService
    • GetCliente() – Recupera la información del cliente al que se le va a enviar el mensaje. El cliente se utiliza para conocer su correo electrónico y para tener la configuración acerca de si se le pueden o no enviar correos.
  • CorreosService
    • Send() – Se utiliza para enviar el correo electrónico con el mensaje. Si hubo algún problema regresa una Excepción.
  • MensajeRepsitory
    • Add() – Es la clase con la que guardamos el mensaje en la Base de Datos.

Con esta información podemos pasar al siguiente paso.

Extraer las Dependencias a Interfaces

Muy bien, ya conocemos nuestras dependencias, lo siguiente es crear las interfaces para poder extraerlas, y posteriormente inyectarlas como dependencias.

En el post pasado creamos la Interfaz a mano, pero ahora utilizamos una de las funciones de Refactoring dentro de Visual Studio. Hagamos el ejemplo con ClienteService. Abrimos el archivo ClienteService.cs, ponemos el cursor sobre la clase, seleccionamos Edit -> Refactor -> Extract Interface. También podemos usar el comando “Ctrl + R, Ctrl + I”. Esto nos muestra la siguiente pantalla:

Esta pantalla nos ayuda a crear una Interfaz. Automáticamente pone el nombre IClienteService, y nos deja seleccionar que métodos y propiedades que se van a extraer. Simplemente la damos OK y esto nos crea la Interfaz y también agrega su referencia en ClienteService. La Interfaz queda así:

//IClienteService.cs
public interface IClienteService
{
    Cliente GetCliente(Guid IdCliente);
}

Y ClienteService queda así:

//ClienteService.cs
public class ClienteService : IClienteService
{
    public Cliente GetCliente(Guid IdCliente)
    {
        //Código para recuperar el cliente del repositorio.
    }
}

Repetimos los mismos pasos con todas las dependencias. Al final deberíamos tener 3 interfaces. IClienteService, ICorreosService y IMensajeRepository.

Actualizamos las Interfaces en la Clase Raíz

Ahora actualizamos MensajesClientesService para utilizar las interfaces. Después de repetir este mismo proceso para las 3 dependencias, nuestra clase MensajesClientesService queda así:

//MensajesClientesService.cs
public class MensajesClientesService
{
    IClienteService clienteService;
    ICorreosService correosService;
    IMensajeRepository mensajesRepository;

    public MensajesClientesService() 
    {
        clienteService = new ClienteService();
        correosService = new CorreosService();
        mensajesRepository = new MensajeRepository();
    }

    public Mensaje AddMensaje(Guid IdCliente, string Titulo, string Contenido) 
    {
        var msgRes = new Mensaje()
        {
            IdCliente = IdCliente,
            Titulo = Titulo,
            Contenido = Contenido,
            IdMensaje = Guid.NewGuid(),
            FechaMensaje = DateTime.Now,
        };

        //var clienteService = new ClienteService();
        var cliente = clienteService.GetCliente(IdCliente);
        msgRes.EMailCliente = cliente.EMail;

        if (cliente.EnviarCorreos)
        {
            //var servicioCorreos = new CorreosService();
            try
            {
                correosService.Send(msgRes.EMailCliente, msgRes.Titulo, msgRes.Contenido);
                msgRes.Enviado = true;
            }
            catch 
            {
                msgRes.Enviado = false;
            }
        }
        else
            msgRes.Enviado = false;

        //var mensajesRepository = new MensajeRepository();
        mensajesRepository.Add(msgRes);

        return msgRes;
    }
}

Los cambios importantes en este código son:

  • Se crean para los objetos como globales, en base a las interfaces, en vez de las clases.
  • Agregamos el constructor e inicializamos los objetos, ahora si con las clases reales.
  • Comentamos la inicialización de las dependencias en el método AddMensaje. Se comentó para que puedas ver lo que estamos cambiando, pero en realidad estas líneas habría que borrarlas.

Agregamos Constructor para Inyección de Dependencia

El último paso es poder implementar la Inyección de Dependencia. En el post anterior lo hicimos agregando la inyección dentro del constructor de la clase. En este caso esta opción no es posible ya que no queremos modificar ningún código que dependa de MensajesClientesService. Para esto aprovechamos que podemos tener más de 1 constructor, e implementamos un segundo que nos permita inyectar la dependencia. De esta manera, todo el código que actualmente utiliza MensajesClientesService seguirá utilizando el constructor vacío, y las pruebas unitarias podrán usar el constructor que permite inyectar las dependencias. Este segundo constructor termina así:

//MensajesClientesService.cs - Solo segundo Constructor.
public MensajesClientesService(IClienteService clnSrv, ICorreosService corSrv, IMensajeRepository msgRep) 
{
    clienteService = clnSrv;
    correosService = corSrv;
    mensajesRepository = msgRep;
}

Creamos los Mocks y las Pruebas Unitarias

Con esto, finalmente podemos crear los Mocks y las Pruebas Unitarias que necesitemos. En nuestro proyecto de pruebas creamos un Mock para cada una de las dependencias:

//ClienteServiceMock.cs
public class ClienteServiceMock : IClienteService
{
    bool _enviarCorreoConf;

    public ClienteServiceMock(bool enviarCorreoConf) 
    {
        _enviarCorreoConf = enviarCorreoConf;
    }

    public Cliente GetCliente(Guid IdCliente)
    {
        return new Cliente() 
        {
            IdCliente = IdCliente,
            EMail = "pedro.paramo@juanrulfo.com",
            Nombre = "Pedro",
            Apellido = "Paramo",
            EnviarCorreos = _enviarCorreoConf,
        };
    }
}
//CorreosServiceMock.cs
public class CorreosServiceMock : ICorreosService
{
    bool _correoEnviado;

    public CorreosServiceMock(bool simularCorreoEnviado) 
    {
        _correoEnviado = simularCorreoEnviado;
    }

    public void Send(string Emails, string Titulo, string Mensaje)
    {
        if (!_correoEnviado)
            throw new Exception("Error al enviar correo.");
    }
}
//MensajeRepositoryMock.cs
public class MensajeRepositoryMock : IMensajeRepository
{
    public Mensaje Add(Mensaje mensaje)
    {
        return mensaje;
    }
}

En general son Mocks muy sencillos. Pero recuerda que puedes agregar comportamientos adicionales para probar diferentes funcionalidades o flujos. Como ejemplos:

  • En ClienteServiceMock, en el constructor agregamos un parámetro booleano, para modificar la configuración de correos del cliente, manipulando así la propiedad cliente.EnviarCorreos. De esta manera podemos probar comportamientos adicionales de la clase, como evitar que el mensaje sea “enviado” por correo dependiendo de la configuración del cliente.
  • En CorreosServiceMock implementamos algo similar para controlar la respuesta de la función Send(). Si inicializamos el Mock enviando true al parámetro simularCorreoEnviado, funcionara de manera normal. Pero si enviamos false, el método enviará una excepción, simulando que hubo un problema con el servicio de correos.

Con esta información ya podemos crear nuestra primera prueba. En este caso se llama AgregarMensaje_HappyPath(), ya que prueba el camino más sencillo, que es que el cliente tenga su propiedad EnviarCorreos como true, y que CorreosService no mande ningún tipo de Excepción.

//MenajesClientesService_Should.cs - Solo la función AgregarMensaje_HappyPath().
[Fact]
public void AgregarMensaje_HappyPath()
{
    //Arrange
    var clnSrvMock = new ClienteServiceMock(true);
    var corSrvMock = new CorreosServiceMock(true);
    var msgRepo = new MensajeRepositoryMock();

    var idCliente = Guid.NewGuid();
    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(idCliente, titulo, contenido);

    //Assert
    Assert.Equal(titulo, respuesta.Titulo);
    Assert.Equal(contenido, respuesta.Contenido);
    Assert.Equal(idCliente, respuesta.IdCliente);
    Assert.NotEmpty(respuesta.EMailCliente);
    Assert.True(respuesta.Enviado);
}

Agregamos Pruebas para Escenarios Adicionales

Muy bien. Ya para terminar. Nuestra prueba unitaria AgregarMensaje_HappyPath() valida solo el escenario más sencillo, pero la clase tiene otros caminos que puede tomar dependiendo de la configuración de correos del cliente, o si el servicio de correos arroja una excepción. Es importante que tus pruebas unitarias cubran todas estas posibilidades para que estés seguro de que al modificar algo no rompas el funcionamiento de la clase. De esta manera, agregamos 2 pruebas unitarias adicionales:

La primera, simula el escenario donde el Cliente tiene configurado que, si se le envíen correos, pero CorreosService nos arroja una excepción, esto evitaría que el mensaje sea enviado de manera correcta. Para esto validamos que el mensaje de respuesta tenga la propiedad Enviado igual a false:

//MenajesClientesService_Should.cs - Solo la función AgregarMensaje_ExcepcionCorreos().
[Fact]
public void AgregarMensaje_ExcepcionCorreos()
{
    //Arrange
    var clnSrvMock = new ClienteServiceMock(true);
    var corSrvMock = new CorreosServiceMock(false);
    var msgRepo = new MensajeRepositoryMock();

    var idCliente = Guid.NewGuid();
    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(idCliente, titulo, contenido);

    //Assert
    Assert.False(respuesta.Enviado);
}

La segunda prueba simula el escenario donde el Cliente tiene configurado el no recibir correos. Igual simplemente validamos que la propiedad Enviado sea False.

//MenajesClientesService_Should.cs - Solo la función AgregarMensaje_ClienteNoCorreos().
[Fact]
public void AgregarMensaje_ClienteNoCorreos()
{
    //Arrange
    var clnSrvMock = new ClienteServiceMock(true);
    var corSrvMock = new CorreosServiceMock(false);
    var msgRepo = new MensajeRepositoryMock();

    var idCliente = Guid.NewGuid();
    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(idCliente, titulo, contenido);

    //Assert
    Assert.False(respuesta.Enviado);
}

Con esto ya tenemos las Pruebas Unitarias que necesitamos para asegurar el comportamiento correcto de la clase. Ahora si podemos empezar a hacer Refactoring o agregar funcionalidad con la tranquilidad de que las pruebas nos ayudarán a no romper nada.

Resumen

En este Post vimos como implementar pruebas unitarias en código legado. Específicamente vimos un ejemplo de una clase con múltiples dependencias, e hicimos el proceso necesario para poder extraer esas dependencias a interfaces, y poderlas inyectar en un segundo constructor, respetando así la firma original de la clase y asegurando así que las clases dependientes no requieran cambios. Con las interfaces pudimos crear Mocks que simulaban el comportamiento de las dependencias, y con estos Mocks creamos pruebas unitarias para todos los flujos de la clase.

Todo este proceso no dejo con las pruebas unitarias necesarias para hacer Refactoring de la clase, o agregar funcionalidad, estando tranquilos de que no rompimos funcionalidad ya existente. El proceso puede parecer largo, aunque realmente no es tan complicado, es cosa de entender bien lo que estamos haciendo. Si ves la cantidad de código que agregamos no es mucha, y su complejidad es baja. Pero ahora tenemos el valor y la tranquilidad de tener las pruebas. Finalmente recuerda practicar, y en muy poco tiempo podrás ejecutar los pasos sin tenerlo que pensar dos veces.

Nuestro acordeón para el Post:

  • Cuando tenemos código que necesitamos modificar, es aún más importante tener pruebas unitarias para estar seguros de que nuestras modificaciones no rompen la funcionalidad existente.
  • Podemos agregar pruebas unitarias al código legado con los siguientes pasos:
    • Identificar las dependencias y su funcionamiento.
    • Extraer las dependencias e interfaces.
    • Actualizar las dependencias de la clase a las interfaces creadas, asegurándonos que estén en variables globales, y que se inicialicen en el constructor original.
    • Agregar un segundo constructor que nos permita inyectar dichas dependencias.
    • Crear Mocks que simulen la funcionalidad de dichas dependencias, con opciones para manipular las diferentes opciones o caminos que necesita la clase original.
    • Implementar las pruebas unitarias con estos Mocks para validar todos los comportamientos de la clase.
  • Y finalmente, Practicar, Practica y Practicar.

Anotaciones:

  1. Firma de la Clase: Son todos los miembros públicos a los que tienen acceso otros objetos que dependen de nuestra clase. Cosas como el constructor, métodos o propiedades creadas como public.

This Post Has One Comment

Deja un comentario

Close Menu