Integration und End-to-End-Tests leicht gemacht mit Node.js und MongoDB

Veröffentlicht: 2022-03-11

Tests sind ein wesentlicher Bestandteil beim Erstellen einer robusten Node.js-Anwendung. Richtige Tests können viele Mängel, auf die Entwickler bei den Entwicklungslösungen von Node.j hinweisen, leicht überwinden.

Während sich viele Entwickler bei Unit-Tests auf eine 100-prozentige Abdeckung konzentrieren, ist es wichtig, dass der von Ihnen geschriebene Code nicht nur isoliert getestet wird. Integrations- und End-to-End-Tests geben Ihnen dieses zusätzliche Vertrauen, indem Sie Teile Ihrer Anwendung gemeinsam testen. Diese Teile können für sich allein gut funktionieren, aber in einem großen System funktionieren Codeeinheiten selten separat.

Node.js und MongoDB bilden zusammen eines der beliebtesten Duos der letzten Zeit. Wenn Sie zufällig einer der vielen Menschen sind, die sie verwenden, haben Sie Glück.

In diesem Artikel erfahren Sie, wie Sie auf einfache Weise Integrations- und End-to-End-Tests für Ihre Node.js- und MongoDB-Anwendung schreiben, die auf echten Instanzen der Datenbank ausgeführt werden, ohne eine aufwändige Umgebung oder komplizierten Setup-/Teardown-Code einrichten zu müssen .

Sie werden sehen, wie das mongo-unit-Paket bei der Integration und End-to-End-Tests in Node.js hilft. Eine umfassendere Übersicht über die Node.js-Integrationstests finden Sie in diesem Artikel.

Umgang mit einer echten Datenbank

In der Regel müssen Ihre Skripts für Integrations- oder End-to-End-Tests zu Testzwecken eine Verbindung zu einer echten dedizierten Datenbank herstellen. Dazu gehört das Schreiben von Code, der am Anfang und Ende jedes Testfalls/jeder Suite ausgeführt wird, um sicherzustellen, dass sich die Datenbank in einem sauberen, vorhersagbaren Zustand befindet.

Dies kann für einige Projekte gut funktionieren, hat aber einige Einschränkungen:

  • Die Testumgebung kann sehr komplex sein. Sie müssen die Datenbank irgendwo am Laufen halten. Dies erfordert oft zusätzlichen Aufwand bei der Einrichtung mit CI-Servern.
  • Die Datenbank und die Operationen können relativ langsam sein. Da die Datenbank Netzwerkverbindungen verwendet und die Vorgänge Dateisystemaktivitäten erfordern, ist es möglicherweise nicht einfach, Tausende von Tests schnell auszuführen.
  • Die Datenbank behält den Status bei und ist für Tests nicht sehr praktisch. Tests sollten unabhängig voneinander sein, aber die Verwendung einer gemeinsamen Datenbank könnte dazu führen, dass sich ein Test auf andere auswirkt.

Andererseits macht die Verwendung einer echten Datenbank die Testumgebung so nah wie möglich an der Produktion. Dies kann als besonderer Vorteil dieses Ansatzes angesehen werden.

Verwenden einer echten In-Memory-Datenbank

Die Verwendung einer echten Datenbank zum Testen scheint einige Herausforderungen zu haben. Aber der Vorteil der Verwendung einer echten Datenbank ist zu gut, um ihn weiterzugeben. Wie können wir die Herausforderungen umgehen und den Vorteil behalten?

Eine gute Lösung von einer anderen Plattform wiederzuverwenden und auf die Node.js-Welt anzuwenden, kann hier der richtige Weg sein.

Java-Projekte verwenden zu diesem Zweck häufig DBUnit mit einer In-Memory-Datenbank (z. B. H2).

DBUnit ist in JUnit (dem Java-Test-Runner) integriert und ermöglicht es Ihnen, den Datenbankstatus für jeden Test/jede Testsuite usw. zu definieren. Es beseitigt die oben diskutierten Einschränkungen:

  • DBUnit und H2 sind Java-Bibliotheken, sodass Sie keine zusätzliche Umgebung einrichten müssen. Es läuft alles in der JVM.
  • Die In-Memory-Datenbank macht diese Zustandsverwaltung sehr schnell.
  • DBUnit macht die Datenbankkonfiguration sehr einfach und ermöglicht es Ihnen, für jeden Fall einen klaren Datenbankstatus zu behalten.
  • H2 ist eine SQL-Datenbank und teilweise mit MySQL kompatibel, sodass die Anwendung in den meisten Fällen damit wie mit einer Produktionsdatenbank arbeiten kann.

Ausgehend von diesen Konzepten habe ich mich entschieden, etwas Ähnliches für Node.js und MongoDB zu machen: Mongo-unit.

Mongo-unit ist ein Node.js-Paket, das mit NPM oder Yarn installiert werden kann. Es führt MongoDB im Arbeitsspeicher aus. Es macht Integrationstests einfach, indem es sich gut in Mocha integriert und eine einfache API zur Verwaltung des Datenbankstatus bereitstellt.

Die Bibliothek verwendet das mongodb-vorgefertigte NPM-Paket, das vorgefertigte MongoDB-Binärdateien für die gängigen Betriebssysteme enthält. Diese MongoDB-Instanzen können im In-Memory-Modus ausgeführt werden.

Mongo-Einheit installieren

Um Mongo-Unit zu Ihrem Projekt hinzuzufügen, können Sie Folgendes ausführen:

 npm install -D mongo-unit

oder

 yarn add mongo-unit

Und das ist alles. Sie müssen nicht einmal MongoDB auf Ihrem Computer installiert haben, um dieses Paket zu verwenden.

Verwenden von Mongo-Unit für Integrationstests

Stellen wir uns vor, Sie haben eine einfache Node.js-Anwendung zum Verwalten von Aufgaben:

 // 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) }

Die MongoDB-Verbindungs-URL ist hier nicht hartcodiert. Wie bei den meisten Back-Ends von Webanwendungen nehmen wir sie aus der Umgebungsvariable. Dadurch können wir sie während der Tests durch jede beliebige URL ersetzen.

 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'))

Dies ist ein Ausschnitt einer Beispielanwendung mit einer Benutzeroberfläche. Der Code für die Benutzeroberfläche wurde der Kürze halber weggelassen. Sie können sich das vollständige Beispiel auf GitHub ansehen.

Integration mit Mocha

Damit Mocha Integrationstests für mongo-unit ausführen kann, müssen wir die mongo-unit-Datenbankinstanz ausführen, bevor der Anwendungscode im Node.js-Kontext geladen wird. Dazu können wir den Parameter mocha --require und die Mocha-prepare-Bibliothek verwenden, mit denen Sie asynchrone Vorgänge in den erforderlichen Skripten ausführen können.

 // 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() }))

Integrationstests schreiben

Der erste Schritt besteht darin, der Testdatenbank ( testData.json ) einen Test hinzuzufügen:

 { "tasks": [ { "name": "test", "started": "2017-08-28T16:07:38.268Z", "completed": false } ] }

Der nächste Schritt besteht darin, die Tests selbst hinzuzufügen:

 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) }) }) })

Und voilà!

Beachten Sie, dass es nur ein paar Codezeilen gibt, die sich mit Setup und Teardown befassen.

Wie Sie sehen können, ist es sehr einfach, Integrationstests mit der Mongo-Unit-Bibliothek zu schreiben. Wir verspotten MongoDB selbst nicht und können dieselben Mongoose-Modelle verwenden. Wir haben die volle Kontrolle über die Datenbankdaten und verlieren nicht viel an Testleistungen, da die gefälschte MongoDB im Speicher läuft.

Dies ermöglicht uns auch, die besten Unit-Testing-Praktiken für Integrationstests anzuwenden:

  • Machen Sie jeden Test unabhängig von anderen Tests. Wir laden vor jedem Test neue Daten, wodurch wir für jeden Test einen völlig unabhängigen Zustand erhalten.
  • Verwenden Sie für jeden Test den mindestens erforderlichen Zustand. Wir müssen nicht die gesamte Datenbank füllen. Wir müssen nur die minimal erforderlichen Daten für jeden einzelnen Test festlegen.
  • Wir können eine Verbindung für die Datenbank wiederverwenden. Es erhöht die Testleistung.

Als Bonus können wir sogar die Anwendung selbst gegen mongo-unit ausführen. Es ermöglicht uns, End-to-End-Tests für unsere Anwendung gegen eine simulierte Datenbank durchzuführen.

End-to-End-Tests mit Selenium

Für End-to-End-Tests verwenden wir Selenium WebDriver und Hermine E2E Test Runner.

Zuerst werden wir den Treiber und den Testläufer booten:

 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))

Wir werden auch einige Hilfsfunktionen benötigen (Fehlerbehandlung der Kürze halber entfernt):

 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)) }

Nachdem wir die Datenbank mit einigen Daten gefüllt und nach Abschluss der Tests bereinigt haben, können wir unsere ersten Tests durchführen:

 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) }) }) })

Wie Sie sehen können, sehen die End-to-End-Tests den Integrationstests sehr ähnlich.

Einpacken

Integration und End-to-End-Tests sind für jede groß angelegte Anwendung wichtig. Insbesondere Node.js-Anwendungen können enorm von automatisierten Tests profitieren. Mit mongo-unit können Sie Integrations- und End-to-End-Tests schreiben, ohne sich um all die Herausforderungen kümmern zu müssen, die mit solchen Tests einhergehen.

Vollständige Beispiele zur Verwendung von mongo-unit finden Sie auf GitHub.

Weiterführende Literatur im Toptal Engineering Blog:

  • Erstellen einer Node.js/TypeScript-REST-API, Teil 3: MongoDB, Authentifizierung und automatisierte Tests