Pruebas de integración y de extremo a extremo simplificadas con Node.js y MongoDB
Publicado: 2022-03-11Las pruebas son una parte esencial de la creación de una aplicación robusta de Node.js. Las pruebas adecuadas pueden superar fácilmente muchas deficiencias que los desarrolladores pueden señalar sobre las soluciones de desarrollo de Node.js.
Si bien muchos desarrolladores se enfocan en una cobertura del 100 % con pruebas unitarias, es importante que el código que escriba no se pruebe de forma aislada. Las pruebas de integración y de extremo a extremo le brindan esa confianza adicional al probar partes de su aplicación juntas. Estas partes pueden funcionar bien por sí solas, pero en un sistema grande, las unidades de código rara vez funcionan por separado.
Node.js y MongoDB juntos forman uno de los dúos más populares de los últimos tiempos. Si eres una de las muchas personas que los usan, estás de suerte.
En este artículo, aprenderá cómo escribir fácilmente pruebas de integración y de extremo a extremo para su aplicación Node.js y MongoDB que se ejecutan en instancias reales de la base de datos, todo sin necesidad de configurar un entorno elaborado o un código complicado de configuración/desmontaje. .
Verá cómo el paquete mongo-unit ayuda con la integración y las pruebas de extremo a extremo en Node.js. Para obtener una descripción general más completa de las pruebas de integración de Node.js, consulte este artículo.
Tratar con una base de datos real
Por lo general, para la integración o las pruebas de un extremo a otro, sus scripts deberán conectarse a una base de datos dedicada real para fines de prueba. Esto implica escribir código que se ejecuta al principio y al final de cada caso/conjunto de prueba para garantizar que la base de datos esté en un estado limpio y predecible.
Esto puede funcionar bien para algunos proyectos, pero tiene algunas limitaciones:
- El entorno de prueba puede ser bastante complejo. Deberá mantener la base de datos ejecutándose en algún lugar. Esto a menudo requiere un esfuerzo adicional para configurar los servidores de CI.
- La base de datos y las operaciones pueden ser relativamente lentas. Dado que la base de datos utilizará conexiones de red y las operaciones requerirán actividad del sistema de archivos, puede que no sea fácil ejecutar miles de pruebas rápidamente.
- La base de datos mantiene el estado y no es muy conveniente para las pruebas. Las pruebas deben ser independientes entre sí, pero el uso de una base de datos común podría hacer que una prueba afecte a otras.
Por otro lado, el uso de una base de datos real hace que el entorno de prueba sea lo más cercano posible a la producción. Esto puede verse como una ventaja particular de este enfoque.
Uso de una base de datos real en memoria
El uso de una base de datos real para las pruebas parece presentar algunos desafíos. Pero, la ventaja de usar una base de datos real es demasiado buena para pasarla por alto. ¿Cómo podemos sortear los desafíos y mantener la ventaja?
Reutilizar una buena solución de otra plataforma y aplicarla al mundo de Node.js puede ser el camino a seguir aquí.
Los proyectos de Java utilizan ampliamente DBUnit con una base de datos en memoria (p. ej., H2) para este fin.
DBUnit está integrado con JUnit (el ejecutor de pruebas de Java) y le permite definir el estado de la base de datos para cada conjunto de pruebas/pruebas, etc. Elimina las restricciones mencionadas anteriormente:
- DBUnit y H2 son bibliotecas de Java, por lo que no necesita configurar un entorno adicional. Todo se ejecuta en la JVM.
- La base de datos en memoria hace que esta gestión de estado sea muy rápida.
- DBUnit hace que la configuración de la base de datos sea muy simple y le permite mantener un estado claro de la base de datos para cada caso.
- H2 es una base de datos SQL y es parcialmente compatible con MySQL por lo que, en la mayoría de los casos, la aplicación puede funcionar con ella como con una base de datos de producción.
A partir de estos conceptos, decidí hacer algo similar para Node.js y MongoDB: Mongo-unit.
Mongo-unit es un paquete de Node.js que se puede instalar usando NPM o Yarn. Ejecuta MongoDB en memoria. Facilita las pruebas de integración al integrarse bien con Mocha y proporcionar una API simple para administrar el estado de la base de datos.
La biblioteca utiliza el paquete NPM prediseñado de mongodb, que contiene archivos binarios de MongoDB prediseñados para los sistemas operativos populares. Estas instancias de MongoDB pueden ejecutarse en modo en memoria.
Instalación de la unidad Mongo
Para agregar mongo-unit a su proyecto, puede ejecutar:
npm install -D mongo-unit
o
yarn add mongo-unit
Y eso es todo. Ni siquiera necesita MongoDB instalado en su computadora para usar este paquete.
Uso de Mongo-unit para pruebas de integración
Imaginemos que tiene una aplicación Node.js simple para administrar tareas:
// service.js const mongoose = require('mongoose') const mongoUrl = process.env.MONGO_URL || 'mongodb://localhost:27017/example' mongoose.connect(mongoUrl) const TaskSchema = new mongoose.Schema({ name: String, started: Date, completed: Boolean, }) const Task = mongoose.model('tasks', TaskSchema) module.exports = { getTasks: () => Task.find(), addTask: data => new Task(data).save(), deleteTask: taskId => Task.findByIdAndRemove(taskId) }
La URL de conexión de MongoDB no está codificada aquí. Al igual que con la mayoría de los back-end de aplicaciones web, lo tomamos de la variable de entorno. Esto nos permitirá sustituirlo por cualquier URL durante las pruebas.
const express = require('express') const bodyParser = require('body-parser') const service = require('./service') const app = express() app.use(bodyParser.json()) app.use(express.static(`${__dirname}/static`)) app.get('/example', (req, res) => { service.getTasks().then(tasks => res.json(tasks)) }) app.post('/example', (req, res) => { service.addTask(req.body).then(data => res.json(data)) }) app.delete('/example/:taskId', (req, res) => { service.deleteTask(req.params.taskId).then(data => res.json(data)) }) app.listen(3000, () => console.log('started on port 3000'))
Este es un fragmento de una aplicación de ejemplo que tiene una interfaz de usuario. El código de la interfaz de usuario se ha omitido por brevedad. Puede consultar el ejemplo completo en GitHub.
Integrando con Mocha
Para hacer que Mocha ejecute pruebas de integración con mongo-unit, debemos ejecutar la instancia de la base de datos de mongo-unit antes de que se cargue el código de la aplicación en el contexto de Node.js. Para hacer esto, podemos usar el parámetro mocha --require
y la biblioteca Mocha-prepare, que le permite realizar operaciones asincrónicas en los scripts requeridos.

// it-helper.js const prepare = require('mocha-prepare') const mongoUnit = require('mongo-unit') prepare(done => mongoUnit.start() .then(testMongoUrl => { process.env.MONGO_URL = testMongoUrl done() }))
Escribir pruebas de integración
El primer paso es agregar una prueba a la base de datos de prueba ( testData.json
):
{ "tasks": [ { "name": "test", "started": "2017-08-28T16:07:38.268Z", "completed": false } ] }
El siguiente paso es agregar las pruebas en sí:
const expect = require('chai').expect const mongoose = require('mongoose') const mongoUnit = require('../index') const service = require('./app/service') const testMongoUrl = process.env.MONGO_URL describe('service', () => { const testData = require('./fixtures/testData.json') beforeEach(() => mongoUnit.initDb(testMongoUrl, testData)) afterEach(() => mongoUnit.drop()) it('should find all tasks', () => { return service.getTasks() .then(tasks => { expect(tasks.length).to.equal(1) expect(tasks[0].name).to.equal('test') }) }) it('should create new task', () => { return service.addTask({ name: 'next', completed: false }) .then(task => { expect(task.name).to.equal('next') expect(task.completed).to.equal(false) }) .then(() => service.getTasks()) .then(tasks => { expect(tasks.length).to.equal(2) expect(tasks[1].name).to.equal('next') }) }) it('should remove task', () => { return service.getTasks() .then(tasks => tasks[0]._id) .then(taskId => service.deleteTask(taskId)) .then(() => service.getTasks()) .then(tasks => { expect(tasks.length).to.equal(0) }) }) })
¡Y voilá!
Observe cómo hay solo un par de líneas de código que se ocupan de la configuración y el desmontaje.
Como puede ver, es muy fácil escribir pruebas de integración utilizando la biblioteca mongo-unit. No nos burlamos de MongoDB en sí, y podemos usar los mismos modelos de Mongoose. Tenemos el control total de los datos de la base de datos y no perdemos mucho en el rendimiento de las pruebas, ya que el MongoDB falso se ejecuta en la memoria.
Esto también nos permite aplicar las mejores prácticas de pruebas unitarias para las pruebas de integración:
- Haga que cada prueba sea independiente de otras pruebas. Cargamos datos nuevos antes de cada prueba, dándonos un estado totalmente independiente para cada prueba.
- Utilice el estado mínimo requerido para cada prueba. No necesitamos llenar toda la base de datos. Solo necesitamos establecer los datos mínimos requeridos para cada prueba en particular.
- Podemos reutilizar una conexión para la base de datos. Aumenta el rendimiento de la prueba.
Como beneficio adicional, incluso podemos ejecutar la aplicación contra mongo-unit. Nos permite realizar pruebas de extremo a extremo para nuestra aplicación contra una base de datos simulada.
Pruebas de extremo a extremo con Selenium
Para las pruebas de extremo a extremo, utilizaremos Selenium WebDriver y el corredor de pruebas Hermione E2E.
Primero, arrancaremos el controlador y el corredor de prueba:
const mongoUnit = require('mongo-unit') const selenium = require('selenium-standalone') const Hermione = require('hermione') const hermione = new Hermione('./e2e/hermione.conf.js') //hermione config seleniumInstall() //make sure selenium is installed .then(seleniumStart) //start selenium web driver .then(mongoUnit.start) // start mongo unit .then(testMongoUrl => { process.env.MONGO_URL = testMongoUrl //store mongo url }) .then(() => { require('./index.js') //start application }) .then(delay(1000)) // wait a second till application is started .then(() => hermione.run('', hermioneOpts)) // run hermiona e2e tests .then(() => process.exit(0)) .catch(() => process.exit(1))
También necesitaremos algunas funciones auxiliares (se eliminó el manejo de errores por brevedad):
function seleniumInstall() { return new Promise(resolve => selenium.install({}, resolve)) } function seleniumStart() { return new Promise(resolve => selenium.start(resolve)) } function delay(timeout) { return new Promise(resolve => setTimeout(resolve, timeout)) }
Después de llenar la base de datos con algunos datos y limpiarla una vez realizadas las pruebas, podemos ejecutar nuestras primeras pruebas:
const expect = require('chai').expect const co = require('co') const mongoUnit = require('../index') const testMongoUrl = process.env.MONGO_URL const DATA = require('./fixtures/testData.json') const ui = { task: '.task', remove: '.task .remove', name: '#name', date: '#date', addTask: '#addTask' } describe('Tasks', () => { beforeEach(function () { return mongoUnit.initDb(testMongoUrl, DATA) .then(() => this.browser.url('http://localhost:3000')) }) afterEach(() => mongoUnit.dropDb(testMongoUrl)) it('should display list of tasks', function () { const browser = this.browser return co(function* () { const tasks = yield browser.elements(ui.task) expect(tasks.length, 1) }) }) it('should create task', function () { const browser = this.browser return co(function* () { yield browser.element(ui.name).setValue('test') yield browser.element(ui.addTask).click() const tasks = yield browser.elements(ui.task) expect(tasks.length, 2) }) }) it('should remove task', function () { const browser = this.browser return co(function* () { yield browser.element(ui.remove).click() const tasks = yield browser.elements(ui.task) expect(tasks.length, 0) }) }) })
Como puede ver, las pruebas de extremo a extremo se parecen mucho a las pruebas de integración.
Envolver
La integración y las pruebas de extremo a extremo son importantes para cualquier aplicación a gran escala. Las aplicaciones de Node.js, en particular, pueden beneficiarse enormemente de las pruebas automatizadas. Con mongo-unit, puede escribir pruebas de integración y de extremo a extremo sin preocuparse por todos los desafíos que conllevan dichas pruebas.
Puede encontrar ejemplos completos de cómo usar mongo-unit en GitHub.
Lecturas adicionales en el blog de ingeniería de Toptal:
- Creación de una API REST de Node.js/TypeScript, parte 3: MongoDB, autenticación y pruebas automatizadas