Интеграция и сквозные тесты стали проще благодаря Node.js и MongoDB
Опубликовано: 2022-03-11Тесты — неотъемлемая часть создания надежного приложения Node.js. Надлежащие тесты могут легко устранить множество недостатков, на которые разработчики могут указать в решениях для разработки Node.js.
Хотя многие разработчики сосредотачиваются на 100-процентном охвате модульными тестами, важно, чтобы код, который вы пишете, не тестировался изолированно. Интеграция и сквозные тесты дают вам дополнительную уверенность, тестируя части вашего приложения вместе. Эти части могут прекрасно работать сами по себе, но в большой системе единицы кода редко работают по отдельности.
Node.js и MongoDB вместе образуют один из самых популярных дуэтов последнего времени. Если вы оказались одним из многих людей, использующих их, вам повезло.
В этой статье вы узнаете, как легко писать интеграционные и сквозные тесты для вашего приложения Node.js и MongoDB, которые работают на реальных экземплярах базы данных, без необходимости настраивать сложную среду или сложный код установки/демонтажа. .
Вы увидите, как пакет mongo-unit помогает с интеграцией и сквозным тестированием в Node.js. Более полный обзор интеграционных тестов Node.js см. в этой статье.
Работа с реальной базой данных
Как правило, для интеграции или сквозных тестов ваши сценарии должны подключаться к реальной выделенной базе данных для целей тестирования. Это включает в себя написание кода, который запускается в начале и в конце каждого тестового случая/набора, чтобы гарантировать, что база данных находится в чистом предсказуемом состоянии.
Это может хорошо работать для некоторых проектов, но имеет некоторые ограничения:
- Среда тестирования может быть довольно сложной. Вам нужно будет где-то поддерживать базу данных. Это часто требует дополнительных усилий для настройки серверов CI.
- База данных и операции могут быть относительно медленными. Поскольку база данных будет использовать сетевые подключения, а операции потребуют активности файловой системы, быстро запустить тысячи тестов может оказаться непросто.
- БД сохраняет состояние, а это не очень удобно для тестов. Тесты должны быть независимы друг от друга, но использование общей БД может привести к тому, что один тест повлияет на другие.
С другой стороны, использование реальной базы данных максимально приближает тестовую среду к рабочей. Это можно рассматривать как особое преимущество данного подхода.
Использование реальной базы данных в памяти
Использование реальной базы данных для тестирования, похоже, имеет некоторые проблемы. Но преимущества использования реальной базы данных слишком хороши, чтобы их упускать. Как мы можем обойти трудности и сохранить преимущество?
Повторное использование хорошего решения с другой платформы и его применение в мире Node.js может быть правильным решением.
Java-проекты широко используют для этой цели DBUnit с базой данных в памяти (например, H2).
DBUnit интегрирован с JUnit (средством запуска тестов Java) и позволяет вам определять состояние базы данных для каждого теста/набора тестов и т. д. Он устраняет ограничения, описанные выше:
- DBUnit и H2 — это библиотеки Java, поэтому вам не нужно настраивать дополнительную среду. Все это работает в JVM.
- База данных в памяти делает это управление состоянием очень быстрым.
- DBUnit делает настройку базы данных очень простой и позволяет сохранять четкое состояние базы данных для каждого случая.
- H2 — это база данных SQL, частично совместимая с MySQL, поэтому в большинстве случаев приложение может работать с ней как с производственной базой данных.
Взяв за основу эти концепции, я решил сделать нечто подобное для Node.js и MongoDB: Mongo-unit.
Mongo-unit — это пакет Node.js, который можно установить с помощью NPM или Yarn. Он запускает MongoDB в памяти. Он упрощает интеграционные тесты, хорошо интегрируясь с Mocha и предоставляя простой API для управления состоянием базы данных.
Библиотека использует предварительно собранный пакет NPM mongodb, который содержит готовые двоичные файлы MongoDB для популярных операционных систем. Эти экземпляры MongoDB могут работать в режиме памяти.
Установка Mongo-модуля
Чтобы добавить mongo-unit в свой проект, вы можете запустить:
npm install -D mongo-unit
или
yarn add mongo-unit
И это все. Вам даже не нужно устанавливать MongoDB на свой компьютер, чтобы использовать этот пакет.
Использование Mongo-unit для интеграционных тестов
Давайте представим, что у вас есть простое приложение Node.js для управления задачами:
// 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) }
URL-адрес подключения MongoDB здесь не является жестко закодированным. Как и в случае с большинством серверных частей веб-приложений, мы берем его из переменной среды. Это позволит нам заменить его на любой URL-адрес во время тестов.
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'))
Это фрагмент примера приложения с пользовательским интерфейсом. Код пользовательского интерфейса для краткости опущен. Вы можете проверить полный пример на GitHub.
Интеграция с Мокко
Чтобы Mocha запускал интеграционные тесты для mongo-unit, нам нужно запустить экземпляр базы данных mongo-unit перед загрузкой кода приложения в контексте Node.js. Для этого мы можем использовать параметр mocha --require
и библиотеку Mocha-prepare, позволяющую выполнять асинхронные операции в требуемых скриптах.

// 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() }))
Написание интеграционных тестов
Первый шаг — добавить тест в тестовую базу данных ( testData.json
):
{ "tasks": [ { "name": "test", "started": "2017-08-28T16:07:38.268Z", "completed": false } ] }
Следующим шагом будет добавление самих тестов:
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) }) }) })
И, вуаля!
Обратите внимание, что есть всего пара строк кода, связанных с установкой и демонтажем.
Как видите, писать интеграционные тесты с помощью библиотеки mongo-unit очень просто. Мы не издеваемся над самой MongoDB и можем использовать те же модели Mongoose. Мы имеем полный контроль над данными базы данных и не сильно теряем производительность при тестировании, так как поддельная MongoDB работает в памяти.
Это также позволяет нам применять лучшие практики модульного тестирования для интеграционных тестов:
- Сделайте каждый тест независимым от других тестов. Мы загружаем свежие данные перед каждым тестом, что дает нам полностью независимое состояние для каждого теста.
- Используйте минимально необходимое состояние для каждого теста. Нам не нужно заполнять всю базу данных. Нам нужно только установить минимально необходимые данные для каждого конкретного теста.
- Мы можем повторно использовать одно соединение для базы данных. Это увеличивает производительность теста.
В качестве бонуса мы можем даже запустить само приложение против mongo-unit. Это позволяет нам проводить сквозные тесты для нашего приложения с имитированной базой данных.
Сквозные тесты с Selenium
Для сквозного тестирования мы будем использовать Selenium WebDriver и средство запуска тестов Hermione E2E.
Во-первых, мы загрузим драйвер и средство запуска тестов:
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))
Нам также понадобятся некоторые вспомогательные функции (обработка ошибок удалена для краткости):
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)) }
После заполнения базы данных некоторыми данными и ее очистки после завершения тестов мы можем запустить наши первые тесты:
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) }) }) })
Как видите, сквозные тесты очень похожи на интеграционные.
Заворачивать
Интеграция и сквозное тестирование важны для любого крупномасштабного приложения. В частности, приложения Node.js могут значительно выиграть от автоматизированного тестирования. С mongo-unit вы можете писать интеграционные и сквозные тесты, не беспокоясь обо всех проблемах, связанных с такими тестами.
Вы можете найти полные примеры использования mongo-unit на GitHub.
Дальнейшее чтение в блоге Toptal Engineering:
- Создание Node.js/TypeScript REST API, часть 3: MongoDB, аутентификация и автоматические тесты