Certified Datasets en Power BI
8 noviembre, 2019
Cambiemos la forma de ver el mundo: La tecnología nos hará superhumanos
13 noviembre, 2019

Cada equipo acaba teniendo su forma de hacer las cosas, y resuelve el mismo problema a su propia manera. Los test unitarios no son una excepción. Con el tiempo, al trabajar en distintos equipos, de las distintas herramientas y costumbres (que también son una herramienta), vas eligiendo las que más te gustan, y acabas teniendo tu propia metodología de test unitarios.

En este post y el siguiente voy a explicar como hago los test unitarios en mis propios proyectos. Esto no es una metodología ni una guía de estilos que seguir a rajatabla. Coge lo que más te guste, adapta lo que te convenga. Y sobre todo, si trabajas en equipo, lo más importante es que todos uséis las mismas herramientas y metodología, que pongáis esto en común, y que encontréis un punto en que todos os sintáis cómodos. Los test no se escriben solos, y cuanto más cómodos estéis con ellos, más código acabareis cubriendo con vuestras pruebas.

El problema

El ejemplo que vamos a resolver es sencillo. La idea es hacer una herramienta que nos indique qué tiempo hará en el próximo partido de un equipo de la NFL (el fútbol americano es mi deporte favorito).

Código fuente
En mi repositorio de github podréis encontrar los ejemplos que voy a usar en este post.

La solución simple

Una solución será API Web escrita en C#, con .NET Core.

Para la predicción del tiempo usaremos el API de accuweather, ya que que una consulta a un API ejemplifica perfectamente lo que quiero mostrar. Sin embargo, la lista de equipos y estadios estarán ‘harcodeados’ en la aplicación por mantener cierta simplicidad en los ejemplos, y la lista de partidos será aleatoria para que siempre haya partido.

Para la primera solución al problema, tenemos un ejemplo FirstVersion. Vamos a centrarnos en el servicio al que se llama desde el controlador, que en este caso, es lo que queremos probar. FirstVersionService tiene un método GetNextGameWeatherAsync, que hace todo lo que necesitamos. Se le pasa un código que consiste en una cadena de dos o tres carácteres que representa a un equipo, busca si este existe y si tienen partido. Si es así, llama al API para encontrar la predicción del tiempo y devuelve los datos encontrados. Todo bastante simple, sin dependencias y concentrado en un único método, lo cual de principio, parece una buena solución.

Quizá algún ejemplo no os funcione y el error se deba a que el API no acepta más peticiones. Los ejemplos están hechos con una cuenta de accuweather gratuita que he creado para este caso y tiene un número de peticiones limitado. Podéis crear la vuestra propia registrándoos aquí y así podréis lanzar los ejemplo otra vez.

Un test unitario simple

La primera decisión que tenemos que tomar para hacer nuestros test es precisamente nuestro framework de test. En mi caso prefiero usar XUnit. En Visual Studio viene integrado por defecto, así que crear nuestro proyecto de test es tan simple como añadir un proyecto a la solución y elegir un proyecto XUnit. Por convención, lo común es llamarlo con el mismo nombre de la librería que vamos a probar, pero acabada en Tests, así que nuestro proyecto se llamará NFLGameWeatherTests.

Añadir proyecto XUnit a la solución

Crear un test con XUnit, es tan simple como crear una clase pública, y decorar uno de sus métodos con el atributo Fact. Esto hará que el runner de XUnit identifique este método como un test unitario.

En esta primera versión, para los métodos usaremos un nombrado que nos permita reconocer rápidamente qué se está probando. Un método bastante común, que he usado y no va mal, es la de usar una clase de test para cada clase a probar, y nombrar los métodos de prueba como MétodoProbado_CondiciónProbada_ResultadoEsperado. Como lo primero que vamos a probar es que cuando le pasemos una clave correcta de equipo a nuestro método, este encuentre el partido y nos dé la predicción del tiempo, nuestro método se llamará GetGameWeatherAsync_TeamKeyIsOk_ForecastFound.

Cuando probamos un método, suele tener una estructura Arrange-Act-Assert, es decir, primero preparamos todo para nuestra prueba, luego se ejecuta y por último comprobamos que la ejecución ha hecho lo que nosotros esperábamos.

    [Fact]
    public async Task GetGameWeatherAsync_TeamKeyIsOk_ForecastFound()
    {
        // Arrange
        Game expectedGame = Game.Schedule
            .Where(x => x.HomeTeam.Equals(Team.Texans) || x.AwayTeam.Equals(Team.Texans))
            .Where(x => x.Date.Date > DateTime.UtcNow.AddDays(-1).Date)
            .FirstOrDefault();

        GameWeather expected = new GameWeather(
            expectedGame,
            new Forecast()
            {
                Minimum = "5,1° C",
                Maximum = "22,6° C",
                Day = "Mostly cloudy",
                Night = "Partly cloudy"
            }
        );

        FirstVersionService service = new FirstVersionService();

        // Act
        GameWeather gameWeather = await service.GetNextGameWeatherAsync(Team.Texans.Key);

        // Assert
        Assert.Equal(expected.HomeTeam, gameWeather.HomeTeam);
        Assert.Equal(expected.AwayTeam, gameWeather.AwayTeam);
        Assert.Equal(expected.Stadium, gameWeather.Stadium);
        Assert.Equal(expected.Date, gameWeather.Date);
        Assert.Equal(expected.Forecast.Day, gameWeather.Forecast.Day);
        Assert.Equal(expected.Forecast.Night, gameWeather.Forecast.Night);
        Assert.Equal(expected.Forecast.Maximum, gameWeather.Forecast.Maximum);
        Assert.Equal(expected.Forecast.Minimum, gameWeather.Forecast.Minimum);
        Assert.Equal(expected.City, gameWeather.City);

        //Assert.Equal(expected, gameWeather);
    }
La última linea esta comentada porque aunque parece la opción ‘natural’ para hacer la aserción, no funciona. En el siguiente post explicaré por qué, y cual es mi opción para solucionarlo.

En el test primero buscamos el próximo partido de los Texans en la lista de partidos de nuestra librería. Después creamos una clase con la predicción del tiempo, que es el resultado que esperamos, pero la creamos a mano, no consultando al API. La verdad es que cuando lances este test no funcionará y elegí este ejemplo concretamente para que así fuese, ya que ya habría pasado el partido que se jugaba cuando yo lo programé y así habría dejado de funcionar.

Y sí, tienes que tener un poco de fe cuando te digo que funcionó, pero la verdad es que lo hizo durante bastante menos de lo que yo esperaba, ya que a la media hora de escribirlo, cambio la predicción del tiempo y este se rompió. Es cierto que podría haber escrito el test llamado al API, y buscando los datos esperados en el API, pero prácticamente lo que estaríamos haciendo es replicar el código del servicio. Además, aún así, si la predicción del tiempo cambiase entre una llamada y la siguiente, el test no pasaría aunque el código estuviese bien. Y puede que al volver a ejecutarlo, funcionase, haciéndonos muy complicado saber cuando falla y por qué.

Pero el motivo más importante por el que esto está mal, no es el que el test pueda fallar, sino que no es un test unitario. Cuando metes una dependencia externa a uno de tus test, lo conviertes en un test de integración, haciendo que cualquier fallo en un código que no es el tuyo rompa tus test. Si nos da igual ser puristas, hay muchos motivos prácticos para no usar el API en nuestro test: el API podría estar caído durante un tiempo durante el cual no podríamos pasar nuestros test. No tenemos acceso al api desde nuestro servidor de compilación por políticas de firewall y nuestros test no pasarían en las builds automáticas. Además, cada llamada nos cuesta dinero y no está la cosa para derrochar, etc.

Sin embargo,no tienen ese problema los tests que no dependen del API, como GetGameWeatherAsync_TeamKeyIsEmpty_ArgumentNullException, que son los que controlan que se lanzan las expceciones esperadas. Al lanzarse estás excepciones antes de hacerse la llamada al API, no estamos cubriendo ese código en estás pruebas, así que estos funcionarán haga el tiempo que haga, o aunque el API esté caído. Así podemos ver que el problema es nuestra dependencia con el API de accuweather.

En este caso es fácil encontrar el problema. Está pensado para que lo sea, porque la predicción del tiempo es algo que no se va a repetir, y enseguida se ve que el test que está fallando. Lo está haciendo porque la predicción ha cambiado. Además falla siempre. Pero si el dato fuese más cambiante, estuviese escondido en medio de muchos otros datos, o la lógica fuese más complicada, ese sería un resultado que probablemente haría que nuestro test funcionase unas veces, y otras no, haciendo mucho más complicado encontrar el error.

Inyección de dependencias

Vamos a crear una clase nueva, otro servicio. Lo llamaremos GameWeatherService y está vez queremos probarlo, y que nuestros test pasen, diga lo que diga el API, pero ¿cómo?. Para resolver nuestro problema con el API, vamos a reconocer una cosa, tenemos una dependencia con este. Lo mejor en estos casos, es crear otra clase, que sea la que se encargue de llamar al API, y nosotros simplemente le pediremos el dato a esta. Así que lo que usaremos, es el concepto de inyección de dependencias. Esto nos ayuda a cumplir con el principio de responsabilidad única (Single Responsibility Principle), la famosa S de SOLID, y que cada clase se encargue de lo suyo.

Resumiendo, le vamos a pasar una clase a nuestro servicio en el contructor que es la que sabe lidiar con el API. Y cuando necesite hacerlo, nuestra clase dejará que su dependencia haga ese trabajo por él, que para eso la hemos creado. Además, usaremos un framework para inyectar nuestras dependencias, y que viene por defecto con Net Core. Si a eso añadimos que estaremos usando una interfaz para pedir nuestra dependencia, ya estamos cumpliendo también con la I y la D, y ya tenemos medio acrónimo SOLID.

Y por este motivo, mucha gente defiende que el desarrollo dirigido por pruebas te fuerza a tener un código más limpio. Aunque no es una bala de plata, si que ayuda a crear una arquitectura más organizada, no solo teniendo tu código probado, si no detectando dependencias y obligándote a separar correctamente las funcionalidades, etc.

Entonces en nuestro caso, vamos a crear una clase ForecastService, que será la que realmente sabe como comunicarse con el API, y que cumple una interfaz IForecastService, que es lo que realmente recibirá nuestro servicio GameWeatherService, y que usará cuando necesite obtener la predicción desde el API.

A volver a probar esta vez con mocks

Vale, ya tenemos el API aislado, pero aún necesitamos conseguir los datos de nuestra previsión de tiempo. Y aquí es dónde entran los mocks. Los mocks son objetos falsos, que cumplen con nuestra interfaz, simulando el comportamiento del objeto real, pero que en realidad, se comportarán tal y como nuestro test necesita que lo hagan. En la clase de test GameWeatherServiceTestWithManualMock se usa un mock de IForecastService y otro de ILogger, que no son más que clases que cumplen estás mismas interfaces. El mock de IForecastService recibe la predicción del tiempo que devolverá en su constructor, y siempre devolverá esta misma predicción. La de ILogger, simplemente cumple la interfaz, aunque su código no hace nada.

    [Fact]
    public async Task GetGameWeatherAsync_TeamKeyIsOk_ForecastFound()
    {
        // Arrange
        (...)

        var mockForecastService = new MockForecastService(expected.Forecast);
        var mockLogger = new MockLogger();
        GameWeatherService service = new GameWeatherService(mockForecastService, mockLogger);

        // Act
        GameWeather gameWeather = await service.GetNextGameWeatherAsync(Team.Texans.Key);

        // Assert
        (...)
    }

Aunque ahora nuestro test pasará sin necesidad de consultar el API, que era nuestro objetivo, podemos ver los problemas de hacer los mocks a mano. Tendremos mucho código que no hace nada. Será difícil reaprovechar nuestros mocks para más de un test sin que se vuelvan difíciles de leer, y vamos, que al final, cuando nuestra aplicación crezca, casi acabaremos necesitando test para comprobar que nuestro mocks funcionan como deben. ¿Cómo resolvemos esto? Para responder eso, ya estoy escribiendo el siguiente post.

 

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