Optimización de pruebas en Cypress

Optimización de pruebas en Cypress

Lleïr García Boada, Quality Engineer at SNGULAR

Lleïr García Boada

Quality Engineer at SNGULAR

27 de diciembre de 2024

En el mundo de la calidad, las pruebas automatizadas están a la orden del día, están presentes en ciclo de desarrollo y son un elemento vital para saber la salud de nuestro proyecto.

Por lo tanto, una de las virtudes que deben de tener siempre nuestras pruebas automáticas es la velocidad de ejecución, han de ejecutarse en el menor tiempo posible, para tener los resultados cuanto antes, esto nos ayuda a analizar y tomar decisiones rápidamente.

Otra de las virtudes que debe tener es una buena gestión de datos de pruebas, su gestión es compleja (crea datos, guardarlos en base de datos, pídele al backend que los cree...) y en muchas ocasiones los datos que tenemos para las pruebas pueden modificarse o incluso eliminarse; esto nos mete en problemas como por qué las siguientes ejecuciones pueden causar lentitud en la ejecución, por no encontrar datos esperados, y un retraso en los resultados y posterior toma de decisiones.

Así que vamos a ver cómo mejorar la velocidad de ejecución de nuestras pruebas, como crear atajos en nuestras pruebas, y tener una gestión de datos simple y desde nuestro código de pruebas, haciendo uso de datos mockeados.

Los datos mockeados, son datos que puede que no provengan de un backend o de un servicio o, si provienen de ellos, igualmente son considerados datos “falsos” ya que los escribimos a nuestro antojo, no son datos reales que provienen de la propia aplicación, y se utilizan para el propio desarrollo o automatización de pruebas.

¿Qué vamos a ver?

¡Cypress! Cypress es un magnífico framework javascript para armar y ejecutar de manera fácil y rápida, pruebas E2E automáticas sobre aplicaciones web hechas.

Pero hoy, en este artículo, nos vamos a centrar sobretodo en tres cosas:

  • Mock de datos.
  • Modificación del LocalStorage y cookies del navegador.
  • Peticiones API rest.

El objetivo principal que perseguimos con el uso de estas técnicas es el de optimizar la ejecución de las pruebas ya que nos permitirán establecer un contexto adecuado a cada escenario de prueba, sin la necesidad de realizar acciones sobre la UI.

Vamos a ver cada funcionalidad en acción, aplicándolo a un caso de ejemplo real (bueno real, real... sobre unas páginas web de ejemplo).

¿Por qué es interesante?

Preparar datos para los tests es interesante y en muchos casos de vital importancia para:

  • Ganar tiempo en definir tests: Uno de los clásicos problemas en el mundo de las pruebas, son los datos… “¡Ay! Que no tenemos este tipo de usuario, hay que crearlo” o “Hay que pedir al backend que nos preparen estos datos para las pruebas”... todo esto se acaba con Cypress; desde esta herramienta se puede mockear cualquier petición, establecer valores en el localStorage/sesión del navegador o incluso hacer una petición API rest (¡siempre que el endpoint exista!), esto facilita la labor a la hora de definir una prueba sin tener que estar pensando en qué datos voy a tener que necesitar lo que nos aporta mayor versatilidad a la hora de encarar la estrategia de testing.

  • Ganar tiempo en la ejecución de tests. Hay muchísimas maneras de preparar datos y, en ocasiones, nos ha tocado prepararlos en la propia ejecución de un test o en la propia batería de tests, se prepara de antemano un set de pruebas vía la propia web (click aquí y creo esto que necesitaré, voy allá y edito esto porque lo necesito así) o incluso llamando a la base de datos y updateando. Además, luego nos toca limpiar el estado de los datos para las siguientes ejecuciones. Este tiempo de ejecución se ve superreducido con Cypress ya que mockear datos es un “pim-pam”.

  • Todos los datos los gestionamos en Cypress. Ya lo he comentado en el punto uno, pero para dejarlo más claro aún como epígrafe independiente, no dependemos de nadie a nivel de datos.Todo es “mockeable”, incluso servicios externos.

¿Dónde y cuándo lo puedes usar?

Como habrás leído anteriormente, vamos aplicar estas técnicas de mockeo de datos en nuestro código de pruebas y siempre que nos sea necesario tener de antemano unos datos.

Una opción es mockear peticiones de red que nos llegan desde el servidor. Esta práctica es muy útil, tenemos un servicio funcionando pero en la base de datos no tenemos todos los datos que necesitamos; en ese caso capturamos la llamada y mockeamos los datos a nuestro gusto según necesidad.

Interceptar peticiones

Por ejemplo: necesitamos una prueba para validar que una lista está vacía y contiene el mensaje X. Lo que suele suceder es que dentro de nuestra aplicación podemos tener esa lista vacía o totalmente llena, suele pasar que se empieza a llenar cuando la funcionalidad de “añadir ítems” está disponible o incluso que un servicio externo nos devuelva la lista siempre llena de ítems; esto nos complica la gestión de datos por qué dependemos del servicio externo o bien de limpiar la lista via base de datos, con un restore o utilizando otro usuario para dicha prueba.

Con Cypress es tan fácil como interceptar la llamada que me devuelve esa lista y especificarle los ítems que necesito, en este caso 0 ítems. Y así utilizamos los datos que esperamos en la prueba.

Setup del estado del navegador

Otro ejemplo: estamos en un ecommerce y queremos empezar la prueba con nuestra moneda local, en este caso el euro y por defecto, al abrir la página tenemos dólares como moneda. Normalmente, esto puede estar ubicado en el localStorage del navegador y en vez de cambiar la moneda mediante acciones en el navegador (hago click en el desplegable de monedas, seleccione el euro, espero a que la página cargue de nuevo) podemos editar la variable en el localStorage a modo de tener de primeras la moneda euro.

Peticiones REST desde el código del test

Y vamos con otro ejemplo final para la petición API rest:

Para este ejemplo podríamos tener la necesidad de logearnos y recibir un token de sesión para nuestra página y así directamente tener acceso a ella sin hacer el login por la propia UI.

Vamos a los ejemplos prácticos

Vamos a ver el código en acción, utilizamos como ejemplo distintas páginas para poder visualizarlo:

Mock data

Para este ejemplo, pondremos un caso funcional que podría ser real, queremos comprobar que hay un mensaje de “alerta” cuando solamente hay un ítem en la siguiente lista:

1

Normalmente, un listado se empieza a rellenar con items o bien introducidos por app o introducidos vía base de datos y más tarde se hace difícil hacer pruebas con menos o incluso cero items.

Estos valores provienen de una request que se le hace al servidor, Cypress nos permite “interceptar” la llamada y decirle qué valores va a tener. Esto lo podemos tener especificado en el propio código o tener un JSON como fixture y utilizarlo como mock.

Usamos esta pagina https://rahulshettyacademy.com/angularAppdemo/ para el siguiente ejemplo.

cy.intercept({
    method: "GET",           // tipo de respuesta que se interceptará
    url: "/Library/**"       // URL que buscará para interceptar 
                             // (se pueden utilizar caracteres especiales)   
},
{
    statusCode: 200,         // mocqueamos el status code y la respuesta en el body
    body : [
       {
          "book_name":"RestAssured with Java",
          "isbn":"RSU",
          "aisle":"2301"
       }
    ]
}).as("books");             // especificamos un alias para saber qué interceptor es
cy.get("[data-target='#exampleModal']").click();    // navegamos o accionamos a través de la          
                                                    // app, donde se llama al endpoint para       
                                                    // que Cypress lo intercepte. 
cy.wait("@books");         // utilizamos la espera de cypress hasta encontrar 
                           // la request a interceptar con el el aliase "books"
                           // finalmente podríamos hacer la validación correspondiente

El resultado después de esta ejecución será el siguiente:

2

3

Como vemos, la respuesta devuelve un item y la página nos muestra el mensaje de alerta (subrallado en amarillo) que queriamos validar para este ejemplo.

En el runner de Cypress vemos de manera muy sencilla en el apartado “routes”, lo que hemos interceptado:

4

Podemos incluso hacerlo más simple y especificar tan solo la URL a interceptar y el JSON que contiene los datos a mockear. Así nos queda un código más limpio:

cy.intercept('GET', '/Library/*', { fixture: books.json' })

Tenemos más atributos para mockear si fueran necesarios:

headers?: { [key: string]: string }

Headers que acompañan a la respuesta

forceNetworkError?: boolean

Si esta opción es “true”, Cypress destruye la conexión con el navegador y no manda ninguna respuesta. Esto es útil para simular fallos de conexión con el servidor.

throttleKbps?: number

Kilobytes por segundo al enviar el “body”.

delay?: number

Milisegundos para provocar un retraso antes que la respuesta se envíe.

Modificación de localStorage y cookies

El objeto Storage (API de almacenamiento web) nos permite almacenar datos de manera local en el navegador y sin necesidad de realizar alguna conexión a una base de datos. Aquí se pueden encontrar propiedades como el token de sesión para un usuario que hizo login, la moneda en un marketplace o incluso el idioma que hay en la página web ahora mismo, entre otras muchas cosas.

Para este ejemplo veremos cómo cambiar una propiedad que localiza e indica el idioma de la página web, basamos el ejemplo en la página de https://www.pullandbear.com/.

¿Para qué especificar valores para propiedades en el localStorage?

Básicamente para preparar pruebas, así no tenemos que especificar por cada prueba o grupo de pruebas, hacer la parte funcional desde nuestra prueba automatizada con la consecuente ganancia de tiempo de ejecucióny la minimización de posibles fallos negativos que puede causar pasar por sitios de nuestra aplicación no necesarios en ese momento.

Así que para modificar un atributo, utilizamos localStorage.setItem(propiedad, valor)

cy.visit("https://www.pullandbear.com/")<br></br>localStorage.setItem("LAST_STORE_GEOBLOCKING", '{"storeId":25009475,"langId":-1}')

Siempre se deben especificar estos valores después de acceder a la página en cuestión, si no, una vez se accede, se sobrescribirá.

Y como vemos, entre otras propiedades, tenemos la que acabamos de especificar.

5

En este caso, para validar que se ha aplicado correctamente, veremos que la URL contiene el país (http//www.pullandbear.com/ba/) o también dentro de la app hay secciones donde se puede validar esto, en este caso Bosnia And Herzegovina y además del lenguaje en el que se inicia.

6

En la cookie también encontramos valores que podemos modificar, este ejemplo con la página de Pull & Bear nos servirá para dar como aceptada la política de cookies y así no tener que aceptarla cada vez que iniciamos una prueba o grupo de pruebas. Tal y como pasa con el localStorage, nos agiliza la ejecución de las pruebas.

Al ser una propiedad guardada en las propias cookies, es tan sencillo como especificar la cookie y su valor:

cy.setCookie("OptanonAlertBoxClosed", "2021-06-15T13:42:44.998Z")

La cookie debe ser especificada antes de abrir la página donde tendrá efecto. Y así se verá en el runner como “setCookie”.

7

Peticiones a un API rest

Para este ejemplo utilizaremos la página https://www.checkli.com/ (una página sencilla de listado de tareas) en la que necesitaremos de antemano unas tareas ya definidas.

La diferencia entre utilizar datos mockeados y hacer una petición e introducir los datos, puede ser básicamente porqué vamos a necesitar esos datos en otras pruebas y no queremos mockeados cada vez.

Para verlo más claro, podemos utilizar un mock para que un servicio externo en el que no podemos, de manera sencilla, introducir datos, nos devuelva lo que necesitamos, pero si tenemos los endpoints ya desarrollados, podríamos utilizar una petición API rest que nos permitiría introducir valores y así nutrir, por ejemplo, un listado.

Por lo tanto tenemos la lista de “todos” vacia:

8

Y la vamos a rellenar con 1 tareas:

cy.request({
     method: "POST",          // definimos el tipo de petición
     url: "https://www.checkli.com/api/v1/checklists/"+url+"/tasks", 
                              // definimos la url de la petición (en este ejemplo se                                  
     body:                    // utiliza un string de la propia url)
     {
        "name": "Tarea de prueba 1”,       // definimos los valores a enviar del body
        "section": 0,
        "priority": 0
     }
});
cy.reload();                // recargamos la pagina para ver el listado relleno.

Al ejecutar este código, vemos que en la página ya tendrémos la tareas:

9

Aquí podéis ver el código completo y funcional con 2 tareas y una de ellas “completada”:

visit("https://www.checkli.com/")
get("a").contains("Make a free checklist").click();
cy.wait(4000)

cy.url().then(url => {         // para este caso concreto necesitamos un valor de la url
    url = url.substr(url.lastIndexOf('/')+1, url.length-1);

    let first = true;
    const tasks = ["prueba 1", "prueba 2"];     // 2 tareas hardcodeadas
    tasks.forEach(t => {
        cy.request({
            method: "POST",                    
            url: "https://www.checkli.com/api/v1/checklists/"+url+"/tasks",               
            body :                             
            {
               "name": t,
               "section": 0,
               "priority": 0
            }
         }).then(resp => {         // encadenamos la "request" con el comando "then" para 
                                   // utilizar variables de la “response” 
           if(first){              // boolean de decisión para completar la primera tarea
               cy.request({        // hacemos otra petición en base al ID de tarea
                   method: "POST", // que nos devolvió la anterior
                   url: "https://www.checkli.com/api/v1/checklists/"+url+"/tasks/" + resp.body.data.id,

                   body :
                   {
                       "section": 0,
                       "name": resp.body.data.name,
                       "complete": 1
                   }
               })
           }
           first = false;
       })
    })
})
cy.reload()

Como ya vimos en el ejemplo de mock data, hay muchos atributos para especificar en la “request” qué encontraremos en la documentación de Cypress.

* (Este ejemplo utiliza un ID existente en la URL, por eso se lo pasa como valor al atributo URL de la petición y, además, al finalizar la petición recargamos la página ya que estamos en la misma página del listado, si no la recargamos no veremos las nuevas tareas. Normalmente, este tipo de peticiones se hacen con URLs estáticas, sin pasarle ningún tipo de atributo (aunque a veces sí es necesario) y así poder definir toda la test data en un método “before” antes de la ejecución del test o el grupo de tests, a modo de preparación, así una vez el test en la aplicación llega donde necesitará esa data, esta ya estará disponible)

Conclusiones

Como acabamos de ver, Cypress es una herramienta muy potente y que facilita enormemente el mockeo de datos y realizar el “setup” previo de una prueba. Esto nos ayuda muchísimo a la hora de preparar escenarios de prueba de manera rápida y sencilla, desde el propio código de pruebas y sin depender ni esperar a terceros ni hacer triquiñuelas. Además, todo esto tiene un impacto positivo en la velocidad de definición y ejecución de pruebas.

Lleïr García Boada, Quality Engineer at SNGULAR

Lleïr García Boada

Quality Engineer at SNGULAR