使用 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、身份驗證和自動化測試