Integrarea și testele end-to-end simplificate cu Node.js și MongoDB
Publicat: 2022-03-11Testele sunt o parte esențială a construirii unei aplicații robuste Node.js. Testele adecvate pot depăși cu ușurință multe deficiențe pe care dezvoltatorii le pot sublinia despre soluțiile de dezvoltare Node.js.
În timp ce mulți dezvoltatori se concentrează pe o acoperire de 100% cu teste unitare, este important ca codul pe care îl scrieți să nu fie testat doar izolat. Integrarea și testele end-to-end vă oferă acest plus de încredere testând împreună părți ale aplicației dvs. Aceste părți pot funcționa foarte bine singure, dar într-un sistem mare, unitățile de cod rareori funcționează separat.
Node.js și MongoDB formează împreună unul dintre cele mai populare duouri din ultima vreme. Dacă se întâmplă să fii una dintre multele persoane care le folosesc, ai noroc.
În acest articol, veți învăța cum să scrieți cu ușurință teste de integrare și teste end-to-end pentru aplicația dvs. Node.js și MongoDB care rulează pe instanțe reale ale bazei de date, fără a fi nevoie să configurați un mediu elaborat sau un cod complicat de configurare/demontare. .
Veți vedea cum pachetul mongo-unit ajută la integrarea și testarea end-to-end în Node.js. Pentru o prezentare mai cuprinzătoare a testelor de integrare Node.js, consultați acest articol.
De-a face cu o bază de date reală
De obicei, pentru integrare sau teste end-to-end, scripturile dumneavoastră vor trebui să se conecteze la o bază de date dedicată reală în scopuri de testare. Aceasta implică scrierea unui cod care rulează la începutul și la sfârșitul fiecărui caz/suită de testare pentru a se asigura că baza de date este într-o stare curată și previzibilă.
Acest lucru poate funcționa bine pentru unele proiecte, dar are câteva limitări:
- Mediul de testare poate fi destul de complex. Va trebui să păstrați baza de date în funcțiune undeva. Acest lucru necesită adesea efort suplimentar pentru a configura cu servere CI.
- Baza de date și operațiunile pot fi relativ lente. Deoarece baza de date va folosi conexiuni de rețea și operațiunile vor necesita activitate de sistem de fișiere, este posibil să nu fie ușor să rulați rapid mii de teste.
- Baza de date păstrează starea și nu este foarte convenabilă pentru teste. Testele ar trebui să fie independente unele de altele, dar utilizarea unui DB comun ar putea face ca un test să-i afecteze pe alții.
Pe de altă parte, utilizarea unei baze de date reale face ca mediul de testare să fie cât mai aproape de producție. Acesta poate fi privit ca un avantaj deosebit al acestei abordări.
Utilizarea unei baze de date reale, în memorie
Utilizarea unei baze de date reale pentru testare pare să aibă unele provocări. Dar, avantajul utilizării unei baze de date reale este prea bun pentru a fi transmis. Cum putem rezolva provocările și să păstrăm avantajul?
Reutilizarea unei soluții bune de pe o altă platformă și aplicarea acesteia în lumea Node.js poate fi calea de urmat aici.
Proiectele Java folosesc pe scară largă DBUnit cu o bază de date în memorie (de exemplu, H2) în acest scop.
DBUnit este integrat cu JUnit (rulerul de testare Java) și vă permite să definiți starea bazei de date pentru fiecare suită de testare/testare etc. Înlătură constrângerile discutate mai sus:
- DBUnit și H2 sunt biblioteci Java, așa că nu trebuie să configurați un mediu suplimentar. Totul rulează în JVM.
- Baza de date în memorie face acest management al stării foarte rapid.
- DBUnit face configurarea bazei de date foarte simplă și vă permite să păstrați o stare clară a bazei de date pentru fiecare caz.
- H2 este o bază de date SQL și este parțial compatibil cu MySQL, așa că, în cazurile majore, aplicația poate funcționa cu ea ca și cu o bază de date de producție.
Luând din aceste concepte, am decis să fac ceva similar pentru Node.js și MongoDB: Mongo-unit.
Mongo-unit este un pachet Node.js care poate fi instalat folosind NPM sau Yarn. Rulează MongoDB în memorie. Face testele de integrare ușoare, integrându-se bine cu Mocha și oferind un API simplu pentru a gestiona starea bazei de date.
Biblioteca folosește pachetul NPM mongodb-preconstruit, care conține binare MongoDB prefabricate pentru sistemele de operare populare. Aceste instanțe MongoDB pot rula în modul în memorie.
Instalarea Mongo-unit
Pentru a adăuga mongo-unit la proiectul dvs., puteți rula:
npm install -D mongo-unit
sau
yarn add mongo-unit
Și, asta este. Nici măcar nu aveți nevoie de instalarea MongoDB pe computer pentru a utiliza acest pachet.
Utilizarea Mongo-unit pentru teste de integrare
Să ne imaginăm că aveți o aplicație simplă Node.js pentru a gestiona sarcini:
// 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) }
Adresa URL a conexiunii MongoDB nu este codificată aici. Ca și în cazul majorității back-end-urilor de aplicații web, o luăm din variabila de mediu. Acest lucru ne va permite să o înlocuim cu orice adresă URL în timpul testelor.
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'))
Acesta este un fragment al unui exemplu de aplicație care are o interfață cu utilizatorul. Codul pentru UI a fost omis din motive de concizie. Puteți consulta exemplul complet pe GitHub.
Integrarea cu Mocha
Pentru ca Mocha să ruleze teste de integrare împotriva mongo-unit, trebuie să rulăm instanța bazei de date mongo-unit înainte ca codul aplicației să fie încărcat în contextul Node.js. Pentru a face acest lucru, putem folosi parametrul mocha --require
și biblioteca Mocha-prepare, care vă permite să efectuați operații asincrone în scripturile require.

// 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() }))
Redactarea Testelor de Integrare
Primul pas este să adăugați un test la baza de date de testare ( testData.json
):
{ "tasks": [ { "name": "test", "started": "2017-08-28T16:07:38.268Z", "completed": false } ] }
Următorul pas este să adăugați testele în sine:
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) }) }) })
Și voila!
Observați cum există doar câteva linii de cod care se ocupă de configurare și demontare.
După cum puteți vedea, este foarte ușor să scrieți teste de integrare folosind biblioteca mongo-unit. Nu batem joc de MongoDB în sine și putem folosi aceleași modele Mongoose. Avem control deplin asupra datelor bazei de date și nu pierdem mult din performanțe de testare, deoarece MongoDB fals rulează în memorie.
Acest lucru ne permite, de asemenea, să aplicăm cele mai bune practici de testare unitară pentru testele de integrare:
- Faceți fiecare test independent de celelalte teste. Încărcăm date noi înainte de fiecare test, oferindu-ne o stare total independentă pentru fiecare test.
- Utilizați starea minimă necesară pentru fiecare test. Nu trebuie să populam întreaga bază de date. Trebuie doar să setăm datele minime necesare pentru fiecare test în parte.
- Putem reutiliza o conexiune pentru baza de date. Mărește performanța testului.
Ca bonus, putem chiar rula aplicația în sine împotriva mongo-unit. Ne permite să facem teste end-to-end pentru aplicația noastră față de o bază de date batjocorită.
Teste end-to-end cu seleniu
Pentru testarea end-to-end, vom folosi Selenium WebDriver și Hermione E2E test runner.
Mai întâi, vom porni driverul și rulerul de testare:
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))
De asemenea, vom avea nevoie de câteva funcții de ajutor (tratarea erorilor a fost eliminată pentru concizie):
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)) }
După ce umplem baza de date cu câteva date și o curățăm odată ce testele sunt făcute, putem rula primele noastre teste:
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) }) }) })
După cum puteți vedea, testele end-to-end arată foarte asemănătoare cu testele de integrare.
Învelire
Integrarea și testarea end-to-end sunt importante pentru orice aplicație la scară largă. Aplicațiile Node.js, în special, pot beneficia enorm de pe urma testării automate. Cu mongo-unit, puteți scrie integrare și testare end-to-end fără să vă faceți griji cu privire la toate provocările care vin cu astfel de teste.
Puteți găsi exemple complete despre cum să utilizați mongo-unit pe GitHub.
Citiți suplimentare pe blogul Toptal Engineering:
- Crearea unui API REST Node.js/TypeScript, partea 3: MongoDB, autentificare și teste automate