Aprenda a utilizar NodeJS Snapshot Testing para probar su código de forma rápida y sencilla. ¡Aprovecha al máximo tu proceso de desarrollo con esta poderosa herramienta!
Si estás leyendo esto, es probable que sepas algo sobre desarrollo web. Y si estás en el mundo del desarrollo web, probablemente sepas qué es NodeJS. Pero en caso de que no: NodeJS es un entorno de ejecución de JavaScript que nos permite ejecutar código JavaScript fuera del navegador. Es una gran herramienta para crear aplicaciones web complejas debido a su naturaleza asincrónica y sin bloqueo. También es ideal para crear API.
Las pruebas son una parte importante del proceso de desarrollo. Aquí veremos un tipo importante: las pruebas de instantáneas.
Pruebas en el desarrollo de software
Para cualquier persona involucrada en los servicios de desarrollo de Node JS, escribir pruebas en el desarrollo de software o seguir prácticas de TDD (Test Driven Development) es fundamental. Nadie quiere enviar códigos defectuosos, interfaces de usuario defectuosas o productos con errores. La mejor manera de evitar estos problemas es realizar pruebas.
Aquí crearemos una aplicación de tareas simple y la probaremos con pruebas instantáneas.
Configurando la base de datos
Antes de comenzar, dado que vamos a escribir una API real para este tutorial, configuremos nuestra base de datos. Para el tutorial, usaremos MongoDB Atlas. Así que vayamos a cloud.mongodb.com y obtengamos nuestra cadena de conexión. Necesitaremos esto más tarde. Nuestra cadena de conexión se verá así => mongodb+srv://
Tenga en cuenta que deberá cambiar su nombre de usuario y contraseña para mantener la conexión.
Variables ambientales
Mientras mantenemos nuestra conexión, mantenemos nuestro nombre de usuario y contraseña ocultos. Crearemos un nuevo archivo llamado .env y agregaremos nuestra cadena de conexión a una variable llamada MONGO_URI. Sería algo como => MONGO_URI=mongodb+srv://
Estamos utilizando variables de entorno en nuestra aplicación. Ahora mismo no podemos hacer nada con ello. Pero pronto instalaremos el paquete dotenv npm y leeremos esta variable en nuestra aplicación.
Configuración del paquete.json
Para nuestra comodidad, agregaremos scripts al archivo package.json en nuestro IDE de Node JS. Así que abrámoslo y agreguemos lo siguiente:
"guiones": { "prueba": "broma --detectOpenHandles --forceExit", "snap": "broma --updateSnapshot --detectOpenHandles --forceExit", "inicio": "nodo index.js", "dev": "nodemon index.js" },
Los comandos start y dev se utilizan para iniciar nuestra aplicación. Instalaremos el paquete Nodemon para el comando dev. También instalaremos Jest. Aquí la diferencia entre el comando de prueba y el de ajuste es que el comando de ajuste actualiza la instantánea. Veremos qué significa esto mientras escribimos nuestras pruebas instantáneas. También estamos usando el indicador –forceExit para salir del conjunto de pruebas una vez completadas las pruebas.
Construyendo la aplicación Todo
Para comprender las pruebas de instantáneas, primero debemos crear nuestra aplicación. Sería fantástico si tuviéramos una aplicación CRUD y, en este caso, creemos una aplicación de tareas pendientes que tendrá las propiedades de obtener, agregar, actualizar y eliminar una tarea pendiente.
Comenzaremos creando una nueva carpeta llamada "todo-app", antes de ingresar a esa carpeta y ejecutar npm init -y. Esto crea un nuevo archivo package.json. Aunque estemos creando una aplicación de tareas sencilla, siempre debemos seguir las mejores prácticas y dividir nuestra aplicación en diferentes partes. Por lo tanto, crearemos carpetas para dividir nuestro código ejecutando route tests mkdir controladores db mdels si estamos en la terminal Linux.
Ahora necesitamos instalar las dependencias que usaremos. Ejecutemos el siguiente comando en la raíz de nuestra aplicación npm i express mongoose jest mongodb nodemon supertest dotenv –save. Esto instalará las dependencias necesarias para construir nuestra aplicación. Usaremos Express para iniciar nuestro servidor, MongoDB para la base de datos, Mongoose para interactuar con MongoDB y Nodemon para monitorear nuestro servidor sin reiniciar. dotenv nos ayudará a ocultar datos confidenciales y Jest y SuperTest nos ayudarán a probar nuestra aplicación.
Ahora que hemos instalado las dependencias necesarias, creemos un nuevo archivo index.js ejecutando touch index.js en la terminal y comencemos a codificar.
Configurando el servidor
Tenga en cuenta el siguiente fragmento de código del archivo index.js:
//importando dependencias const expreso = requerir("expreso"); const connectDB = requerir("./db/connect"); requerir("dotenv").config ; //importando rutas const todoRoutes = require("./rutas/todoRoutes"); //creando una aplicación express aplicación constante = expreso; /* Un middleware que analiza el cuerpo de la solicitud y lo pone a disposición en el objeto req.body . */ aplicación.use(express.json); /* Esta es la ruta raíz . Se utiliza para comprobar si el servidor se está ejecutando. */ aplicación.get(" (req, res) => { res.status(200).json({ vivo: "Verdadero" }); }); /* Esta es la ruta que maneja todas las rutas de tareas pendientes . */ app.use("/todos", todoRoutes); puerto constante = proceso.env.PORT 3000; inicio constante = asíncrono => { intentar { espere connectDB(process.env.MONGO_URI); app.listen(port, console.log(`El servidor está escuchando en el puerto ${port}....`)); } captura (error) { consola.log(error); } }; comenzar; módulo.exportaciones = aplicación;
Si bien hay comentarios sobre el código, repasemos lo que está pasando.
Primero, al importar las dependencias, notará que estamos importando connectDB desde ./db/connect. Esto se debe a que nos vamos a conectar a nuestra base de datos en un archivo diferente. Crearemos este archivo pronto.
En segundo lugar, estamos importando todoRoutes desde ./routes/todoRoutes. Esto también se debe a que escribiremos nuestras rutas allí.
Luego de usar las rutas vía app.use(“/todos”, todoRoutes);, estamos configurando el puerto e iniciando el servidor. También estamos exportando la aplicación para poder usarla en nuestras pruebas.
Conexión a la base de datos
Como queremos separar nuestras preocupaciones, dentro de la carpeta db crearemos un archivo llamado connect.js y escribiremos el siguiente código:
const mangosta = require("mangosta"); mangosta.set ("Consulta estricta", falso); const connectDB = (url) => { return mangosta.connect(url, {}); }; módulo.exportaciones = connectDB;
Hasta que consigamos la mangosta y podamos conectarnos a la base de datos. En el archivo index.js, la última función se llamó inicio:
inicio constante = asíncrono => { intentar { espere connectDB(process.env.MONGO_URI); app.listen(port, console.log(`El servidor está escuchando en el puerto ${port}....`)); } captura (error) { consola.log(error); } };
Como puede ver, connectDB se importa aquí con el MONGO_URI que guardamos anteriormente en el archivo .env. Ahora que nuestro servidor puede conectarse a la base de datos, es hora de crear un modelo.
Creando el modelo
Iremos al directorio de rutas y crearemos un nuevo archivo llamado todoModel.js . Rellenamos este archivo con el siguiente código:
const mangosta = require("mangosta"); const todoEsquema = mangosta.Esquema({ nombre: {tipo: Cadena, requerido: verdadero}, }); module.exports = mangosta.model("Todo", todoSchema);
Nuestro todo tendrá un solo nombre. MongoDB generará automáticamente el ID. Aquí estamos exportando el esquema con el nombre "Todo". Usaremos este nombre cuando queramos interactuar con la base de datos.
Creando los controladores
Ahora que tenemos un modelo, podemos crear los controladores. Iremos a la carpeta de controladores y ejecutaremos un archivo llamado todoControllers.js. Los controladores requerirán que el modelo funcione y, dado que el modelo requiere Mongoose, podemos usar comandos de Mongoose en los controladores. Comencemos por obtener todos los todos. Ahora no existe nada en la base de datos. Simplemente estamos escribiendo la lógica que los recibirá todos una vez que estén completos.
const Todo = require("../modelos/todomodel"); const getAllAll = async (req, res) => { intentar { const todos = await Todo.find({}).exec; res.status(200).json({ todos }); } captura (error) { res.status(500).json({ mensaje: error }); } };
Primero importamos el modelo y luego, con la función asíncrona getAllTodos, queremos obtener todos los todos. Para nuestras funciones restantes, usaremos la misma sintaxis async/await try/catch ya que simplifica la legibilidad del código y facilita la depuración.
Bajo el código anterior, hemos agregado las siguientes líneas:
const createTodo = async (req, res) => { intentar { const todo = esperar Todo.create(req.body); res.status(201).json({ todo }); } captura (error) { res.status(500).json({ mensaje: error }); } }; actualización constanteTodo = async (req, res) => { intentar { const todo = await Todo.findOneAndUpdate({ _id: req.params.id }, req.body, { nuevo: cierto, }).exec; res.status(200).json({ todo }); } captura (error) { res.status(500).json({ mensaje: error }); } }; const eliminarTodo = async (req, res) => { intentar { const todo = await Todo.findOneAndDelete({ _id: req.params.id }).exec; res.status(200).json({ todo }); } captura (error) { res.status(500).json({ mensaje: error }); } }; módulo.exportaciones = { obtener todo todo, crearTodo, actualizarTodo, borrarTodo, };
Crear, actualizar y eliminar siguen el mismo patrón que crear uno, pero necesitamos hacer algo más: exportar estas funciones. Los usaremos en nuestras rutas.
Creando las rutas
Para crear rutas, iremos a la carpeta de rutas, crearemos un archivo llamado todoRoutes.js e insertaremos el siguiente código:
const expreso = requerir("expreso"); enrutador constante = express.Router; constante { obtener todo todo, crearTodo, actualizarTodo, borrarTodo, } = require("../controladores/todoControllers"); enrutador.ruta(" router.route("/:id").patch(updateTodo).delete(deleteTodo); módulo.exportaciones = enrutador;
Ahora, exigimos enrutadores exprés y exprés. A continuación, importaremos las funciones que creamos y las exportaremos a controladores. Entonces estamos usando el enrutador para especificar qué ruta llamará a qué función.
En nuestro caso, la ruta base llamará a la función getAllTodos en caso de una solicitud de obtención y creará Todo en caso de una solicitud de publicación. Para corregir y eliminar, debemos especificar el ID de la tarea específica que queremos actualizar o eliminar. Es por eso que utilizamos la sintaxis /:id. Ahora podemos exportar el enrutador y usarlo en nuestro archivo index.js.
Ahora las líneas const todoRoutes = require(“./routes/todoRoutes”); y app.use(“/todos”, todoRoutes); en el archivo index.js tiene sentido.
Si ejecutamos el servidor y lo probamos mediante Postman o Insomnia, podemos realizar operaciones CRUD. Pero queremos hacer más: queremos probar nuestro código usando instantáneas para poder ver si nuestro código funciona como se esperaba.
Probando el código
Después de diseñar meticulosamente nuestra aplicación, el siguiente paso crucial es garantizar su confiabilidad y efectividad mediante pruebas rigurosas.
Prueba de instantánea
Las pruebas instantáneas son diferentes de las pruebas estándar. Además, es importante tener en cuenta que, si bien generalmente se usa con tecnologías front-end como ReactJs, también puede ser útil en el desarrollo back-end. ¿Qué es esto exactamente? Para comprender las pruebas instantáneas, resulta útil comprender primero las pruebas en sí.
En el desarrollo de software, existen diferentes métodos de prueba, desde pruebas unitarias hasta pruebas de un extremo a otro. También existen herramientas para escribir pruebas utilizando métodos como Jest, Mocha, Chai, Cypress y más.
Aquí usaremos Jest. Normalmente, cuando escribimos pruebas con Jest, buscamos ciertas cosas. Queremos escribir una prueba que verifique que el código funciona como se esperaba.
Considere el siguiente ejemplo: mientras creamos una aplicación CRUD, es posible que deseemos verificar si el método de parche funciona como se esperaba. Digamos que hay una tarea "Comprar velas" y mediante una solicitud de parche queremos cambiarla a "Comprar encendedor". En el conjunto de pruebas, escribiríamos una prueba que verificaría si la tarea se cambió según lo previsto o no. "Esperaríamos" que la tarea en cuestión fuera "Comprar más ligero". Si es así, la prueba pasará. De lo contrario, la prueba fallará.
Ahora las pruebas de instantáneas son diferentes. En lugar de esperar un determinado comportamiento e iniciar una situación de aprobación/falla en consecuencia, tomamos instantáneas de nuestro código en su estado en un momento determinado y las comparamos con la instantánea anterior. Si hay una diferencia, la prueba falla. Si no hay diferencia, la prueba pasa.
Esto ayuda a reducir la posibilidad de cambios no deseados. Si hay un cambio, la depuración ahora sería mucho más fácil.
Ahora codifiquemos con un caso de prueba de instantánea típico. Ya instalamos Jest y SuperTest, otra herramienta que nos ayudará a probar las solicitudes de API.
Escribiendo la prueba instantánea
Primero, vayamos a la carpeta de pruebas que creamos antes y agreguemos el archivo index.test.js. Jest encontrará este archivo automáticamente. Ahora, dentro del archivo, comenzaremos escribiendo las siguientes líneas de código:
const mangosta = require("mangosta"); solicitud constante = requerir("superprueba"); aplicación constante = require("../index"); const connectDB = requerir("../db/connect"); requerir("dotenv").config ; /* Conexión a la base de datos antes de cada prueba. */ antes de cada uno (async => { espere connectDB(process.env.MONGO_URI); }); /* Descartar la base de datos y cerrar la conexión después de cada prueba. */ después de cada (async => { // espera mangosta.connection.dropDatabase; esperar mangosta.conexión.cerrar; });
Comenzamos importando las dependencias necesarias. Posteriormente, definimos dos métodos: beforeEach y afterEach. Se ejecutarán antes y después de cada prueba. En el método beforeEach, nos conectamos a la base de datos. En el método afterEach, soltamos la base de datos y cerramos la conexión. Ahora, escribiremos nuestra primera prueba de la siguiente manera:
describir("OBTENER /todo", => { it("debería devolver todos todos", async => { const res = esperar solicitud(aplicación).get("/todos"); // esperar(res.statusCode).toEqual(200); // expect(res.body).toHaveProperty("todos"); esperar(res.body).toMatchSnapshot; }); });
Ahora ejecutaremos npm run test en la terminal. Esto corresponderá a jest –detectOpenHandles –forceExit como lo definimos en los scripts package.json. Tenga en cuenta que las líneas confirmadas son la forma en que normalmente probaríamos la respuesta de la API. Pero como estamos probando instantáneas, utilizamos un enfoque diferente con la palabra clave toMatchSnapshot.
Después de ejecutar el comando npm run test, si observa la carpeta de pruebas, notará que hay otra carpeta llamada __snapshots__ dentro de ella. Esta carpeta tiene un archivo llamado index.test.js.snap. Si abre este archivo, verá que contiene:
// Instantánea de broma v1, exports(`GET /todos debería devolver todos todos 1`) = ` { "todo": , } `;
Esto significa que hemos tomado con éxito una instantánea del estado actual de la aplicación. Como aún no los hemos publicado todos, la matriz todos vuelve vacía. Ahora, cada vez que hagamos un cambio y ejecutemos las pruebas, comparará el estado actual de la aplicación con esta instantánea. Si hay una diferencia, la prueba falla. Si no hay diferencia, la prueba pasa. Vamos a intentarlo. En index.test.js, agregaremos la siguiente prueba:
describir("POST /todo", => { it("debería crear una nueva tarea pendiente", async => { const res = esperar solicitud(aplicación).post("/todos").send({ nombre: "Comprar velas", }); esperar(res.body).toMatchSnapshot; }); });
Esta prueba creará una nueva tarea llamada "Comprar velas". Ahora, tome una instantánea del estado actual de la aplicación. Ejecutemos la prueba de ejecución npm nuevamente y veamos qué sucede. Las pruebas pasan. Pero si observa el archivo index.test.js.snap, verá que ha cambiado:
// Instantánea de broma v1, exports(`GET /todos debería devolver todos todos 1`) = ` { "todo": , } `; exports(`POST /todos debería crear un nuevo todo 1`) = ` { "todo": { "__v": 0, "_id": "646dba457c9da2bc152c498a", "name": "Comprar velas", }, } `;
Rehagamos las pruebas y veamos qué pasa. Ahora las pruebas fallan. Si consulta el atlas de MongoDB y observa su colección, verá que hay dos "Buy Candles", todas con diferentes ID. Pero en el archivo de instantánea, solo teníamos uno. Por eso falla. Compara el estado de la aplicación donde se tomó la instantánea con la actual y muestra los cambios. Si miras en tu terminal verás los detalles de la prueba.
Podemos actualizar nuestra instantánea. Cambiemos "Comprar velas" por "Comprar encendedor" para mayor comodidad y esta vez ejecutemos npm run snap. Quizás recuerde que este comando corresponde a jest –updateSnapshot –detectOpenHandles –forceExit en los scripts package.json. Las pruebas pasan. También actualizará el archivo de instantánea. Si volvemos al archivo index.test.js.snap y miramos lo que hay dentro, deberíamos ver esto:
// Instantánea de broma v1, exports(`GET /todos debería devolver todos todos 1`) = ` { "todo": ( { "__v": 0, "_id": "646dba457c9da2bc152c498a", "name": "Comprar velas", }, { "__v": 0, "_id": "646dba747fda9c2f1fb94a7d", "name": "Comprar velas", }, ), } `; exports(`POST /todos debería crear un nuevo todo 1`) = ` { "todo": { "__v": 0, "_id": "646dbcb461df5575a4a63bf1", "name": "Comprar encendedor", }, } `;
Veamos otro ejemplo. Las pruebas de instantáneas son especialmente útiles en los casos en los que puede haber cambios inesperados. Por ejemplo, existe la posibilidad de que se elimine uno de todos. Comentemos la solicitud de publicación para mayor comodidad y agreguemos otra prueba a nuestro archivo index.test.js:
describir("BORRAR /todos/:id", => { it("debería eliminar una tarea pendiente", async => { const res = espera solicitud(aplicación).delete("/todos/646ce381a11397af903abec9"); esperar(res.body).toMatchSnapshot; }); });
Anote el ID en eliminar(“/todos/646ce381a11397af903abec9”); es el ID de la primera tarea de nuestra colección. Suministramos por código estricto. Ahora, si ejecutamos npm run test, debería fallar y mostrarnos las diferencias. En la terminal, las pruebas deberían pasar, y cuando miremos nuestro archivo de instantánea, deberíamos ver esto:
// Instantánea de broma v1, exports(`DELETE /todos/:id debería eliminar un todo 1`) = ` { "todo": { "__v": 0, "_id": "646dba457c9da2bc152c498a", "name": "Comprar velas", }, } `; exports(`GET /todos debería devolver todos todos 1`) = ` { "todo": ( { "__v": 0, "_id": "646dba457c9da2bc152c498a", "name": "Comprar velas", }, ... ...
La identificación que eliminamos todavía está en la instantánea. Necesitamos actualizarlo. Si ejecutamos npm run snap y miramos el archivo de instantánea, vemos que la tarea con el ID especificado desapareció. Desde aquí podremos seguir jugando con nuestra app y ver si ha cambiado.
Beneficios de las pruebas instantáneas
Existen varios beneficios menos obvios al realizar pruebas de instantáneas en aplicaciones nodeJS. Algunos de ellos son:
- Pruebas de regresión: las pruebas de instantáneas son excelentes para garantizar que cualquier cambio que realice no afecte su aplicación. Puede ejecutar las pruebas y ver si hay cambios inesperados en la aplicación.
- Verificación del contrato de API: las pruebas de instantáneas son útiles para confirmar que el contrato entre el frontend y el backend no está roto. Al tomar instantáneas de las respuestas de la API, puede asegurarse de que la interfaz obtenga los datos que espera.
- Documentación: al tomar instantáneas, puede comunicar el estado de su aplicación y qué tipo de datos se deben devolver en cada escenario a sus compañeros de equipo.
- Desarrollo colaborativo: las pruebas instantáneas pueden ser beneficiosas en la comunicación entre los desarrolladores de front-end y back-end. Con las instantáneas, los desarrolladores front-end pueden anticipar y manejar cualquier cambio en el backend.
- Refactorización y cambios de código: en la refactorización de código, las instantáneas proporcionan una red de seguridad. Puede asegurarse de que sus cambios no cambien nada no deseado.
Conclusión
Aquí, aprendemos cómo realizar pruebas de instantáneas en aplicaciones nodeJS, instalar y configurar Jest, escribir pruebas y tomar instantáneas del estado actual de la aplicación. También revisamos los beneficios de las pruebas instantáneas. Ahora debería tener una idea más clara de lo que implican las pruebas de instantáneas y por qué ayudan en el proceso de desarrollo.
Si te gustó este artículo, quizás te guste;
- Libere el poder de los microservicios Node.JS
- Cambiar la versión del nodo: una guía paso a paso
- Caché de Node JS: aumento del rendimiento y la eficiencia
- Liberando el poder de Websocket Nodejs
Preguntas frecuentes
¿Qué bibliotecas se utilizan habitualmente para las pruebas de instantáneas en Node.js?
Jest es una de las bibliotecas de prueba más populares para pruebas de instantáneas de Node.js. Le permite crear y administrar fácilmente instantáneas de sus componentes, lo que simplifica la identificación de cualquier cambio inesperado. Otra biblioteca que se puede utilizar para realizar pruebas de instantáneas es Ava, aunque no se utiliza tanto como Jest.
¿Cuándo debo utilizar Snapshot Testing en mi proyecto Node.js?
Las pruebas de instantáneas se utilizan mejor cuando desea asegurarse de que los cambios en su código no cambien inesperadamente los componentes de la interfaz de usuario. Es especialmente útil para aplicaciones grandes y complejas donde puede resultar difícil comprobar manualmente la interfaz de usuario después de cada cambio. Sin embargo, las pruebas instantáneas no deben ser la única estrategia de prueba, ya que no garantizan la corrección de la lógica empresarial, solo la coherencia de la interfaz de usuario.
¿Cómo se crea un archivo de instantánea de referencia?
Normalmente se crea un archivo de instantánea de referencia la primera vez que ejecuta una prueba de instantánea. El marco de prueba (como Jest) creará automáticamente una instantánea del estado actual del componente de la interfaz de usuario u otro resultado que se esté probando y lo almacenará en un archivo. Este archivo de prueba se utilizará como instantánea de referencia para pruebas posteriores.
¿Qué debo hacer si mi prueba de instantánea falla debido a una discrepancia en la instantánea?
Si una prueba falla debido a que el archivo de la instantánea no coincide, significa que el estado actual de la interfaz de usuario u otra salida que se está probando no coincide con los valores de la instantánea almacenados. Primero debe investigar para determinar si el cambio fue intencional o el resultado de un error. Si el cambio fue intencional y el nuevo estado es correcto, puede actualizar la instantánea como se describe anteriormente. Si el cambio no fue intencional, deberá depurar la causa del cambio inesperado.