Pruebas Unitarias en .Net Core con xUnit – 4 – Proxys y Clases Estáticas

Pruebas Unitarias en .Net Core con xUnit – 4 – Proxys y Clases Estáticas

En el post anterior vimos cómo hacer pruebas unitarias en código legado. La posibilidad de agregar pruebas unitarias a clases ya existentes te puede solucionar la mayoría de los casos. Pero que haces cuando tu dependencia es de una clase estática, o métodos estáticos. En este post veremos cómo podemos solucionar este problema por medio de la implementación de un Proxy.  El proceso es casi idéntico al que vimos el post anterior, por lo que vamos a aprovechar para repasarlo en paralelo y así te ayudara a recordar y practicar la técnica. Igual, si no has leído el post, igual dale una revisada inicial antes de seguir con este:

El Problema

Empezamos con la definición de la clase. Vamos a usar un ejemplo un poco más sencillo que el del post anterior con la finalidad de agilizarlo. En este caso vamos a trabajar con la clase ClienteService. Esta clase tiene un método AddCliente que queremos probar. Esta es una típica clase de servicio donde se juntan las reglas de negocio, y el almacenamiento de los datos. La clase es como sigue:

 
//ClienteService.cs
public class ClienteService
{
    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.");

        if (!ValidadorEmailHelper.EsValido(cliente.EMail))
            throw new Exception("El correo electrónico agregado no es valido.");

        var clienteRepo = new ClienteRepository();

        return clienteRepo.Add(cliente);
    }
}

Primero que nada, entendemos el funcionamiento del método AddCliente que queremos probar. Este funciona de la siguiente manera:

  1. Recibe como parámetro un objeto Cliente que se desea guardar.
  2. Se realiza la validación de nombre y apellido. Simplemente se revisa que no estén vacías. Si lo están se envía un Excepción.
  3. Se realiza la validación del correo electrónico utilizando la clase y el método estático ValidadorEmailHelper.EsValido.
  4. Se inicializa el ClienteRepository.
  5. Se guarda el cliente en la Base de Datos usando ClienteRepository.Add.

Identificar Dependencias y su Funcionamiento

Ya que entendemos el código necesitamos entender las dependencias que tenemos, de las cuales hay 2:

  1. ClienteRepository – Está es una clase normal que se inicializa en el mismo método.
    1. Add() – El método que se utiliza para mandar a guardar el cliente en la Base de Datos. Regresa el cliente con toda su información.
  2. ValidadorEmailHelper – Esta es una clase estática por lo que no se requiere inicializar. Esta es una clase que no podemos cambiar porque no sabemos dónde más se utiliza.
    1. EsValido() – La función que se encarga de validar si el correo es correcto, regresando verdadero o falso.

En este caso, la clase estática es muy sencilla, con la finalidad de simplificar el ejemplo. El problema real es cuando las clases estáticas de las que depende tu código tienen a su vez dependencias de infraestructura, o simplemente no se puede ejecutar en un ambiente de pruebas. En esos casos no podemos hacer nuestra prueba unitaria. Podríamos hacer una Refactoring para tratar de extraerla, pero no lo podemos hacerlo sin estar seguros de que no rompimos algo más.

Recuerda también que nuestra prueba unitaria es para validar ClienteService.Add, no para validar que la clase estática. Lo único que tenemos que entender de la clase estática es en cómo influye en el comportamiento de nuestra clase, para posteriormente poder simular el comportamiento de la clase estática y poder probar. En este ejemplo es tan simple como regresarnos un verdadero o falso.

Extraer las Dependencias a Interfaces

Continuando con el proceso, el siguiente paso es extraer las dependencias a interfaces.

Empezamos con ClienteRepository que es el más sencillo. Primero extraemos la interfaz, para esto la clase ClienteRepository y seleccionamos Edit -> Refactor -> Extract Interface, o usamos el comando el comando “Ctrl + R, Ctrl + I”. Esto nos abre la pantalla para crear la interfaz, damos clic en OK. Se crea la interfaz y agrega la referencia en ClienteRepository. Ambas clases terminan así:

//ClienteRepository.cs
public class ClienteRepository : IClienteRepository
{
    public Cliente Add(Cliente cliente)
    {
        if (cliente.IdCliente == Guid.Empty)
            cliente.IdCliente = Guid.NewGuid();
        //Operaciones para conexión y extracción de la Base de Datos

        return cliente;
    }
}
//IClienteRepository.cs
public interface IClientRepository
{
    Cliente Add(Cliente cliente);
}

Ahora hacemos lo mismo con nuestra clase estática ValidadorEmailHelper. Abrimos y seleccionamos Edit -> Refactor -> Extract Interface, y como puedes ver nos arroja un error que dice:

Y este es el problema. Por su naturaleza, una clase estática tiene una sola instancia, no se puede instanciar. La interfaz solo aplica a clases que puedes instanciar. Por eso una clase estática no puede implementar una interfaz. Incluso si creamos la interfaz a mano y agregamos la referencia en la clase estática, Visual Studio nos muestra este error:

¿Como podemos entonces inyectar la dependencia de la clase estática?

Implementando una clase Proxy

La solución a este problema es implementar una clase Proxy la cual funcione como sustituto de las llamadas a la clase estática. Esta clase nos permitirá sustituir el funcionamiento de la clase estática con una clase que se puede instanciar. Ya que podemos instanciar esta nueva clase, esta puede implementar una Interfaz, lo que nos abre la puerta a tener ya inyección de dependencia.

Lo primero que hacemos es implementar esta clase Proxy. La nombramos siguiendo el estándar de terminar el nombre de la clase con Proxy. En este caso el nombre final es ValidadorEmailHelperProxy. Dentro de esta clase implementaremos los mismos métodos que ofrece la clase estática, con el mismo nombre, los mismos parámetros de entrada, y el mismo tipo de respuesta. Dentro del método de la clase Proxy, utilizaremos la clase estática para realizar la llamada al método correcto, simplemente regresando su respuesta. En este caso la clase sería así:

//ValidadorEmailHelperProxy.cs
public class ValidadorEmailHelperProxy : IValidadorEmailHelperProxy
{
    public bool EsValido(string email)
    {
        return ValidadorEmailHelper.EsValido(email);
    }
}

Y actualizamos la llamada a esta clase en ClienteService. Quedando así:

//ClienteService.cs
public class ClienteService
{
    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 validador = new ValidadorEmailHelperProxy(); //Instanciamos el nuevo Proxy.
        if (!validador.EsValido(cliente.EMail)) 
            throw new Exception("El correo electrónico agregado no es valido.");

        var clienteRepo = new ClienteRepository();

        return clienteRepo.Add(cliente);
    }
}

Y con esta implementación podemos hacer ya lo mismo que con las clases normales, y extraer su Interfaz:

//IValidadorEmailHelperProxy.cs
public interface IValidadorEmailHelperProxy
{
    bool EsValido(string email);
}

Actualizamos las Interfaces en la Clase Raíz

Con esto ya podemos seguir con los pasos normales. Ahora implementamos las interfaces en la clase ClienteService, aprovechando para mover los objetos validador y clienteRepo a variables globales y instanciándolas en el constructor:

//ClienteService.cs
public class ClienteService
{
    IValidadorEmailHelperProxy validador;
    IClientRepository clienteRepo;

    public ClienteService() 
    {
        validador = new ValidadorEmailHelperProxy();
        clienteRepo = new ClienteRepository();
    }

    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.");

        if (!validador.EsValido(cliente.EMail)) 
            throw new Exception("El correo electrónico agregado no es valido.");

        return clienteRepo.Add(cliente);
    }
}

Y listo, con eso podemos pasar al último paso.

Agregamos Constructor para la Inyección de Dependencia

Para finalizar, agregamos un segundo constructor que nos permita hacer la inyección de dependencia, y con esto la posibilidad de crear Mocks para nuestras pruebas unitarias. Nuestra clase termina así:

//ClienteService.cs
public class ClienteService
{
    IValidadorEmailHelperProxy validador;
    IClientRepository clienteRepo;

    public ClienteService()
    {
        validador = new ValidadorEmailHelperProxy();
        clienteRepo = new ClienteRepository();
    }

    public ClienteService(IValidadorEmailHelperProxy validadorMail, IClientRepository clienteRepository) 
    {
        validador = validadorMail;
        clienteRepo = clienteRepository;
    }

    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.");

        if (!validador.EsValido(cliente.EMail)) 
            throw new Exception("El correo electrónico agregado no es valido.");

        return clienteRepo.Add(cliente);
    } 
}

Creamos los Mocks y las Pruebas Unitarias

Ya con las 2 interfaces creadas, y con la posibilidad de inyectar la dependencia. El siguiente paso es crear nuestras pruebas unitarias. Primero agregamos un Mock para cada una de las dependencias, uno para IClienteRepository y otro para ICalidadorEmailHelperProxy.

Para esto, primero creamos una clase. En este caso empezaremos con ClienteRepositoryMock. Una vez creada, Visual Studio nos puede ayudar a implementar las interfaces de una manera más rápida. Simplemente agregamos la referencia a la Interfaz que queremos implementar, no olvides agregar antes el using al Namespace correcto. Visual Studio nos mostrara que hay un error, al seleccionar el icono del foquito para ver las posibles soluciones, seleccionamos Implement Interface.

Esto crea todos los métodos y propiedades que necesita cumplir la clase para respetar la interfaz. Lo único que nos queda hacer es agregar el código a los métodos. Usando este proceso creamos ClienteRepositoryMock y ValidadorEmailHelperProxyMock:

//ClienteRepositoryMock.cs
public class ClienteRepositoryMock : IClientRepository
{
    bool excepcion;

    public ClienteRepositoryMock(bool mandarExcepcion) 
    {
        excepcion = mandarExcepcion;
    }

    public Cliente Add(Cliente cliente)
    {
        if (excepcion)
            throw new Exception("Error al guardar.");

        cliente.IdCliente = Guid.NewGuid();
        return cliente;
    }
}

En este caso ClienteRepositoryMock no presenta ningún escenario complejo. Únicamente regresa el cliente que recibe agregando su ID. Ya que es una clase que depende de la comunicación con otro sistema (la Base de Datos) es posible que genere excepciones, por lo que agregamos al constructor y a la clase la posibilidad de regresar una excepción. Si tu clase controla más de un tipo de excepción, con este método podemos probar, no solo el envío de una excepción genérica, sino la generación de diferentes tipos excepciones con la finalidad de probar todos los comportamientos.

//ValidadorEmailHelperProxyMock.cs
public class ValidadorEmailHelperProxyMock : IValidadorEmailHelperProxy
{
    bool respuesta;

    public ValidadorEmailHelperProxyMock(bool queRegresar) 
    {
        respuesta = queRegresar;
    }

    public bool EsValido(string email)
    {
        return respuesta;
    }
}

Para ValidadorEmailHelperProxyMock implementamos la posibilidad de regresar true o false, con la intención de probar todas las posibilidades que tiene la clase estática. El Mock no tiene que hacer todo el proceso de ser estático y generar un proxy, ya que lo único que tiene que hacer es simular los comportamientos posibles. Si la clase estática tiene la posibilidad de generar excepciones, no olvides también agregar esta opción.

Finalmente creamos nuestra prueba unitaria. En este caso solo voy a dejar el Happy Path, pero tú no olvides hacer pruebas para todos los escenarios adicionales. Incluso puedes utilizar este ejemplo para practicar crear las pruebas de los escenarios faltantes. La prueba unitaria termina así:

//MensajesClientes_Should.cs
public class ClientesService_Should
{
    [Fact]
    public void AgregarCliente_HappyPath()
    {
        //Arrange
        var emailValidador = new ValidadorEmailHelperProxyMock(true);
        var clnRepo = new ClienteRepositoryMock(false);

        var cliente = new Cliente() 
        {
            Nombre = "Pedro",
            Apellido = "Paramo",
            EMail = "pedro.paramo@juanrulfo.com",
        };

        var sut = new ClienteService(emailValidador, clnRepo);

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

        //Assert
        Assert.NotEqual(respuesta.IdCliente, Guid.Empty);
    }
}

Resumen

Cuando tienes una clase estática no es tan fácil extraer su dependencia ya que no puedes simplemente extraer su interfaz y sustituirla. En estos casos es necesario agregar una clase adicional que funcionará como Proxy a la clase estática. Esta clase Proxy nos permite extraer su Interfaz y realizar la inyección de dependencia, pasando el funcionamiento normal a la clase estática, pero permitiéndonos sustituirla con un Mock para las pruebas. Es solo un paso adicional sobre la técnica que del post pasado. Una vez que tu clase depende de la clase Proxy y de su Interfaz, agregar un Mock para poder crear las pruebas unitarias es muy sencillo.

Este Post fue algo repetitivo, pero nos permitió repasar el proceso para extraer las dependencias y poder inyectarlas. Créeme, este es un patrón que utilizaras mucho como programador, no importando que lenguaje uses.

La siguiente semana veremos un caso muy específico, las pruebas unitarias de controladores de API, usando Entity Framework. Por lo pronto te dejo con el acordeón.

  • Una clase estática no puede implementar una Interfaz.
  • Cuando tienes una dependencia de una clase estática, la manera de quitar la dependencia es creando una clase Proxy que no sea estática, pero que dentro de sus métodos utilice la clase estática para funcionar.
  • Una vez que tienes una clase Proxy, puedes extraer la interfaz de está, y continuar el proceso de inyección de dependencia.
  • Ya que tienes una interfaz, es muy fácil hacer un Mock para poder probar.
  • Practica, practica, practica.

This Post Has One Comment

Deja un comentario

Close Menu