Intégration et tests de bout en bout simplifiés avec Node.js et MongoDB
Publié: 2022-03-11Les tests sont une partie essentielle de la construction d'une application Node.js robuste. Des tests appropriés peuvent facilement surmonter de nombreuses lacunes que les développeurs peuvent signaler à propos des solutions de développement Node.js.
Alors que de nombreux développeurs se concentrent sur une couverture à 100 % avec des tests unitaires, il est important que le code que vous écrivez ne soit pas seulement testé de manière isolée. L'intégration et les tests de bout en bout vous donnent cette confiance supplémentaire en testant ensemble des parties de votre application. Ces parties peuvent très bien fonctionner seules, mais dans un grand système, les unités de code fonctionnent rarement séparément.
Node.js et MongoDB forment ensemble l'un des duos les plus populaires de ces derniers temps. Si vous faites partie des nombreuses personnes qui les utilisent, vous avez de la chance.
Dans cet article, vous apprendrez à écrire facilement des tests d'intégration et de bout en bout pour votre application Node.js et MongoDB qui s'exécutent sur des instances réelles de la base de données, sans avoir besoin de configurer un environnement élaboré ou un code de configuration/démontage compliqué. .
Vous verrez comment le package mongo-unit aide à l'intégration et aux tests de bout en bout dans Node.js. Pour un aperçu plus complet des tests d'intégration Node.js, consultez cet article.
Traiter avec une vraie base de données
Généralement, pour des tests d'intégration ou de bout en bout, vos scripts devront se connecter à une véritable base de données dédiée à des fins de test. Cela implique d'écrire du code qui s'exécute au début et à la fin de chaque cas/suite de test pour s'assurer que la base de données est dans un état propre et prévisible.
Cela peut bien fonctionner pour certains projets, mais présente certaines limites :
- L'environnement de test peut être assez complexe. Vous devrez garder la base de données en cours d'exécution quelque part. Cela nécessite souvent des efforts supplémentaires pour configurer les serveurs CI.
- La base de données et les opérations peuvent être relativement lentes. Étant donné que la base de données utilisera des connexions réseau et que les opérations nécessiteront une activité du système de fichiers, il peut être difficile d'exécuter rapidement des milliers de tests.
- La base de données conserve l'état, et ce n'est pas très pratique pour les tests. Les tests doivent être indépendants les uns des autres, mais l'utilisation d'une base de données commune peut faire en sorte qu'un test affecte les autres.
D'autre part, l'utilisation d'une base de données réelle rend l'environnement de test aussi proche que possible de la production. Cela peut être considéré comme un avantage particulier de cette approche.
Utilisation d'une véritable base de données en mémoire
L'utilisation d'une véritable base de données pour les tests semble poser certains problèmes. Mais l'avantage d'utiliser une vraie base de données est trop beau pour être ignoré. Comment pouvons-nous contourner les défis et garder l'avantage?
Réutiliser une bonne solution d'une autre plate-forme et l'appliquer au monde Node.js peut être la voie à suivre ici.
Les projets Java utilisent largement DBUnit avec une base de données en mémoire (par exemple, H2) à cette fin.
DBUnit est intégré à JUnit (l'exécuteur de tests Java) et vous permet de définir l'état de la base de données pour chaque test/suite de tests, etc. Il supprime les contraintes évoquées ci-dessus :
- DBUnit et H2 sont des bibliothèques Java, vous n'avez donc pas besoin de configurer un environnement supplémentaire. Tout tourne dans la JVM.
- La base de données en mémoire rend cette gestion d'état très rapide.
- DBUnit rend la configuration de la base de données très simple et vous permet de conserver un état clair de la base de données pour chaque cas.
- H2 est une base de données SQL et elle est partiellement compatible avec MySQL donc, dans les cas majeurs, l'application peut fonctionner avec elle comme avec une base de données de production.
À partir de ces concepts, j'ai décidé de créer quelque chose de similaire pour Node.js et MongoDB : Mongo-unit.
Mongo-unit est un package Node.js qui peut être installé à l'aide de NPM ou de Yarn. Il exécute MongoDB en mémoire. Il facilite les tests d'intégration en s'intégrant bien à Mocha et en fournissant une API simple pour gérer l'état de la base de données.
La bibliothèque utilise le package NPM mongodb-prebuilt, qui contient des binaires MongoDB préconstruits pour les systèmes d'exploitation populaires. Ces instances MongoDB peuvent s'exécuter en mode en mémoire.
Installation de l'unité Mongo
Pour ajouter mongo-unit à votre projet, vous pouvez exécuter :
npm install -D mongo-unit
ou
yarn add mongo-unit
Et c'est tout. Vous n'avez même pas besoin que MongoDB soit installé sur votre ordinateur pour utiliser ce package.
Utilisation de Mongo-unit pour les tests d'intégration
Imaginons que vous disposiez d'une simple application Node.js pour gérer les tâches :
// 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) }
L'URL de connexion MongoDB n'est pas codée en dur ici. Comme avec la plupart des back-ends d'applications Web, nous le prenons à partir de la variable d'environnement. Cela nous permettra de la substituer à n'importe quelle URL lors des tests.
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'))
Il s'agit d'un extrait d'un exemple d'application doté d'une interface utilisateur. Le code de l'interface utilisateur a été omis par souci de brièveté. Vous pouvez consulter l'exemple complet sur GitHub.
Intégration avec Moka
Pour que Mocha exécute des tests d'intégration sur mongo-unit, nous devons exécuter l'instance de base de données mongo-unit avant que le code de l'application ne soit chargé dans le contexte Node.js. Pour ce faire, nous pouvons utiliser le paramètre mocha --require
et la bibliothèque Mocha-prepare, qui vous permet d'effectuer des opérations asynchrones dans les scripts 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() }))
Rédaction de tests d'intégration
La première étape consiste à ajouter un test à la base de données de test ( testData.json
):
{ "tasks": [ { "name": "test", "started": "2017-08-28T16:07:38.268Z", "completed": false } ] }
L'étape suivante consiste à ajouter les tests eux-mêmes :
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) }) }) })
Et voilà !
Remarquez qu'il n'y a que quelques lignes de code traitant de l'installation et du démontage.
Comme vous pouvez le constater, il est très facile d'écrire des tests d'intégration à l'aide de la bibliothèque mongo-unit. Nous ne nous moquons pas de MongoDB lui-même et nous pouvons utiliser les mêmes modèles Mongoose. Nous avons le contrôle total des données de la base de données et ne perdons pas grand-chose sur les performances des tests puisque le faux MongoDB tourne en mémoire.
Cela nous permet également d'appliquer les meilleures pratiques de tests unitaires pour les tests d'intégration :
- Faites en sorte que chaque test soit indépendant des autres tests. Nous chargeons de nouvelles données avant chaque test, ce qui nous donne un état totalement indépendant pour chaque test.
- Utilisez l'état minimum requis pour chaque test. Nous n'avons pas besoin de remplir toute la base de données. Il nous suffit de définir les données minimales requises pour chaque test particulier.
- Nous pouvons réutiliser une connexion pour la base de données. Il augmente les performances des tests.
En prime, nous pouvons même exécuter l'application elle-même contre mongo-unit. Cela nous permet de faire des tests de bout en bout pour notre application par rapport à une base de données simulée.
Tests de bout en bout avec Selenium
Pour les tests de bout en bout, nous utiliserons Selenium WebDriver et Hermione E2E test runner.
Tout d'abord, nous allons amorcer le pilote et le lanceur de test :
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))
Nous aurons également besoin de certaines fonctions d'assistance (la gestion des erreurs a été supprimée par souci de brièveté) :
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)) }
Après avoir rempli la base de données avec quelques données et l'avoir nettoyée une fois les tests effectués, nous pouvons lancer nos premiers tests :
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) }) }) })
Comme vous pouvez le voir, les tests de bout en bout ressemblent beaucoup aux tests d'intégration.
Emballer
L'intégration et les tests de bout en bout sont importants pour toute application à grande échelle. Les applications Node.js, en particulier, peuvent bénéficier énormément des tests automatisés. Avec mongo-unit, vous pouvez écrire des tests d'intégration et de bout en bout sans vous soucier de tous les défis qui accompagnent de tels tests.
Vous pouvez trouver des exemples complets d'utilisation de mongo-unit sur GitHub.
Lectures complémentaires sur le blog Toptal Engineering :
- Création d'une API REST Node.js/TypeScript, partie 3 : MongoDB, authentification et tests automatisés