Como desarrolladores nuestro objetivo principal es crear software de calidad, confiable y fácil de mantener. Para llegar a este fin es importante asegurarnos de tener nuestra lógica testada con pruebas unitarias, aunque no siempre es fácil cubrir la cantidad de código que nos gustaría.
Conexiones con bases de datos, operaciones contra el sistema de ficheros o interacciones con APIs externas en general pueden hacer más difícil que nuestros test unitarios sean realmente unitarios, ya que añaden una dependencia sobre la que no siempre vamos a tener control.
Por otro lado, dicha dependencia causa a menudo problemas de velocidad de ejecución, lo que hace pesado ejecutar nuestra batería de test.
Una manera de solucionar estos problemas es utilizando “mocks”, que no son más que objetos simulados que imitan el comportamiento de objetos reales.
Crear estos «mocks» a mano puede sonar costoso, y si la complejidad del sistema a testar es alta sin duda que lo es. Por fortuna, existen «frameworks» que simplifican y agilizan esta tarea. En .NET disponemos de muchos «frameworks» (FakeItEasy, JustMock…), pero nosotros nos vamos a centrar en Moq.
Moq
Moq nos ayuda aprovechar toda la potencia de C# para crear “mocks” limpios y mantenibles. La inclusión LINQ y su sintaxis intuitiva hace que sea extremadamente fácil de utilizar y aprovechar en toda su extensión, ayudando a desarrolladores sin conocimientos previos de ténicas de «mocking» a ser productivos desde el minuto uno.
Moq está disponible en NuGET, así que para utilizarlo simplemente tenemos que instalar el paquete del «framework» en nuestro proyecto de test. Para ello, lanzaremos el siguiente comando en la “Package Manager Console” (asegurándonos de tener seleccionado nuestro proyecto de test como «Base Project»:
Install-Package Moq -Version 4.5.30
Esto deja listo e instalado el «framework» y todas sus dependencias, así que ya podemos comenzar a trabajar con él.
Primeros pasos
Para demostrar lo fácil de utilizar que es, vamos a ver un ejemplo simple de cómo crear un objeto «mock» y simular una llamada a uno de sus métodos.
// Creamos el mock sobre nuestra interfaz var mock = new Mock<IFoo>(); // Definimos el comportamiento del método GetCount y su resultado mock.Setup(m => m.GetCount()).Returns(1); // Creamos una instancia del objeto mockeado y la testeamos Assert.AreEqual(1, mock.Object.GetCount());
Como se puede ver, la sintaxis es muy clara y nos permite crear código “fluent” que aprovecha las expresiones lambda con la que todos estamos familiarizados. Sólo es necesario crear el “mock” a partir de la interfaz o la clase que queramos y empezar a definir comportamientos y resultados. Luego, simplemente hacemos una llamada al propio «mock» mediante la propiedad «Object» que nos devuelve una instancia del objeto simulado. Esta instancia se comportará como hayamos definido mediante los «Setup».
Un poco mas en profundidad
También podemos definir comportamientos dependiendo de los parámetros que se le pasen al objeto “mock” e incluso ejecutar acciones complejas accediendo al mismo parámetro proporcionado al método simulado. Por ejemplo:
// Creamos el mock sobre nuestra interfaz var mock = new Mock<IFoo>(); // Definimos el comportamiento del método mock.Setup(m => m.ToUpperCase(It.IsAny<string>())) .Returns((string value) => { return value.ToUpperInvariant(); }); // Definimos un comportamiento específico con parameter-matching mock.Setup(m => m.ToUpperCase("NotOK")).Returns("notok"); // Obtenemos una instancia del objeto mockeado var mockObject = mock.Object; // Comprobamos el comportamiento genérico Assert.AreEqual("OK", mockObject.ToUpperCase("ok")); // Comprobamos que al pasar "NotOK" no lo devolvemos en mayúsculas Assert.AreNotEqual("NOTOK", mock.Object.ToUpperCase("NotOK"));
Mediante «It.IsAny» podemos definir un comportamiento para todas las peticiones cuyo parámetro sea del tipo «T», aunque también podemos especificar parámetros concretos en el mismo contexto. Con esto podemos simular comportamientos inesperados y testar casos difíciles de reproducir en un entorno real.
Moq también nos permite utilizar expresiones lambda, rangos de parámetros e incluso expresiones regulares para filtrar parámetros. Esto nos ayuda a programar «mocks» que sean todo lo complejos que necesitemos y aun así mantener el código limpio y legible.
Por otro lado, es muy fácil especificar que ciertas llamadas a nuestro “mock” lancen una excepción, o incluso definir “callbacks” a la ejecución de un método simulado:
// Podemos definir callbacks de manera muy simple mock.Setup(m => m.ToUpperCase(It.IsAny<string>())) .Returns((string value) => { return value.ToUpperInvariant(); }) .Callback(() => { calls++; }); // Esta línea lanzará la excepción definida arriba Assert.AreEqual("EXCEPTION", mock.Object.ToUpperCase("Exception")); // Llamamos una vez más al método Assert.AreEqual("OK", mock.Object.ToUpperCase("ok")); // Comprobamos que se ha ejecutado el callback Assert.AreEqual(1, calls);
Un ejemplo real
Uno de los casos en los que mejor se comportan este tipo de «frameworks» es en el testeo de aplicaciones N capas. Al desarrollar este tipo de aplicaciones, normalmente utilizamos inyección de dependencias y las interfaces que generamos para esto son un candidato perfecto para la generación de «mocks».
Supongamos que estamos creando una aplicación como las anteriormente descritas y que además utilizamos el patrón «repository». Para evitarnos todos los problemas relacionados con conexiones contra base de datos cuando trabajamos con test unitarios y aun así poder cubrir toda nuestra capa de negocio, podemos utilizar Moq para simular la capa repositorio:
var mockPersonRepository = new Mock<IPersonRepository>(); // Simulamos un comportamiento correcto mockPersonRepository.Setup(m => m.Update(It.IsAny<Person>())).Returns(true); // Simulamos un comportamiento incorrecto mockPersonRepository .Setup(m => m.Create(It.Is<Person>(p => p.Age > 0)).Returns(false); // Creamos una instancia del mock y la inyectamos a la capa superior var personService = new PersonService(mockPersonRepository.Object); // Probamos Assert.IsTrue(personService.Update(new Person())); Assert.IsFalse(personService.Create(new Person { Age = -1 }));
Usando estás técnicas podemos crear test verdaderamente unitarios, reproducibles, sin dependencias de ningún tipo y que realmente prueben la lógica que nos interesa.
En Resumen
Moq es un «framework» muy completo que nos permite lanzarnos al mundo del «mocking» sin prácticamente ningún conocimiento previo.
Pero su simpleza no lo hace quedarse corto ni en características ni en versatilidad. Como siempre, es recomendable leer la documentación para no perdernos nada y aprovecharlo al cien por cien.
Con este conocimiento en nuestro poder ¡ya no hay excusas para no tener la cobertura de código de nuestros test al máximo! ?