Pruebas de APIs utilizando Approval Testing
22 de noviembre de 2024
¿Qué vamos a ver?
Dentro del ámbito de las pruebas de APIs existen multitud de herramientas que nos pueden facilitar esta labor. En función de las necesidades, podemos decantarnos por opciones tipo Postman donde prima la facilidad de uso y una curva de entrada prácticamente nula hasta llegar a librerías de código que nos permitan desarrollar los tests con un nivel de detalle mucho mayor.
No obstante, escojamos la herramienta que escojamos, habrá escenarios donde verificar que la API responde de la manera esperada se pueda convertir en algo realmente complejo y difícil de mantener. Un ejemplo muy común que nos podemos encontrar es el caso de una API donde recibimos respuestas con un número elevado de campos (o arrays de objetos) y resulta necesario comprobar el valor de todos ellos.
Ante este tipo de situaciones, siempre es aconsejable reflexionar y hacerse la pregunta de si es imprescindible revisar todos y cada uno de los valores devueltos por el servicio, o bien si realmente nos importa el valor específico devuelto o meramente queremos asegurar la presencia de ese campo.
En caso de que efectivamente necesitemos revisar todos los campos, la manera directa y tradicional de resolver estas situaciones es evaluar los campos uno a uno, lo que implica que se generen test complejos, largos y difíciles de mantener, ya que terminan con multitud de comprobaciones individuales.
Es aquí donde tiene sentido utilizar otra serie de técnicas de pruebas, en este caso, hablamos de “Approval Testing”
¿Qué es Approval Testing?
Como se ha mencionado anteriormente, Approval Testing no es una herramienta de pruebas en sí, sino que se trata de una técnica, es decir, una manera de realizar verificaciones de manera automática.
Approval Testing, esencialmente, está basado en la comparación exhaustiva de dos objetos, los cuales pueden ser valores numéricos, cadenas de caracteres, clases, ficheros binarios, imágenes, etc. De manera que la prueba se considera OK si la comparación es exitosa y fallo en caso contrario.
Con esto en mente, el flujo de trabajo de Approval Testing se fundamenta en la creación de una línea base que establece los ejemplos correctos y que se utilizarán como ejemplo validado en futuras comprobaciones.
Teniendo esto en cuenta, el flujo básico de Approval Testing sería el siguiente:
- Generación de línea base (veremos más adelante cómo hacerlo)
- Incorporación de línea base al repositorio de código
- Ejecución de pruebas
- Comparación automática de resultados obtenidos contra la línea base
Llegados a este punto pueden darse diferentes situaciones
- Comprobación idéntica: Resultado de los test OK
- Comprobación distinta: Se producirá un fallo en las pruebas
En caso de fallo, será el momento de valorar la causa del mismo y actuar en consecuencia. Es decir, podemos encontrarnos con estas dos posibilidades.
- Cambio no controlado, por tanto, se ha detectado un error
- Cambio legítimo por nueva funcionalidad: Se debe actualizar la línea base con el nuevo resultado y volver a ejecutar los tests
De manera resumida, el flujo quedaría representado por este diagrama:
Mediante este flujo de trabajo conseguiremos realizar comprobaciones exhaustivas de manera rápida, sencilla y mantenible como veremos a continuación.
¿Por qué es interesante?
Cómo se ha mencionado previamente, se trata de una técnica de automatización de pruebas que establece un flujo de trabajo claro y que da como resultado una batería de pruebas clara y fácil de mantener. Además, se trata de una herramienta que podemos compaginar perfectamente con otro tipo de pruebas unitarias o de integración.
Para qué sirve
La estrategia de Approval Testing es la base de otros tipos de pruebas como Visual y snapshot testing, los cuales, también, están basados en la comparación minuciosa de imágenes o código HTML, respectivamente.
Dónde encontrarlo
Las librerías de Approval Testing están disponibles para varios lenguajes de programación.
Página oficial: Home (approvaltests.com)
¿Dónde puedes aplicar Approval Testing?
Casos de uso habituales
Como se ha mencionado anteriormente, es interesante plantearse la opción de incluir Approval Testing en situaciones como cuando:
- Necesitamos efectuar una comprobación exhaustiva de grandes volúmenes de datos
- Disponemos de resultados previamente validados
- Deseamos generar un conjunto de ejemplos de resultados de manera rápida y sencilla
- Consideremos que los valores concretos de los campos no son determinantes para el resultado de la prueba
- Vamos a realizar comparaciones de imágenes / ficheros de texto
Dónde no es recomendable
Hay que tener en cuenta que la línea de base de ejemplos formará parte del repositorio de proyecto, por lo que tendremos más ficheros que gestionar. Por tanto, tampoco resulta conveniente añadir ficheros y versiones a un repositorio de código cuando no aporte valor.
En este sentido, puede no resultar una opción del todo adecuada cuando la cantidad de datos a validar no sea elevada y nos resulte cómodo realizar comprobaciones individuales.
Otro escenario donde tendremos que evaluar la idoneidad de esta técnica será cuando el conjunto de datos a validar tenga un número alto de campos dinámicos, ya que si no establecemos las medidas adecuadas (que veremos más adelante) los tests estarán fallando constantemente, puesto que la comparación siempre dará un resultado negativo.
¿Cómo funciona?
Veamos el Approval Testing en acción con un ejemplo real.
Entorno utilizado, prerrequisitos
Prerrequisitos:
- Java 8+
- Maven o Gradle
- Eclipse o IntelliJ IDE
Todos los ejemplos que vamos a ver, y mucho más, están disponibles en este repositorio.
Setup inicial
Dentro de nuestro proyecto debemos añadir las dependencias de las librerías de Approval Testing, rest-assured para realizar las peticiones al API y las de JUnit que serán las que finalmente lancen las prueba
El fichero pom.xml quedaría de la siguiente manera:
<dependency>
<groupId>com.approvaltests</groupId>
<artifactId>approvaltests</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>4.5.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
Caso de uso básico
Para este ejemplo, utilizaremos la API pública que devuelve datos sobre el universo Star Wars. Sobre ella, mostraremos cómo emplear la técnica de Approval Testing tanto cuando se trata de validar datos estáticos como dinámicos.
En este caso, validaremos que el endpoint que da información sobre los personajes, devuelve los datos esperados.
En este caso, para recibir información sobre Luke Skywalker, debemos realizar una llamada de tipo GET al la URL: https://swapi.dev/api/people/1
Cuya respuesta es:
HTTP 200 OK
Content-Type: application/json
Vary: Accept
Allow: GET, HEAD, OPTIONS
{
"name": "Luke Skywalker",
"height": "172",
"mass": "77",
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "https://swapi.dev/api/planets/1/",
"films": [
"https://swapi.dev/api/films/1/",
"https://swapi.dev/api/films/2/",
"https://swapi.dev/api/films/3/",
"https://swapi.dev/api/films/6/"
],
"species": [],
"vehicles": [
"https://swapi.dev/api/vehicles/14/",
"https://swapi.dev/api/vehicles/30/"
],
"starships": [
"https://swapi.dev/api/starships/12/",
"https://swapi.dev/api/starships/22/"
],
"created": "2014-12-09T13:50:51.644000Z",
"edited": "2014-12-20T21:17:56.891000Z",
"url": "https://swapi.dev/api/people/1/"
}
Como vemos, y dentro del contexto del ejemplo, podríamos decir que se trata de un conjunto de datos suficientemente amplio como para tratar de validar los campos uno a uno. En escenarios como este, tiene sentido aplicar la técnica de approval testing.
Una vez que hemos realizamos los imports correspondientes en nuestro código Java, el test quedaría tan simple como esto:
private static String baseUrl = "https://swapi.dev/api/";
@BeforeAll
static void setUp() {
RestAssured.baseURI = baseUrl;
}
@Test
public void lukeDataShouldBeCorrect()
{
final Response response = given().get("/people/1");
Approvals.verify(response.body().prettyPrint());
}
De manera que, la primera vez que ejecutemos la prueba, se producirá un error, puesto que aún no existe una línea base definida.
Es este momento, es cuando la propia librería se encargará de lanzar la herramienta por defecto de comparación de ficheros que tengamos instalada en nuestro equipo. Por tanto, al no existir un fichero previo que coincida con el nombre de nuestro test, deberíamos guardar en disco dicho resultado como línea base, la cual servirá como referencia para futuras ejecuciones.
Una vez que aceptemos la línea base, quedará guardada en la estructura de directorios del proyecto y pasará a formar parte del mismo. Es muy importante tener en cuenta que este fichero forma parte del test en sí y que, por lo tanto, también debe formar parte del repositorio de código.
De esta manera, en sucesivas ejecuciones de pruebas, la librería realizará las comprobaciones oportunas basándose en los ficheros almacenados.
Validación de respuesta con datos dinámicos
Supongamos ahora que necesitamos hacer una validación de respuestas que contienen datos dinámicos. Por seguir con el ejemplo anterior, imaginemos que queremos incluir las cabeceras en las verificaciones. Como puede verse en este ejemplo, puede haber campos que varíen en cada llamada al API.
En casos así y para evitar que las comprobaciones realizadas por la librería de Approval Testing fallen en cada ejecución, necesitamos enmascarar de alguna manera esos datos. La manera más sencilla de conseguirlo, es realizar sustituciones que transformen esos campos antes de realizar las verificaciones.
Una posible solución sería:
@Test
public void lukeDataAndHeaderShouldBeCorrect()
{
final Response response = given().get("/people/1");
String responseStr = response.headers().toString() + "\n\n" + response.body().prettyPrint();
responseStr = responseStr.replaceAll("Date=.*", "Date=***DATE***");
responseStr = responseStr.replaceAll("ETag=.*", "Etag=***ETAG***");
Approvals.verify(responseStr);
}
Esto hará que el fichero base se almacene con los campos modificados y que, por tanto, los datos dinámicos no afecten a las pruebas.
Repositorio de código
El código utilizado en este artículo está disponible en este repositorio.
Qué nos ha parecido
Ventajas
Se trata de una técnica que facilita enormemente la verificación exhaustiva de juegos de datos y que, además, hace que los tests sean tremendamente fáciles de mantener, puesto que apenas añade líneas de código.
Resultará muy útil en escenarios donde tengamos que validar grandes volúmenes de datos o donde ya dispongamos de una línea base previamente validada.
Inconvenientes
Al tratarse de una técnica adicional que puede complementar las pruebas existentes, no presenta ningún inconveniente realmente importante, puesto que no sustituye a nada que ya estemos haciendo y que consideremos adecuado.
De todas formas, dado que esta herramienta generará un archivo nuevo por cada test que realicemos, debemos tener en cuenta que necesitaremos ser más cuidadosos a la hora de gestionar el repositorio de código y sus respectivas ramas.
Bibliografía: