使用 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 项目广泛使用带有内存数据库(例如 H2)的 DBUnit。
DBUnit 与 JUnit(Java 测试运行器)集成,允许您为每个测试/测试套件等定义数据库状态。它消除了上面讨论的约束:
- DBUnit 和 H2 是 Java 库,因此您不需要设置额外的环境。 这一切都在JVM中运行。
- 内存数据库使这种状态管理非常快速。
- DBUnit 使数据库配置非常简单,并允许您为每种情况保持清晰的数据库状态。
- H2 是一个 SQL 数据库,它与 MySQL 部分兼容,因此在大多数情况下,应用程序可以像使用生产数据库一样使用它。
从这些概念出发,我决定为 Node.js 和 MongoDB 做一些类似的东西:Mongo-unit。
Mongo-unit 是一个可以使用 NPM 或 Yarn 安装的 Node.js 包。 它在内存中运行 MongoDB。 它通过与 Mocha 很好地集成并提供简单的 API 来管理数据库状态,使集成测试变得容易。
该库使用 mongodb-prebuilt NPM 包,其中包含用于流行操作系统的预构建 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) }MongoDB 连接 URL 在这里没有硬编码。 与大多数 Web 应用程序后端一样,我们从环境变量中获取它。 这将让我们在测试期间将其替换为任何 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'))这是具有用户界面的示例应用程序的片段。 为简洁起见,省略了 UI 的代码。 您可以在 GitHub 上查看完整的示例。
与摩卡集成
为了让 Mocha 对 mongo-unit 运行集成测试,我们需要在应用程序代码加载到 Node.js 上下文之前运行 mongo-unit 数据库实例。 为此,我们可以使用mocha --require参数和 Mocha-prepare 库,它允许您在 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() }))编写集成测试
第一步是将测试添加到测试数据库( 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,您可以编写集成和端到端测试,而不必担心此类测试带来的所有挑战。
您可以在 GitHub 上找到有关如何使用 mongo-unit 的完整示例。
进一步阅读 Toptal 工程博客:
- 构建 Node.js/TypeScript REST API,第 3 部分:MongoDB、身份验证和自动化测试
