Pruebas de APIs con Cypress: POST, PUT y DELETE
Clase 9 de 17 • Curso de Automatización de Pruebas de Backend con Cypress
Contenido del curso
Clase 9 de 17 • Curso de Automatización de Pruebas de Backend con Cypress
Contenido del curso
Emmanuel Rodríguez
Jairo Campos Ruiz
Emmanuel Rodríguez
Antonio Hernandez
Jesús Manuel Mejía Rojas
Diego Fernando Gómez Álvarez
Pablo Herrera
Fabián Alejandro Oger
Francisco Antonio Hernandez Sabatino
Leonardo Cristancho
Javier Fuentes Mora
Javier Fuentes Mora
Nicolas Cardona
Javier Fuentes Mora
gloria alejandra mejia alvarez
Mario Alexander Vargas Celis
Luis Alberto Juarez Corona
Mock responses
. Esto esta genial cuando desde nuestro Frontend tenemos la posibilidad de utilizar un server como propio, mejorando su efecto, cada vez que por UI se lanza una petición al API, al interceptar las operaciones con interceptor. . Aquí, tenemos que tener cuidado con un detalle ya que en nuestro backend (utilizando express / nest / etc.), no podemos lanzar un CRUD porque recordemos que afectaríamos en directo a nuestra base de datos. . Para ello, generamos "un espejo" o mock de la capa concerniente para que, cuando lancemos nuestros E2E, obtengamos por ruta una respuesta deseada o proceso específico. . Por ejemplo, supongamos que tenemos la capa del modelo de la siguiente forma:
// User.entity.ts type TQUser = { id: number } // User.model.ts export default class User { // Static response without using the parameter findMany(query: TQUser) { return { "id": 2, "name": "Antonina Fawltey", "email": "afawltey1@uol.com.br" } } }
Entonces, ya que Cypress incorpora SinonJS (una librería similar a Supertest / SuperAgent para pruebas E2E), podemos generar un mock de nuestra capa y por ende, realizar prueba tipo Unit Test.
import User from './models/User.model' describe('Testing User Routes', () => { beforeEach(function () { cy.fixture('user').then((user) => this.user = user) cy.stub(User.prototype, 'findMany', () => this.user) }) it('should be success and valid response', function () { const user = new User() const response = user.findMany({id: 2}) expect(response).to.deep.equal(this.user) expect(response).to.have.property('name') expect(response).to.have.property('email') }) })
Aquí, uso un fixture para homologar mi información, obteniendo dicho archivo en cypress/fixtures/user.json:
{ "id": 1, "name": "User Bot", "email": "userBot@email.com" }
Después podríamos lanzar peticiones directas al server:
const findEmployees = (response: any) => { expect(response.status).to.eq(200) expect(response.body).to.be.an('object') expect(employee).to.have.property('name') expect(employee).to.have.property('email') } describe("Testing blocks", () => { it('should be success and valid response', function () { cy.request("/employees/1").as("employee") cy.get("@employees").should((response: any) => findEmployees(response)) }) })
Cypress nos ofrece una manera muy fácil de tener json, imágenes y un sinfín de archivos para poder utilizarlos en las pruebas, por ejemplo si tenemos una api donde tenemos que iniciar sesión con un usuario para hacer peticiones para no tener que escribir esto una y otra vez.
Para poder usarlo vamos a la carpeta fixtures y creamos un archivo json con los datos de employee
{ "first_name": "Test", "last_name": "Last Name", "email": "Test@gmail.com", "gender": "Male" }
Luego en el test dentro del describe hacemos un beforeEach
beforeEach(function () { //Es para poder cargar json de la carpeta fixtures, por ejemplo //si tenemos usuarios de ejemplo con diferentes niveles de privilegios //y queremos estar usándolos continuamente cy.fixture("employee.json").then((employee) => { this.employee = employee; }); });
Ahora en los test de crear, comprobar y demás podemos hacer uso de los datos que precargamos, no se os puede olvidar que para hacer uso del this las funciones no pueden ser lambdas.
it("Debe crear un empleado", function () { cy.request({ url: "employees", method: "POST", body: this.employee, }).then((response) => { expect(response.status).to.eq(201); expect(response.body).to.have.property("id"); const id = response.body.id; cy.wrap(id).as("id"); }); });
Ahora para comprobar si se ha creado no tenemos porque traernos todos los resultados podemos usar el id, aparte aquí en lugar de comprobar solo el nombre compruebo el cuerpo entero del body.
it("Debe de validar que se haya creado en la base de datos comprobando todos los parámetros", function () { cy.request("GET", `employees/${this.id}`).then((response) => { expect(response.body).deep.equal({ id: this.id, ...this.employee, }); }); });
Cuantos menos datos tengamos hardcodeados menos frágiles serán nuestros tests.
¡Genial! 🤟 Esto esta genial cuando desde nuestro Frontend tenemos la posibilidad de utilizar un server como propio lo qué, al realizar por UI peticiones podemos mejor interceptar las operaciones con interceptor. .
Nota. No es necesario que anexes la extensión del archivo, Cypress posee una jerarquía de búsqueda.
. El cual estará realizando "un falso espejo" o mock. Sin embargo, como es nuestro backend (utilizando express / nest / etc.), no podemos lanzar un CRUD porque estaremos lanzando procesos que afectarán en directo en nuestra base de datos. . Te recomiendo que, en cuyo caso, generes un mock de la capa concerniente y cuando lances tu E2E, obtengas por ruta una respuesta deseada. . Por ejemplo, supongamos que tenemos la capa del modelo de la siguiente forma:
// User.entity.ts type TQUser = { id: number } // User.model.ts export default class User { // Static response without using the parameter findMany(query: TQUser) { return { "id": 2, "name": "Antonina Fawltey", "email": "afawltey1@uol.com.br" } } }
Entonces, ya que Cypress incorpora SinonJS, una librería similar a Supertest / SuperAgent para pruebas E2E, podemos generar un mock de nuestra capa y por ende, realizar prueba tipo Unit Test.
import User from './models/User.model' describe('Testing User Routes', () => { beforeEach(function () { cy.fixture('user').then((user) => this.user = user) cy.stub(User.prototype, 'findMany', () => this.user) }) it('should be success and valid response', function () { const user = new User() const response = user.findMany({id: 2}) expect(response).to.deep.equal(this.user) expect(response).to.have.property('name') expect(response).to.have.property('email') }) })
Me da un error cuando trato de usar el request para llamar a la variable que se crea al principio
Hola Compañero, me daba el mismo error y es por que estaba utilizando arrow functions, cambia a la function() normal de toda la vida y me cuentas como te va.
Gracias Jesús, tenía el mismo problema y lo solucioné como comentaste. Me parece curioso porque investigando encontré que cuanto utilizamos function normal se crea un nuevo scope mientras que arrow functions hereda el scope de la función padre. Así que no entiendo por qué funciona con function pero no con arrow function.
¿para enviar atributos en el header, cómo se debe hacer?
Esto es algo que tuve que hacer en un script porque la api requería sí o sí que estén presentes, espero que te sirva.
let randomBuy = await cy.request({ method: 'PUT', url: '/cart/'+product.sku_value, form: false, headers: { 'accept': 'application/json', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'es-ES,es;q=0.9', 'authorization': tokenLoginBuyer, 'content-length': contentArtLength, 'content-type': 'application/json;charset=UTF-8' }, body: { 'country_id': product.country_id, 'currency_id': product.currency_id, 'provider': product.provider, 'quantity': productQuantity, 'region_id': product.region_id, } })
// fixture/new-employee.json { "first_name": "Carlos", "last_name": "Test", "email": "carlos.test@example.com" }
// cypress/e2e/api/crud-employee.cy.js describe("Flujo CRUD completo para la API de Empleados", () => { // Usaremos un alias de Cypress para almacenar el ID del empleado creado // y que esté disponible en cada prueba. beforeEach(() => { // Hook que se ejecuta ANTES de cada 'it' cy.fixture("new-employee").then((employee) => { // POST: Crear un nuevo empleado cy.request("POST", "/employees", employee).then((response) => { expect(response.status).to.eq(201); // Guardamos el ID del empleado recién creado en un alias cy.wrap(response.body.id).as("employeeId"); }); }); }); afterEach(() => { // Hook que se ejecuta DESPUÉS de cada 'it' // DELETE: Limpiar el empleado creado para no dejar basura if (this?.employeeId) { // Este 'if' ahora es clave cy.request("DELETE", `/employees/${this.employeeId}`); } }); // --- Ahora vienen nuestras pruebas atómicas --- it("GET - Debería poder leer los datos del empleado recién creado", () => { cy.get("@employeeId").then((employeeId) => { cy.request("GET", `/employees/${employeeId}`).then((response) => { expect(response.status).to.eq(200); expect(response.body.first_name).to.eq("Carlos"); // Validamos los datos expect(response.body.last_name).to.eq("Test"); // Validamos los datos }); }); }); it("PUT - Debería poder actualizar los datos del empleado", () => { cy.get("@employeeId").then((employeeId) => { const updatedData = { first_name: "Carlos Actualizado", email: "carlos.updated@example.com", id: employeeId, }; cy.request("PUT", `/employees/${employeeId}`, updatedData).then( (response) => { expect(response.status).to.eq(200); // Verificamos que el nombre se haya actualizado en la respuesta expect(response.body.first_name).to.eq("Carlos Actualizado"); } ); }); }); it("DELETE - Debería eliminar el empleado y luego no encontrarlo", function () { // Primero, eliminamos el recurso cy.request("DELETE", `/employees/${this.employeeId}`).then((response) => { expect(response.status).to.eq(200); // ¡LA CLAVE! Le decimos al 'afterEach' que ya no necesita limpiar. this.employeeId = null; }); // Luego, intentamos obtenerlo de nuevo para confirmar que ya no existe cy.request({ url: `/employees/${this.employeeId}`, failOnStatusCode: false, }).then((response) => { expect(response.status).to.eq(404); }); }); });
Gracias por la explicación. Alguien tiene la configuración para conectarlo con bdd (cucumber)? Gracias.
Hola , se viene en el curso avanzado de cypress ya con la nueva version de cypress
en el curso avanzado de CYPRESS veremos como usarlo con BDD
¿Esta lógica sirve para ambientes donde los tests se corren en paralelo? porque esta secuencia de crear, modificar, borrar, puede no ser la misma y empiece borrando algo que no existe... Yo lo veo como añadirlo todo en un mismo test crear, modificar, borrar. Pero, ¿cómo haría para que estuvieran todos en test separados pero el paralelismo no les afecten? ¿Alguna idea? ¡Gracias!
normalmente se corren en paralelo por suite osea pro archivo de test , no por test case . De todas formas aqui esta un articulo que te puede servir si las quieres correr en orden https://testersdock.com/cypress-execute-tests-in-order/
que diferencia tiene el cy.request y el cy.api ?
function
Como evitar que se hagan varios POST al momento de hacer la pruebas?