Pruebas de extremo a extremo en JavaScript con Cypress

En esta guía, le presentaremos Cypress, un marco de trabajo de prueba de JavaScript de un extremo a otro, y exploraremos sus ventajas y desventajas, creando un flujo de trabajo de prueba de demostración.

Introducción

La automatización de pruebas de extremo a extremo es una parte importante del ciclo de vida de desarrollo de cualquier aplicación basada en web. Podría decirse que elegir la herramienta adecuada para usted y para su aplicación es aún más importante.

En esta guía, echaremos un vistazo a las pruebas de extremo a extremo usando Ciprés.

Las pruebas de "Extremo a extremo" se refieren al proceso de simular la experiencia del usuario y la interacción del usuario con un servicio, y probar si hay algún error en ese proceso.

¿Por qué usar ciprés?

Fácilmente, la mayor ventaja de usar Cypress es algo que los desarrolladores de Cypress llaman "Viaje en el tiempo".

Facilita el proceso de depuración al permitirle ver todo lo que sucedió en la prueba en su Registro de comandos y su Vista previa de la aplicación. Cada paso mostrará el estado de la aplicación en el momento de la ejecución, lo que le permitirá identificar con precisión el problema cuando algo sale mal.

Basamos una parte significativa de su percepción cognitiva en nuestra vista, y "Viaje en el tiempo" nos permite buscar errores de manera intuitiva (humanamente), al mismo tiempo que nos brinda el beneficio de la automatización.

También es un enfoque muy natural para la búsqueda de errores basado en el hecho de que este es un marco centrado en las pruebas de extremo a extremo, lo que significa que, además de probar las funcionalidades, podemos ver lo que haría el usuario final. ver.

Algunas de las otras razones por las que podría querer usar Cypress son:

  • No está basado en Selenium, por lo que no comparte los mismos problemas y ofrece una nueva perspectiva. Cypress está construido desde cero.
  • Hiperenfocado en pruebas de extremo a extremo.
  • Si puede ejecutarlo en el navegador, puede probarlo con Cypress.
  • Solo tendrás que aprender JavaScript.
  • La configuración es súper fácil y ultrarrápida.
  • Fue creado con Test-Driven-Development en mente.
  • Mucha documentación oficial.
  • Puede ver cada solicitud de red realizada en el momento en que se realizó desde el navegador, con acceso a todos los datos.
  • Puede bloquear cualquier solicitud de red, al mismo tiempo que puede crear cualquier solicitud de red (lo que significa que también puede usar Cypress para pruebas de API).
  • [desarrolladores] activos y transparentes (https://github.com/cypress-io).

Cypress está construido sobre moca y chai, que son bibliotecas BDD y TDD modernas y populares, y en realidad toma prestadas algunas de la sintaxis debido a esto. Si ha trabajado con estos antes, notará que los ganchos de ciprés se toman prestados directamente de Mocha.

¿Por qué no usar ciprés?

No existe una herramienta perfecta y, por extensión, no existe una herramienta de prueba perfecta. Si bien es genial, Cypress no es una excepción a esta regla.

Dependiendo de sus requisitos personales o del proyecto, algunas de las cosas enumeradas como ventajas pueden convertirse en desventajas:

  • Dado que no utiliza Selenium y está basado en JavaScript, deberá tener conocimientos de JavaScript. Selenium tiene soporte para JavaScript, Java, Python, Ruby y C#.
  • Dado que está muy centrado en las pruebas de extremo a extremo, no será una solución que pueda aplicar a todos los demás tipos de pruebas (excepto las pruebas de API).
  • No es (y posiblemente nunca lo sea) compatible con todos los navegadores (puede encontrar la lista de navegadores compatibles aquí) Esto puede ser un problema ya que ciertos tipos de clientes pueden solicitar soporte para IE, Opera o Safari.
  • Sin pruebas móviles.
  • Conocido por ser escamoso cuando se utiliza la navegación URL directa.
  • No se puede trabajar con más de una pestaña.
  • No se puede navegar a una URL de dominio diferente: esto puede ser una gran estafa si tiene más de una aplicación como parte de su solución, o si necesita probar algo en una interfaz de usuario de terceros. Deberá mantener un proyecto separado para su otra aplicación, o confiar completamente en las solicitudes de red para obtener datos.
  • Relativamente nuevo, por lo que no tiene tanto material comunitario como algunas herramientas de prueba más antiguas.
  • Algunas de las características de la hoja de ruta parecen haber quedado en un segundo plano, para algunas acciones que podría tener comúnmente en su aplicación, como la carga de archivos, el desplazamiento y el desplazamiento. Tendrás que encontrar soluciones alternativas.
  • Se necesita un trabajo significativo si desea una comunicación directa con la base de datos o prácticamente cualquier cosa fuera del trabajo directo del navegador. Sin embargo, están planeando lanzar adaptadores de back-end para otros idiomas. Esta guía se actualizará rápidamente a medida que se publiquen.

Algunos de estos nunca cambiarán mientras que otros están planeados para cambiar. Si desea obtener más detalles sobre qué características se mantendrán y cuáles no, su página de compensaciones es un gran lugar para comenzar.

Instalación y configuración de Cypress {#instalación y configuración de Cypress}

Para facilitar la prueba de Cypress y permitir que los desarrolladores prueben todas sus funciones, el equipo de Cypress compiló varias aplicaciones de demostración que puede usar si aún no tiene un proyecto iniciado y listo para probar.

We'll be using the Aplicación de demostración de fregadero.

Nota: Para usuarios de Windows, ejecute npm run start:ci:windows para iniciar la aplicación.

Una vez que se inicia la aplicación, instalemos Cypress usando npm:

1
$ npm install cypress --save-dev

Finalmente, podemos iniciar la biblioteca, usando npx o yarn:

1
2
3
$ ./node_modules/.bin/cypress run open # Directly
$ npx cypress open # Using npx
$ yarn run cypress open # Using yarn

Si está utilizando la aplicación de demostración, ya tendrá muchas especificaciones de ejemplo:

cypress demo example specifications

Al hacer clic en cualquiera de ellos (por ejemplo, actions.specs.js) se iniciará el corredor:

demonstration specification cypress

API y estilo de Cypress

Cypress está construido sobre Mocha y Chai y toma prestadas algunas de sus sintaxis y características.

Es decir, los elementos prestados más notables son los métodos describe(), context(), it() specify(). Básicamente, son envoltorios para los métodos de prueba reales que se utilizan para anotar grupos de prueba con etiquetas.

Vale la pena señalar que specify() y it() son sinónimos, al igual que describe() y context(). Dependiendo de lo que te suene más natural, puedes usar cualquier combinación de estos.

describe() se usa para dar contexto a un conjunto de pruebas, mientras que it() describe pruebas individuales. Por lo general, los anidará en una estructura similar a esta:

1
2
3
4
5
6
7
8
describe("Element X Testing", () => {
    it("Does Y", () => {
        // Test...
    });
    it("Does Z", () => {
        // Test...
    });
});

Esto es puramente para que sea más fácil para nosotros, así como para otros desarrolladores, echar un vistazo rápido a lo que está pasando sin tener que pasar por toda la cadena (potencialmente larga) de métodos utilizados para probar algo.

Dentro de cada prueba, confiaremos en la instancia de Cypress (cy) para ejecutar varios métodos, como visit(), get(), fixture(), etc., así como métodos de cadena a estos resultados.

Los métodos visit() y get(), que generalmente se usan con mucha frecuencia, también afirman que el elemento y la URL visitada existen, considerándolos como pruebas aprobadas si no arrojan errores. También son el comienzo de cada cadena, por lo tanto, son conocidos como métodos principales.

De manera similar a afirmar la existencia, puede comprobar si un elemento contiene() un valor.

El método exec() ejecuta un comando en la interfaz de línea de comandos, y el método request() envía una solicitud HTTP.

El método type() ingresa contenido textual en elementos que pueden aceptar contenido textual y click() hace clic en un elemento seleccionado.

Con solo estos pocos métodos, puede hacer mucho, y un conjunto de prueba generalmente contendrá la mayoría de estos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
describe("Testing CRUD Form", () => {
    it("Visits the addition page", () => {
        cy.visit('/addProduct');
    });
    it("Gets the input field and inputs text", () => {
        cy.get('.input-element')
          .type('Product 1');
    });
    it("Clicks the 'Add Product' button", () => {
        cy.contains('Add Product')
          .click();
    });
    it("Checks if X was added correctly", () => {
        cy.get('product-title')
          .should('have.value', 'Product 1');
    });
    it("Runs a CLI Command", () => {
        cy.exec('npm run other-service');
    });
    it("Sends POST HTTP request", () => {
        cy.request('POST', '/host/other-service/updateCustomers', { mail: 'Product 1 is out!' })
          .its('body');
    });
});

La sintaxis de Mocha utilizada en Cypress es muy simple, directa e intuitiva. El uso de los bloques describe() y it() nos permite navegar con mucha naturalidad a través de las pruebas y anotar lo que hacen.

El método should() se basa en las afirmaciones de Chai, que también son bastante intuitivas.

Finalmente, cuando esté listo para ejecutar las pruebas, puede ejecutar:

1
$ cypress run --browser chrome

Este comando ejecuta todas las pruebas registradas, hasta su finalización. Avancemos y agreguemos Cypress a un proyecto real.

Adición de Cypress a un proyecto

Elija un editor de código de su elección, abra la raíz del proyecto y navegue hasta /cypress/integration/examples/actions.specs.js para ver el código detrás de todas las pruebas que ejecuta.

Ya hay toneladas de ejemplos aquí, pero creemos nuestro propio archivo spec.js en un momento y exploremos:

  • El archivo de configuración (cypress.js)
  • Cómo usar archivos de accesorios
  • Cómo usar los comandos

El archivo de configuración, denominado cypress.js, se generará automáticamente en la raíz del proyecto y, de forma predeterminada, solo contiene un marcador de posición para la ID de su proyecto:

1
2
3
{   
   "projectId": "yourId"
}

Agreguemos la clave baseUrl y asígnele un valor apropiado. Dado que nuestra aplicación se ejecuta en el puerto 8080, en localhost vamos a señalarlo:

1
2
3
4
{
  "projectId": "4b7344",
  "baseUrl": "http://localhost:8080"
}

Ahora, vamos a crear un nuevo directorio en /integration llamado my_tests y un archivo llamado tests.spec.js. Notará que en Cypress ya le indicará la opción de ejecutar este nuevo archivo, ya que escanea de manera receptiva el directorio /integration en busca de nuevas pruebas.

Avancemos y definamos un par de pruebas en nuestro archivo tests.spec.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/// <reference types="cypress" />

/* In general, it is a good practice to store 
 all your selectors in variables, since they 
 might change in the future */
var email;
var emailSelector = '.action-email';

describe('Email Input', () => {
    /* beforeEach() which will run before every 
    it() test in the file. This is great 
    if you want to perform some common actions 
    before each test */
    beforeEach(() => {
        /* Since we defined `baseUrl` in cypress.json,
        using `/` as the value in `cy.visit()` will navigate to it.
        Adding `commads/actions` will add the value to the `baseUrl`. */
        cy.visit('/commands/actions');
        /* We are reading the example fixture file and assigning its
        value to a global variable so it is accessible in every test */
        cy.fixture('example').then((json)=>{
            email = json.email;
        });
    });

    it('Clicks on Actions, and writes the email from the fixture', () => {
        cy.get(emailSelector)
            .type(email)
            .should('have.value', email);
    });
});

El método beforeEach() se ejecuta antes de cada método it(). Esto significa que podemos configurar algunas tareas comunes allí para evitar repetirlas en cada llamada it(). Aquí, simplemente hemos navegado a localhost:8080/commands/actions. El método visit() acepta una cadena (URL) para navegar y la agrega a la baseUrl definida en el archivo de configuración.

Además, hemos configurado un accesorio. Los accesorios son solo documentos estáticos (JSON es un formato popular para almacenar datos, naturalmente), que podemos usar para inyectar datos en nuestras pruebas. También se usan comúnmente para bloquear solicitudes de red.

Aquí, leemos los datos JSON del archivo example, ubicado en cypress/fixtures/example.json, y usamos el valor extraído para asignarlo a nuestra variable email.

De esta manera, podemos usar el correo electrónico de ejemplo en las pruebas, en lugar de trabajar con literales de cadena.

Como ya hemos señalado, el método get() recupera el elemento con la clase action-email. Ya hemos guardado esta clase como la variable emailSelector. Luego, escribimos el email del archivo example.json en ese elemento, ingresándolo efectivamente.

Finalmente, afirmamos que la acción fue exitosa a través de El método should() de Chai. Avancemos y ejecutemos esta prueba:

1
$ cypress run

Lo que resulta en:

running all cypress tests via the cli

Configuración de variables globales dentro de los accesorios {#configuración de variables globales dentro de los accesorios}

Si necesitamos acceder a la variable emailSelector con mucha más regularidad que solo para estas pruebas, es posible que queramos definirla como una variable global. Este es un caso de uso perfecto para accesorios nuevamente, al que podemos acceder fácilmente a través de cy.fixture().

Vamos a crear un nuevo archivo JSON en el directorio /fixtures, llamado selectors.js que contendrá todos los selectores de nivel global para nuestra aplicación:

1
2
3
{
 "emailSelector": ".action-email"
}

Nota: También puede agregarlo a cualquiera de los archivos de dispositivos existentes pero, en general, es mejor crear nuevos archivos para los datos específicos en lugar de crear un archivo de propósito general para todo. Esto hace que la organización sea mucho más fácil y consistente.

Creación de métodos personalizados {#creación de métodos personalizados}

Además, si desea ejecutar el mismo beforeEach() en varios archivos de especificación, es posible que desee evitar esa redundancia también, agregándolo al archivo commands.js. Cualquier método agregado al archivo commands.js se registrará como un método personalizado que puede usar a través de la instancia cy. Entonces, si estamos visitando constantemente la URL de comandos/acciones, también podríamos crear un método, por ejemplo, navigateToActionsPage() que sea accesible globalmente.

El archivo commands.js se encuentra en el directorio /support:

1
2
3
Cypress.Commands.add('navigateToActionsPage', () => {
    cy.visit('/commands/actions');
})

De esta manera, podemos agregar N comandos para ejecutar y simplemente hacer referencia a ellos en lugar de escribirlos una y otra vez. Volvamos a tests.spec.js y rehagamos nuestro código para eliminar la llamada cy.visit() y usemos el método del archivo commands.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// <reference types="cypress" />

var email;
var emailSelector;

describe('Email Input', () => {
    beforeEach(() => {
        // Our method in `commands.js`
        // can now be used from anywhere 
        cy.navigateToActionsPage();
        cy.fixture('example').then((json)=>{
            email = json.email;
        });
        // We can now read the selectors fixture 
        // file and load it into our global variable 
        // to store the selector
        cy.fixture('selectors').then((json)=>{
            emailSelector = json.emailSelector;
        });
    });
    it('Clicks on Actions, and writes the email from fixture', () => {
        cy.get(emailSelector).type(email).should('have.value', email);
    });
});

Puede que no parezca una gran diferencia ahora, pero tener un proyecto en el que una página tiene, digamos, 20 campos de entrada que son propensos a cambiar significa que es necesario tener un espacio centralizado para mantener los selectores para un buen mantenimiento del código.

Asignación de alias a una solicitud XHR

Se puede usar una XMLHttpRequest (Solicitud XHR) para enviar y recuperar datos de una página web, sin tener que volver a cargarla. Originalmente se creó para la transferencia de datos XML, pero se usa mucho más comúnmente para enviar y solicitar datos JSON, aunque el nombre sugiere que es solo para XML. Este no es un escenario poco común de ver, ya que muchas aplicaciones web envían varias solicitudes y muestran las respuestas enviadas a esas solicitudes en una página web.

Podemos crear un alias para las solicitudes XHR, para probar su funcionalidad a través de Cypress. Primero, agreguemos otro método personalizado a nuestro archivo commands.js para que podamos acceder a él como un método global y usarlo en nuestros ganchos beforeEach():

1
2
3
Cypress.Commands.add('navigateToAliasingPage', () => {
    cy.visit('/commands/aliasing');
})

A continuación, creemos un nuevo archivo llamado aliasing_tests.spec.js en nuestro directorio /my_tests.

Nota: Alternativamente, también puede agregar otro fragmento de código (envuelto dentro de un describe()) en nuestro archivo existente. Aunque, en general, es una buena práctica mantener una función en un archivo y crear una nueva cuando esté probando una función diferente.

Haremos uso de los métodos cy.intercept() y cy.wait() aquí, creados para afirmar las solicitudes y respuestas de la red. El método cy.intercept() se usa para espiar y bloquear solicitudes y respuestas de la red, y reemplazó al método cy.route(). Por otro lado, el método cy.wait() se usa para esperar un tiempo fijo o hasta que se resuelva un recurso con alias.

Enviaremos una solicitud XHR al extremo /comments, esperando la respuesta y probándola. Este es exactamente el caso de uso correcto para intercept() (para bloquear la solicitud) y wait() (para esperar hasta que se resuelva el recurso devuelto).

Agreguemos un par de pruebas al archivo aliasing_tests.spec.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/// <reference types="cypress" />
context('Aliasing XHR', () => {    
  // Aliasing in beforeEach makes the route aliased in every test in this context    
  beforeEach(() => {        
    // Stub and access any XHR GET request and route to **/comments/*.         
    // The ** and * are wild cards, meaning it could be `/microservice/comments/1`
    // or in our case `https://jsonplaceholder.cypress.io/comments/1`       
    // the `as()` function allows us to later `get()` this route with any valid chainable function
    cy.intercept('GET', '**/comments/*').as('getComment');        
    cy.navigateToAliasingPage();    
  });        
  it('clicks a button and expects a comment', () => {        
    // Clicking this button will create and XHR request that generates a comment        
    cy.get('.network-btn').click()        
    // `wait()` is one of the valid chainable actions where we can use the aliased endpoint
    // `then()` will allow us to access all the elements of the response 
    // for validation whether or not this is available on UI        
    cy.wait('@getComment').then((getCommentResponse) => {            
      // `getCommentResponse` contains all the data from our response. 
      // You can investigate this in the network tab of your browser            
      // Check that the response code is what we expect            
      expect(getCommentResponse.response.statusCode).to.equal(200);            
      // Check that the `response.body` has a parameter named 'email', equal to a certain value
      expect(getCommentResponse.response.body.email).to.equal('[correo electrónico protegido]');            
      // Perform same check but for the `name` parameter            
      expect(getCommentResponse.response.body.name).to.equal('id labore ex et quam laborum');        
    });    
  });
});

Avancemos y ejecutemos esta prueba:

1
$ cypress run --record --spec "cypress/integration/my_tests/aliasing_tests.spec.js"

Lo que resulta en:

enviando solicitudes XHR con cypress

Simulacro de respuestas de solicitud XHR

Otra característica muy útil a tener en cuenta es el hecho de que puede omitir por completo el proceso de creación del comentario de la sección anterior. Puede crear su propia respuesta simulada bloqueando esta solicitud de red usando cy.intercept(). Esto es útil cuando el back-end aún no está desarrollado, por lo que puede simular la respuesta antes de que finalice, o simplemente desea probar solo esta parte de la aplicación.

Aquí también tenemos otro uso para los archivos de accesorios. Vamos a crear uno llamado mock_comment.json que contendrá los datos simulados de un comentario y agregue los siguientes contenidos JSON:

1
2
3
4
5
6
7
{  
  "postId": 1,  
  "id": 1,  
  "name": "My Name",  
  "email": "[correo electrónico protegido]",  
  "body": "My Comment Body"
}

Ahora, creemos otro archivo, llamado intercepting_requests.spec.js en el directorio /my_tests. Aquí, interceptaremos el mismo punto final, pero inyectaremos nuestro dispositivo como respuesta, omitiendo por completo la solicitud real:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/// <reference types="cypress" />
describe('Intercepting XHR', () => {
  beforeEach(() => {       
    // By adding an object `{fixture: 'mock_comment.json'}` in the `intercept()` call,
    // we are telling cypress to use the JSON file as the response.      
    // It can also be aliased using `.as()`.  
    cy.intercept('GET', '**/comments/*',
                 {fixture: 'mock_comment.json'}).as('getComment');       
    cy.navigateToAliasingPage();    
  });        
  it('Clicks a button and expects a comment', () => {        
    cy.get('.network-btn').click()        
    // There is no need to validate the response now since we mocked it,
    // but there is a need to validate the UI        
    cy.fixture('mock_comment').then((json)=>{           
      // We are accessing the comment directly from `mock_comment.json`
      // and checking that the UI is displaying it in its fullest         
      cy.get('.network-comment').should('have.text', json.body);        
    });    
  });
});

Hagamos esta prueba:

1
$ cypress run --record --spec "cypress/integration/my_tests/intercepting_requests.spec.js"

Lo que resulta en:

mocking XHR response for cypress

Conclusión

Cypress es una gran herramienta de prueba emergente. Es súper liviano y fácil de configurar, e increíble para TDD, ya que se construyó sobre Mocha y Chai. Le permite simular completamente su back-end, lo que también es excelente para probar antes de integrarse con su back-end, o en caso de que su back-end aún no exista. También podría ayudar a dar forma al back-end en algunos casos, ya que describirá exactamente lo que espera el front-end.

Sin embargo, si está buscando una herramienta que sea súper flexible en lo que puede cubrir, y necesita un marco grande, personalizado y personalizado, es posible que desee apegarse a Selenium. enium.