Kabel obtiene la especialización avanzada de Microsoft en Modernización de Aplicaciones Web, sobre la competencia Gold Cloud Platform
25 noviembre, 2019
LAT Escuzar - Lachar
Cuerva mejora su productividad gracias al uso inteligente de los datos.
11 diciembre, 2019

En el post anterior habíamos presentado un problema simple (obtener la predicción del tiempo para un evento deportivo). Esto se resolvía mediante un servicio que consultaba una API externa para obtener los datos que necesitamos (la predicción).

Habíamos llegado al punto de lograr aislar nuestra dependencia de esa API externa en nuestras pruebas mediante mocks que escribíamos nosotros mismos. Pero también comentábamos que esa solución no era la ideal si nuestra aplicación crecía, debido a la dificultad de escribir y mantener estos mocks, que supondrían una cantidad importante de código. Y habíamos hecho la pregunta; ¿Cómo resolvemos esto? Aquí está la respuesta.

Las clases que no implementen interfaces, que no tengan un constructor sin parámetros, o que no te dejen sobrescribir sus métodos y propiedades, te impedirán hacer mocks de ellas. No las uses como dependencias de tus clases si tienes la oportunidad. Esto vale tanto para las clases que escribes tú, es decir, que tus clases implementen una interfaz te facilitará los test. Tanto como para las librerías externas que uses, que a la hora de elegirlas, el hecho de que puedas mockerlas debería ser un factor a tener en cuenta.

Moq al rescate

Como no somos los primeros que tenemos este problema, ya hay quien ha hecho la parte difícil del trabajo, que es facilitar la generación de mocks. Hay cientos de frameworks para tal fin, pero en este ejemplo usaremos Moq. Gracias a este, con una simple línea, ya tendremos nuestro mock creado.

var mockForecastService = new Mock<IForecastService>();

Obviamente es un poco simple, y tendremos que darle el comportamiento que queremos que tenga para que nuestro test pase.

mockForecastService
    .Setup(x => x.GetForecastAsync(It.IsAny<float>(), It.IsAny<float>(), It.IsAny<DateTime>()))
    .Returns(Task.FromResult<Forecast>(expected.Forecast));

En este caso, con el método Setup configuramos como va a ser la llamada a nuestro método, y con el Returns, qué es lo que vamos a devolver. Lo que le estamos diciendo en el Setup, mediante el uso de los distintos It.IsAny es que lo estamos configurando para cualquier llamada a GetForecastAsync, con cualquier parámetro, y devolverá siempre la predicción de tiempo que nosotros queramos Returns(Task.FromResult<Forecast>(expected.Forecast). El Task.FromResult es porque el método es asíncrono, así que lo que debemos devolver es un Task de la predicción.

Pero podríamos hacer todos los Setup que necesitásemos, configurando el mismo método más de una vez para devolver distintos resultados, según los parámetros que reciba, o cuantas veces se ha llamado al método, etc. Si te interesa, puedes profundizar en el framework en su repo de github o en la documentación de su proyecto.

Los mocks no solo sirven para crear dependencias.

En este caso, como nuestro código hace uso de una dependencia externa, el API, y esa llamada, puede afectar al rendimiento de nuestra aplicación, queremos comprobar que no se abusa de las llamadas al API. Gracias a Moq, podemos hacer esta comprobación en una sola línea.

mockForecastService.Verify(m => m.GetForecastAsync(It.IsAny<float>(), It.IsAny<float>(), It.IsAny<DateTime>()), Times.Once);

Esto nos garantiza que tan solo se llamará una vez al API, sea cual sea el estadio que se pida. Esto puede, al igual que el ejemplo anterior, ser mucho más rico y complejo. Podemos verificar qué parámetros exactos se pasan, y cuantas veces se hace la llamada. Y hasta pasar una función, que haga este cálculo, si necesitamos una lógica más compleja.

Aunque en GameWeatherService se llama a LogWarning y a LogInformation, en el test, a la hora de verificar estás llamadas, puedes ver que el método que verificamos es Log. Esto se debe, a que como puedes ver en el mock de ILogger que hicimos a mano, este es el único método real, los LogWarning y LogInformation que se usan son extension methods, que no se pueden verificar directamente, si no por la llamada que hacen ellos mismos internamente Log.

La importancia del nombrado. Haciendo las aserciones más humanas.

Como comenté en una nota anterior, la siguiente línea no funciona.

Assert.Equal(expected, gameWeather);

Esto se debe a que internamente, usa la referencia de la clase para comparar estos dos objetos, así que no llega con que tengan las mismas propiedades, para que esta condición se cumpla, deben ser el mismo objeto. Esto se puede solucionar de distintas maneras: tal y como hemos hecho en el primer test de ejemplo, comparando propiedad por propiedad, sobrescribiendo el método Equals y GetHasCode, o escribiendo una clase que implemente IEqualityComparer para todas las clases que necesites comparar. Cualquiera de estás opciones, te obliga a escribir mucho código, que será un dolor de cabeza cuando tengas que mantenerlo.

Buscando una solución a esto fue como di con FluentAssertions. Me permitía comparar dos objetos, y además hacerlo con un API fluent, que era muy agradable de usar.

gameWeather.Should().BeEquivalentTo(expected);

Pero esto me enseño que podría nombrar mis tests de otra manera, y empecé a cambiar mis clases de test, adaptándolos a la mentalidad con la que se hizo esta API, para que fuese más verbal, más cercana al lenguaje humano.

Lista de test unitarios

 

Ahora uso la técnica de llamar a mis test Should_ExpectedBehavior_When_StateUnderTest, es decir, debería pasar esto (comportamiento esperado) cuando se den tales condiciones (el estado bajo el que se ejecuta el test). Lo cual acerca el nombre del test a un lenguaje de ‘negocio’ que te ayuda a entender que es lo que estás probando y por qué, y evita que simplemente estés escribiendo tests para que pasen y cumplir el trámite. Además, tu assert prácticamente se asemejará al nombre de tu test.

Otro motivo por el que uso FluentAssertions es porque es muy concreto a la hora de explicar por qué tu test no ha pasado cuando falla, y eso ayuda mucho a encontrar el motivo del error.

 

También podéis seguirnos en Twitter, LinkedIn y Facebook.


Licencia de Creative Commons
Este obra está bajo una licencia de Creative Commons Reconocimiento-NoComercial 4.0 Internacional.

Compártelo: Share on FacebookTweet about this on TwitterShare on LinkedInPin on Pinterest

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

NEWSLETTER