Saltar al contenido

e

Pruebas de extremo a extremo: Cypress

Cypress ha sido la librería de pruebas E2E más popular durante los últimos años, pero Playwright está ganando terreno rápidamente. Este curso ha estado usando Cypress durante años. Ahora, Playwright es una nueva adición. Puedes elegir si completar la parte de pruebas E2E del curso con Cypress o Playwright. Los principios operativos de ambas librerías son muy similares, así que tu elección no es muy importante. Sin embargo, ahora Playwright es la librería E2E preferida para el curso.

Si tu elección es Cypress, por favor continúa. Si terminas usando Playwright, ve aquí.

Cypress

La librería E2E Cypress se ha vuelto popular durante los últimos años. Cypress es excepcionalmente fácil de usar y, en comparación con Selenium, por ejemplo, requiere mucha menos molestia y dolor de cabeza. Su principio operativo es radicalmente diferente al de la mayoría de las librerías de prueba E2E, porque las pruebas Cypress se ejecutan completamente dentro del navegador. Otras librerías ejecutan las pruebas en un proceso de Node, que está conectado al navegador a través de una API.

Hagamos algunas pruebas de extremo a extremo para nuestra aplicación de notas.

A diferencia de las pruebas de backend o las pruebas unitarias realizadas en el front-end de React, las pruebas de extremo a extremo no necesitan estar ubicadas en el mismo proyecto npm donde está el código. Hagamos un proyecto completamente separado para las pruebas E2E con el comando npm init. Luego instala Cypress en el nuevo proyecto como una dependencia de desarrollo.

npm install --save-dev cypress

y agregando un script npm para ejecutarlo:

{
  // ...
  "scripts": {
    "cypress:open": "cypress open"  },
  // ...
}

También le hacemos un pequeño cambio al script que inicia la aplicación, sin este cambio Cypress no puede acceder a ella.

A diferencia de las pruebas unitarias del frontend, las pruebas de Cypress pueden estar en el repositorio frontend o backend, o incluso en su propio repositorio separado.

Las pruebas requieren que el sistema bajo prueba esté funcionando. A diferencia de nuestras pruebas de integración de backend, las pruebas de Cypress no inician el sistema cuando se ejecutan.

Agreguemos un script npm al backend que lo inicia en modo de prueba, o para que NODE\_ENV sea test.

{
  // ...
  "scripts": {
    "start": "NODE_ENV=production node index.js",
    "dev": "NODE_ENV=development nodemon index.js",
    "build:ui": "rm -rf build && cd ../frontend/ && npm run build && cp -r build ../backend",
    "deploy": "fly deploy",
    "deploy:full": "npm run build:ui && npm run deploy",
    "logs:prod": "fly logs",
    "lint": "eslint .",
    "test": "jest --verbose --runInBand",
    "start:test": "NODE_ENV=test node index.js"  },
  // ...
}

NB Para conseguir que Cypress funcione con WSL2 se debe realizar una configuración preliminar. Estos dos enlaces son buenos lugares para iniciar.

Cuando tanto el backend como el frontend están ejecutándose, podemos iniciar Cypress con el comando

npm run cypress:open

Cypress nos pregunta qué tipo de prueba realizaremos. Debemos elegir "E2E Testing":

flecha apuntando a opción e2e en menú de cypress

A continuación debemos elegir un navegador (por ejemplo Chrome) y luego debemos hacer click en "Create new spec":

flecha apuntando a crear nuevo spec en menú de cypress

Creemos el archivo de prueba cypress/e2e/note_app.cy.js:

ubicación de archivo de prueba de cypress en cypress/e2e/note_app.cy.js

Podemos editar la prueba en Cypress, pero usemos en cambio VS Code:

vscode mostrando cambios en la prueba y cypress mostrando que la prueba fue agregada

Ahora podemos cerrar la vista de edición de Cypress.

Cambiemos el contenido de la prueba como se muestra a continuación:

describe('Note app', function() {
  it('front page can be opened', function() {
    cy.visit('http://localhost:5173')
    cy.contains('Notes')
    cy.contains('Note app, Department of Computer Science, University of Helsinki 2023')
  })
})

La prueba se ejecuta haciendo clic en ella en Cypress:

Ejecutar la prueba muestra cómo se comporta la aplicación mientras esta se ejecuta:

cypress mostrando la automatización de la prueba de notas

La estructura de la prueba debería resultar familiar. Utilizan bloques describe para agrupar diferentes casos de prueba, al igual que Jest. Los casos de prueba se han definido con el método it. Cypress tomó estas partes de la librería de pruebas Mocha a la que utiliza bajo el capó.

cy.visit y cy.contains son comandos de Cypress, y su propósito es bastante obvio. cy.visit abre la dirección web dada como parámetro en el navegador utilizado por la prueba. cy.contains busca la cadena que recibió como parámetro en la página.

Podríamos haber declarado la prueba usando una función de flecha

describe('Note app', () => {  it('front page can be opened', () => {    cy.visit('http://localhost:5173')
    cy.contains('Notes')
    cy.contains('Note app, Department of Computer Science, University of Helsinki 2023')
  })
})

Sin embargo, Mocha recomienda que no se utilicen funciones de flecha, porque podrían causar algunos problemas en ciertas situaciones.

Si cy.contains no encuentra el texto que está buscando, la prueba no pasa. Por lo tanto, si extendemos nuestra prueba de la siguiente manera

describe('Note app', function() {
  it('front page can be opened',  function() {
    cy.visit('http://localhost:5173')
    cy.contains('Notes')
    cy.contains('Note app, Department of Computer Science, University of Helsinki 2023')
  })

  it('front page contains random text', function() {    cy.visit('http://localhost:5173')    cy.contains('wtf is this app?')  })})

la prueba falla

cypress mostrando falla al esperar encontrar wtf pero no

Eliminemos el código que falla de la prueba.

La variable cy que utilizan nuestras pruebas nos da un error Eslint molesto

captura de pantalla de vscode mostrando que cy no está definido

Podemos deshacernos de él instalando eslint-plugin-cypress como una dependencia de desarrollo

npm install eslint-plugin-cypress --save-dev

y cambiando la configuración en .eslintrc.cjs de la siguiente manera:

module.exports = {
  "env": {
    browser: true,
    es2020: true,
    "jest/globals": true,
    "cypress/globals": true  },
  "extends": [ 
    // ...
  ],
  "parserOptions": {
    // ...
  },
  "plugins": [
      "react", "jest", "cypress"  ],
  "rules": {
    // ...
  }
}

Escribiendo en un formulario

Extendamos nuestras pruebas para que nuestra nueva prueba intente iniciar sesión en nuestra aplicación. Suponemos que nuestro backend contiene un usuario con el nombre de usuario mluukkai y la contraseña salainen.

La prueba comienza abriendo el formulario de inicio de sesión.

describe('Note app',  function() {
  // ...

  it('login form can be opened', function() {
    cy.visit('http://localhost:5173')
    cy.contains('log in').click()
  })
})

La prueba primero busca el botón de inicio de sesión por su texto y hace clic en el botón con el comando cy.click.

Nuestras dos pruebas comienzan de la misma manera, abriendo la página http://localhost:5173, por lo que deberíamos extraer el código compartido en un bloque beforeEach que se ejecuta antes de cada prueba:

describe('Note app', function() {
  beforeEach(function() {    cy.visit('http://localhost:5173')  })
  it('front page can be opened', function() {
    cy.contains('Notes')
    cy.contains('Note app, Department of Computer Science, University of Helsinki 2023')
  })

  it('login form can be opened', function() {
    cy.contains('log in').click()
  })
})

El campo de inicio de sesión contiene dos campos de input, en los que la prueba debe escribir.

El comando cy.get permite buscar elementos mediante selectores CSS.

Podemos acceda al primer y último campo de input de la página, y escribir en ellos con el comando cy.type así:

it('user can login', function () {
  cy.contains('log in').click()
  cy.get('input:first').type('mluukkai')
  cy.get('input:last').type('salainen')
})  

La prueba funciona. El problema es que si luego agregamos más campos de input, la prueba se interrumpirá porque espera que los campos que necesita sean el primero y el último en la página.

Sería mejor dar a nuestros inputs IDs únicos y usarlos para encontrarlos. Cambiamos nuestro formulario de inicio de sesión de la siguiente manera:

const LoginForm = ({ ... }) => {
  return (
    <div>
      <h2>Login</h2>
      <form onSubmit={handleSubmit}>
        <div>
          username
          <input
            id='username'            value={username}
            onChange={handleUsernameChange}
          />
        </div>
        <div>
          password
          <input
            id='password'            type="password"
            value={password}
            onChange={handlePasswordChange}
          />
        </div>
        <button id="login-button" type="submit">          login
        </button>
      </form>
    </div>
  )
}

También agregamos una ID a nuestro botón submit para que podamos acceder a él en nuestras pruebas.

La prueba se convierte en:

describe('Note app',  function() {
  // ..
  it('user can log in', function() {
    cy.contains('log in').click()
    cy.get('#username').type('mluukkai')    cy.get('#password').type('salainen')    cy.get('#login-button').click()
    cy.contains('Matti Luukkainen logged in')  })
})

La última línea asegura que el inicio de sesión fue exitoso.

Ten en cuenta que el selector de ID de CSS es #, así que si queremos buscar un elemento con el ID username el selector de CSS es #username.

Por favor, ten en cuenta que para que la prueba pase en esta etapa, es necesario que haya un usuario en la base de datos de pruebas del entorno de test del backend, cuyo nombre de usuario sea mluukkai y la contraseña sea salainen. ¡Crea un usuario si es necesario!

Probando el formulario para agregar notas

A continuación, agreguemos pruebas para probar la funcionalidad "new note":

describe('Note app', function() {
  // ..
  describe('when logged in', function() {    beforeEach(function() {      cy.contains('log in').click()      cy.get('input:first').type('mluukkai')      cy.get('input:last').type('salainen')      cy.get('#login-button').click()    })
    it('a new note can be created', function() {      cy.contains('new note').click()      cy.get('input').type('a note created by cypress')      cy.contains('save').click()      cy.contains('a note created by cypress')    })  })})

La prueba se ha definido en su propio bloque describe. Solo los usuarios registrados pueden crear nuevas notas, por lo que agregamos el inicio de sesión en la aplicación en un bloque beforeEach.

La prueba confía en que al crear una nueva nota, la página contiene solo una entrada, por lo que la busca así:

cy.get('input')

Si la página tuviera más inputs, la prueba se rompería

error de cypress: cy.type solo puede ser llamado en un elemento individual

Debido a esto, nuevamente sería mejor darle al input un ID y buscar el elemento por su ID.

La estructura de las pruebas se ve así:

describe('Note app', function() {
  // ...

  it('user can log in', function() {
    cy.contains('log in').click()
    cy.get('#username').type('mluukkai')
    cy.get('#password').type('salainen')
    cy.get('#login-button').click()

    cy.contains('Matti Luukkainen logged in')
  })

  describe('when logged in', function() {
    beforeEach(function() {
      cy.contains('log in').click()
      cy.get('input:first').type('mluukkai')
      cy.get('input:last').type('salainen')
      cy.get('#login-button').click()
    })

    it('a new note can be created', function() {
      // ...
    })
  })
})

Cypress ejecuta las pruebas en el orden en que están en el código. Entonces, primero ejecuta user can log in, donde el usuario inicia sesión. Entonces cypress ejecutará a new note can be created para la cual el bloque beforeEach también inicia sesión. ¿Por qué hacer esto? ¿No inició sesión el usuario después de la primera prueba? No, porque cada prueba comienza desde cero en lo que respecta al navegador. Todos los cambios en el estado del navegador se invierten después de cada prueba.

Controlando el estado de la base de datos

Si las pruebas necesitan poder modificar la base de datos del servidor, la situación inmediatamente se vuelve más complicada. Idealmente, la base de datos del servidor debería ser la misma cada vez que ejecutamos las pruebas, para que nuestras pruebas se puedan repetir de forma fiable y sencilla.

Al igual que con las pruebas unitarias y de integración, con las pruebas E2E es mejor vaciar la base de datos y posiblemente formatearla antes de ejecutar las pruebas. El desafío con las pruebas E2E es que no tienen acceso a la base de datos.

La solución es crear endpoints de API en el backend para la prueba. Podemos vaciar la base de datos usando estos endpoints. Creemos un nuevo enrutador para las pruebas dentro de la carpeta controllers, en el archivo testing.js

const testingRouter = require('express').Router()
const Note = require('../models/note')
const User = require('../models/user')

testingRouter.post('/reset', async (request, response) => {
  await Note.deleteMany({})
  await User.deleteMany({})

  response.status(204).end()
})

module.exports = testingRouter

y agrégalo al backend solo si la aplicación se ejecuta en modo de prueba:

// ...

app.use('/api/login', loginRouter)
app.use('/api/users', usersRouter)
app.use('/api/notes', notesRouter)

if (process.env.NODE_ENV === 'test') {  const testingRouter = require('./controllers/testing')  app.use('/api/testing', testingRouter)}
app.use(middleware.unknownEndpoint)
app.use(middleware.errorHandler)

module.exports = app

Después de los cambios, una solicitud POST HTTP al endpoint /api/testing/reset vacía la base de datos. Asegúrate de que tu backend esté ejecutándose en modo de prueba iniciándolo con este comando (previamente configurado en el archivo package.json):

  npm run start:test

El código de backend modificado se puede encontrar en GitHub, rama part5-1.

A continuación, cambiaremos el bloque beforeEach para que vacíe la base de datos del servidor antes de ejecutar las pruebas.

Actualmente no es posible agregar nuevos usuarios a través de la interfaz de usuario del frontend, por lo que agregamos un nuevo usuario al backend desde el bloque beforeEach.

describe('Note app', function() {
   beforeEach(function() {
    cy.request('POST', 'http://localhost:3001/api/testing/reset')    const user = {      name: 'Matti Luukkainen',      username: 'mluukkai',      password: 'salainen'    }    cy.request('POST', 'http://localhost:3001/api/users/', user)     cy.visit('http://localhost:5173')
  })
  
  it('front page can be opened', function() {
    // ...
  })

  it('user can login', function() {
    // ...
  })

  describe('when logged in', function() {
    // ...
  })
})

Durante el formateo, la prueba realiza solicitudes HTTP al backend con cy.request.

A diferencia de antes, ahora la prueba comienza con el backend en el mismo estado cada vez. El backend contendrá un usuario y ninguna nota.

Agreguemos una prueba más para verificar que podemos cambiar la importancia de notas.

Anteriormente cambiamos el frontend para que una nueva nota se importante por defecto, por lo que el campo important es true:

const NoteForm = ({ createNote }) => {
  // ...

  const addNote = (event) => {
    event.preventDefault()
    createNote({
      content: newNote,
      important: true    })

    setNewNote('')
  }
  // ...
} 

Hay varias formas de probar esto. En el siguiente ejemplo, primero buscamos una nota y hacemos clic en su botón make not important. Luego verificamos que la nota ahora contenga un botón make important.

describe('Note app', function() {
  // ...

  describe('when logged in', function() {
    // ...

    describe('and a note exists', function () {
      beforeEach(function () {
        cy.contains('new note').click()
        cy.get('input').type('another note cypress')
        cy.contains('save').click()
      })

      it('it can be made not important', function () {
        cy.contains('another note cypress')
          .contains('make not important')
          .click()

        cy.contains('another note cypress')
          .contains('make important')
      })
    })
  })
})

El primer comando busca un componente que contenga el texto another note cypress, y luego busca un botón make not important dentro de él. Luego hace clic en el botón.

El segundo comando comprueba que el texto del botón haya cambiado a make important.

Prueba de inicio de sesión fallida

Hagamos una prueba para asegurarnos de que un intento de inicio de sesión falla si la contraseña es incorrecta.

Cypress ejecutará todas las pruebas cada vez de forma predeterminada y, a medida que aumenta el número de pruebas, comienza a consumir bastante tiempo. Al desarrollar una nueva prueba o al depurar una prueba rota, podemos definir la prueba con it.only en lugar de it, de modo que Cypress solo ejecutará la prueba requerida. Cuando la prueba esté funcionando, podemos eliminar .only.

La primera versión de nuestras pruebas es la siguiente:

describe('Note app', function() {
  // ...

  it.only('login fails with wrong password', function() {
    cy.contains('log in').click()
    cy.get('#username').type('mluukkai')
    cy.get('#password').type('wrong')
    cy.get('#login-button').click()

    cy.contains('wrong credentials')
  })

  // ...
)}

La prueba utiliza cy.contains para garantizar que la aplicación imprima un mensaje de error.

La aplicación muestra el mensaje de error en un componente con la clase CSS error:

const Notification = ({ message }) => {
  if (message === null) {
    return null
  }

  return (
    <div className="error">      {message}
    </div>
  )
}

Podríamos hacer que la prueba asegure que el mensaje de error se renderiza al componente correcto, es decir, al componente con la clase CSS error:

it('login fails with wrong password', function() {
  // ...

  cy.get('.error').contains('wrong credentials')})

Primero usamos cy.get para buscar un componente con la clase CSS error. Luego verificamos que el mensaje de error se pueda encontrar en este componente. Ten en cuenta que los selectores de clase CSS comienzan con un punto final, por lo que el selector para la clase error es .error.

Podríamos hacer lo mismo usando la sintaxis should:

it('login fails with wrong password', function() {
  // ...

  cy.get('.error').should('contain', 'wrong credentials')})

Usar should es un poco más complicado que usar contains, pero permite pruebas más diversas que contains, que funciona solo con contenido de texto.

La lista de las aserciones más comunes con las que se puede usar should se puede encontrar aquí.

Podemos, por ejemplo, asegurarnos de que el mensaje de error sea rojo y tenga un borde:

it('login fails with wrong password', function() {
  // ...

  cy.get('.error').should('contain', 'wrong credentials') 
  cy.get('.error').should('have.css', 'color', 'rgb(255, 0, 0)')
  cy.get('.error').should('have.css', 'border-style', 'solid')
})

Cypress requiere que los colores se den como rgb.

Debido a que todas las pruebas son para el mismo componente al que accedimos usando cy.get, podemos encadenarlos usando and.

it('login fails with wrong password', function() {
  // ...

  cy.get('.error')
    .should('contain', 'wrong credentials')
    .and('have.css', 'color', 'rgb(255, 0, 0)')
    .and('have.css', 'border-style', 'solid')
})

Terminemos la prueba para que también verifique que la aplicación no muestre el mensaje de éxito 'Matti Luukkainen logged in':

it('login fails with wrong password', function() {
  cy.contains('log in').click()
  cy.get('#username').type('mluukkai')
  cy.get('#password').type('wrong')
  cy.get('#login-button').click()

  cy.get('.error')
    .should('contain', 'wrong credentials')
    .and('have.css', 'color', 'rgb(255, 0, 0)')
    .and('have.css', 'border-style', 'solid')

  cy.get('html').should('not.contain', 'Matti Luukkainen logged in')})

El comando should se usa más frecuentemente encadenándolo después del comando get (o otro comando similar que pueda ser encadenado). El cy.get('html') usado en la prueba prácticamente significa el contenido visible de toda la aplicación.

También podríamos verificar lo mismo encadenando el comando contains con el comando should con un parámetro ligeramente diferente:

cy.contains('Matti Luukkainen logged in').should('not.exist')

NOTA: Algunas propiedades CSS se comportan de manera diferente en Firefox. Si ejecutas las pruebas con Firefox:

running

entonces las pruebas que involucran, por ejemplo, border-style, border-radius y padding, pasarán en Chrome o Electron, pero fallarán en Firefox:

borderstyle

Omitiendo la interfaz de usuario

Actualmente tenemos las siguientes pruebas:

describe('Note app', function() {
  it('user can login', function() {
    cy.contains('log in').click()
    cy.get('#username').type('mluukkai')
    cy.get('#password').type('salainen')
    cy.get('#login-button').click()

    cy.contains('Matti Luukkainen logged in')
  })

  it('login fails with wrong password', function() {
    // ...
  })

  describe('when logged in', function() {
    beforeEach(function() {
      cy.contains('log in').click()
      cy.get('input:first').type('mluukkai')
      cy.get('input:last').type('salainen')
      cy.get('#login-button').click()
    })

    it('a new note can be created', function() {
      // ... 
    })
   
  })
})

Primero probamos el inicio de sesión. Luego, en su propio bloque de descripción, tenemos un montón de pruebas que esperan que el usuario inicie sesión. El usuario ha iniciado sesión en el bloque beforeEach.

Como dijimos anteriormente, ¡cada prueba comienza desde cero! Las pruebas no comienzan en el estado donde terminaron las pruebas anteriores.

La documentación de Cypress nos da el siguiente consejo: Prueba completamente el flujo de inicio de sesión, ¡pero solo una vez!. Por lo tanto, en lugar de iniciar sesión como usuario mediante el formulario en el bloque beforeEach, vamos a omitir la interfaz de usuario y realizaremos una solicitud HTTP al backend para iniciar sesión. La razón de esto es que iniciar sesión con una solicitud HTTP es mucho más rápido que completar un formulario.

Nuestra situación es un poco más complicada que en el ejemplo de la documentación de Cypress, porque cuando un usuario inicia sesión, nuestra aplicación guarda sus detalles en localStorage. Sin embargo, Cypress también puede manejar esto. El código es el siguiente:

describe('when logged in', function() {
  beforeEach(function() {
    cy.request('POST', 'http://localhost:3001/api/login', {      username: 'mluukkai', password: 'salainen'    }).then(response => {      localStorage.setItem('loggedNoteappUser', JSON.stringify(response.body))      cy.visit('http://localhost:5173')    })  })

  it('a new note can be created', function() {
    // ...
  })

  // ...
})

Podemos acceder a la respuesta de un cy.request con el método then. Debajo del capó, cy.request, al igual que todos los comandos de Cypress, son asíncronos. La función de callback guarda los detalles de un usuario conectado en localStorage y recarga la página. Ahora no hay diferencia con un usuario que inicia sesión a través del formulario de inicio de sesión.

Si cuando escribimos nuevas pruebas en nuestra aplicación, tenemos que usar el código de inicio de sesión en varios lugares, deberíamos convertirlo en un comando personalizado.

Los comandos personalizados se declaran en cypress/support/commands.js. El código para iniciar sesión es el siguiente:

Cypress.Commands.add('login', ({ username, password }) => {
  cy.request('POST', 'http://localhost:3001/api/login', {
    username, password
  }).then(({ body }) => {
    localStorage.setItem('loggedNoteappUser', JSON.stringify(body))
    cy.visit('http://localhost:5173')
  })
})

Usar nuestro comando personalizado es fácil y nuestra prueba se vuelve más limpia:

describe('when logged in', function() {
  beforeEach(function() {
    cy.login({ username: 'mluukkai', password: 'salainen' })  })

  it('a new note can be created', function() {
    // ...
  })

  // ...
})

Lo mismo se aplica a la creación de una nueva nota ahora que pensamos sobre ello. Tenemos una prueba que hace una nueva nota usando el formulario. También hacemos una nueva nota en el bloque beforeEach de la prueba que cambia la importancia de una nota:

describe('Note app', function() {
  // ...

  describe('when logged in', function() {
    it('a new note can be created', function() {
      cy.contains('new note').click()
      cy.get('input').type('a note created by cypress')
      cy.contains('save').click()

      cy.contains('a note created by cypress')
    })

    describe('and a note exists', function () {
      beforeEach(function () {
        cy.contains('new note').click()
        cy.get('input').type('another note cypress')
        cy.contains('save').click()
      })

      it('it can be made important', function () {
        // ...
      })
    })
  })
})

Creemos un nuevo comando personalizado para crear una nueva nota. El comando creará una nueva nota con una solicitud HTTP POST:

Cypress.Commands.add('createNote', ({ content, important }) => {
  cy.request({
    url: 'http://localhost:3001/api/notes',
    method: 'POST',
    body: { content, important },
    headers: {
      'Authorization': `Bearer ${JSON.parse(localStorage.getItem('loggedNoteappUser')).token}`
    }
  })

  cy.visit('http://localhost:5173')
})

El comando espera que el usuario haya iniciado sesión y que los detalles del usuario estén guardados en localStorage.

Ahora el bloque beforeEach de la nota se convierte en:

describe('Note app', function() {
  // ...

  describe('when logged in', function() {
    it('a new note can be created', function() {
      // ...
    })

    describe('and a note exists', function () {
      beforeEach(function () {
        cy.createNote({          content: 'another note cypress',          important: true        })      })

      it('it can be made important', function () {
        // ...
      })
    })
  })
})

Hay otra cosa en nuestras pruebas que es molesta. La URL de nuestra aplicación http://localhost:5173 esta codificada literalmente en varios lugares.

Definamos la URL de nuestra aplicación baseUrl en el archivo de configuración pre-generado de Cypress cypress.config.js:

const { defineConfig } = require("cypress")

module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
    },
    baseUrl: 'http://localhost:5173'  },
})

Todos los comandos en las pruebas que usan la dirección de la aplicación

cy.visit('http://localhost:5173')

se pueden cambiar a

cy.visit('')

La dirección codificada del backend, http://localhost:3001, todavía está en las pruebas. La documentación de Cypress recomienda definir otras direcciones utilizadas por las pruebas como variables de entorno.

Expandamos el archivo de configuración cypress.config.js de la siguiente manera:

const { defineConfig } = require("cypress")

module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
    },
    baseUrl: 'http://localhost:5173',
    env: {
      BACKEND: 'http://localhost:3001/api'    }
  },
})

Reemplacemos todas las direcciones del backend en las pruebas de la siguiente manera:

describe('Note ', function() {
  beforeEach(function() {

    cy.request('POST', `${Cypress.env('BACKEND')}/testing/reset`)    const user = {
      name: 'Matti Luukkainen',
      username: 'mluukkai',
      password: 'secret'
    }
    cy.request('POST', `${Cypress.env('BACKEND')}/users`, user)    cy.visit('')
  })
  // ...
})

Cambiando la importancia de una nota

Por último, echemos un vistazo a la prueba que hicimos para cambiar la importancia de una nota. Primero cambiaremos el bloque beforeEach para que cree tres notas en lugar de una:

describe('when logged in', function() {
  describe('and several notes exist', function () {
    beforeEach(function () {
      cy.login({ username: 'mluukkai', password: 'salainen' })      cy.createNote({ content: 'first note', important: false })      cy.createNote({ content: 'second note', important: false })      cy.createNote({ content: 'third note', important: false })    })

    it('one of those can be made important', function () {
      cy.contains('second note')
        .contains('make important')
        .click()

      cy.contains('second note')
        .contains('make not important')
    })
  })
})

¿Cómo funciona realmente el comando cy.contains?

Cuando hacemos clic en el comando cy.contains('second note') en Cypress Test Runner, vemos que ese comando busca el elemento que contiene el texto second note:

cypress test runner haciendo clic en la segunda nota

Al hacer clic en la línea siguiente .contains('make important') vemos que la prueba utiliza el botón 'make important' correspondiente a la segunda nota:

cypress test runner haciendo clic en make important

Cuando está encadenado, el segundo comando contains continúa la búsqueda desde dentro del componente encontrado por el primer comando.

Si no hubiéramos encadenado los comandos, y en su lugar hubiéramos escrito

cy.contains('second note')
cy.contains('make important').click()

el resultado habría sido totalmente diferente. La segunda línea de la prueba haría clic en el botón de una nota incorrecta:

cypress mostrando error e intentando hacer clic incorrectamente en el primer botón

Al escribir pruebas, ¡debes verificar en el ejecutor de pruebas que las pruebas utilicen los componentes correctos!

Cambiemos el componente Note para que el texto de la nota se renderice en un span .

const Note = ({ note, toggleImportance }) => {
  const label = note.important
    ? 'make not important' : 'make important'

  return (
    <li className='note'>
      <span>{note.content}</span>      <button onClick={toggleImportance}>{label}</button>
    </li>
  )
}

¡Nuestras pruebas se rompen! Como revela el test runner, cy.contains('second note') ahora devuelve el componente que contiene el texto y el botón no está en él.

cypress mostrando que la prueba está rota intentando hacer clic en "make important"

Una forma de solucionarlo es la siguiente:

it('one of those can be made important', function () {
  cy.contains('second note').parent().find('button').click()
  cy.contains('second note').parent().find('button')
    .should('contain', 'make not important')
})

En la primera línea, usamos el comando parent para acceder al elemento padre del elemento que contiene second note y buscamos el botón dentro de él. Luego hacemos clic en el botón y verificamos que el texto cambie.

Ten en cuenta que usamos el comando find para buscar el botón. No podemos usar cy.get aquí, porque siempre busca desde la página completa y devolvería los 5 botones en la pagina.

Desafortunadamente, ahora tenemos algo de copia-pega en las pruebas, porque el código para buscar el botón correcto es siempre el mismo.

En este tipo de situaciones, es posible usar el comando as:

it('one of those can be made important', function () {
  cy.contains('second note').parent().find('button').as('theButton')
  cy.get('@theButton').click()
  cy.get('@theButton').should('contain', 'make not important')
})

Ahora la primera línea encuentra el botón correcto y usa as para guardarlo como theButton. Las siguientes líneas pueden usar el elemento nombrado con cy.get('@theButton').

Ejecutando y depurando tus pruebas

Finalmente, algunas notas sobre cómo funciona Cypress y la depuración de tus pruebas.

Debido a la forma de las pruebas de Cypress, da la impresión de que son código JavaScript normal y, por ejemplo, podríamos intentar esto:

const button = cy.contains('log in')
button.click()
debugger
cy.contains('logout').click()

Sin embargo, esto no funcionará. Cuando Cypress ejecuta una prueba, agrega cada comando cy a una cola de ejecución. Cuando se haya ejecutado el código del método de prueba, Cypress ejecutará cada comando en la cola uno por uno.

Los comandos de Cypress siempre devuelven undefined, por lo que button.click() en el código anterior causaría un error. Un intento de iniciar el depurador no detendría el código entre la ejecución de los comandos, sino antes de que se haya ejecutado algún comando.

Los comandos de Cypress son como promesas, así que si queremos acceder a sus valores de retorno, tenemos que hacerlo usando el comando then. Por ejemplo, la siguiente prueba imprime el número de botones en la aplicación y hace clic en el primer botón:

it('then example', function() {
  cy.get('button').then( buttons => {
    console.log('number of buttons', buttons.length)
    cy.wrap(buttons[0]).click()
  })
})

Detener la ejecución de la prueba con el depurador es posible. El depurador se inicia solo si la consola para desarrolladores del test runner de Cypress está abierta.

La Consola para desarrolladores es muy útil para depurar tus pruebas. Puedes ver las solicitudes HTTP realizadas por las pruebas en la pestaña Network, y la pestaña Console te mostrará información sobre tus pruebas:

consola para desarrolladores mientras se ejecuta Cypress

Hasta ahora hemos ejecutado nuestras pruebas Cypress usando el test runner gráfico. También es posible ejecutarlas desde la línea de comandos. Solo tenemos que agregarle un script npm:

  "scripts": {
    "cypress:open": "cypress open",
    "test:e2e": "cypress run"  },

Ahora podemos ejecutar nuestras pruebas desde la línea de comandos con el comando npm run test:e2e

Salida de terminal al ejecutar las pruebas npm e2e mostrando aprobadas

Ten en cuenta que los videos de la ejecución de las pruebas se guardarán en cypress/videos/, por lo que probablemente deberías ignorar este directorio en git. También es posible desactivar la creación de videos.

Las pruebas se encuentran en GitHub.

La versión final del código frontend se puede encontrar en la rama GitHub part5-9.