Pruebas de APIs utilizando Approval Testing

Pruebas de APIs utilizando Approval Testing

Francisco Moreno, QE Director

Francisco Moreno

QE Director

November 23, 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:

  1. Generación de línea base (veremos más adelante cómo hacerlo)
  2. Incorporación de línea base al repositorio de código
  3. Ejecución de pruebas
  4. 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:

workflow

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.

diff

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.

IDE

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.

postman

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:

Home (approvaltests.com)

Verifying Entire API Responses - Angie Jones

SWAPI - The Star Wars API

Francisco Moreno, QE Director

Francisco Moreno

QE Director

Francisco Moreno, QE Director at SNGULAR, leads the company's testing strategy and a team of professionals specialized in quality assurance in agile projects.