Node.jsとMongoDBで統合とエンドツーエンドのテストが簡単に

公開: 2022-03-11

テストは、堅牢なNode.jsアプリケーションを構築するための重要な部分です。 適切なテストは、開発者がNode.js開発ソリューションについて指摘する可能性のある多くの欠点を簡単に克服できます。

多くの開発者は単体テストで100%のカバレッジに焦点を合わせていますが、作成するコードを単独でテストするだけではないことが重要です。 統合テストとエンドツーエンドテストにより、アプリケーションの一部を一緒にテストすることで、信頼性が高まります。 これらの部分はそれ自体で問題なく機能する可能性がありますが、大規模なシステムでは、コードのユニットが個別に機能することはめったにありません。

Node.jsとMongoDBは一緒になって、最近最も人気のあるデュオの1つを形成しています。 あなたがたまたまそれらを使用している多くの人々の一人であるなら、あなたは幸運です。

この記事では、複雑な環境や複雑なセットアップ/分解コードをセットアップすることなく、データベースの実際のインスタンスで実行されるNode.jsおよびMongoDBアプリケーションの統合テストとエンドツーエンドテストを簡単に作成する方法を学習します。 。

mongo-unitパッケージがNode.jsでの統合とエンドツーエンドのテストにどのように役立つかがわかります。 Node.js統合テストのより包括的な概要については、この記事を参照してください。

実データベースの取り扱い

通常、統合テストまたはエンドツーエンドテストの場合、スクリプトはテスト目的で実際の専用データベースに接続する必要があります。 これには、すべてのテストケース/スイートの最初と最後に実行されるコードを記述して、データベースがクリーンで予測可能な状態にあることを確認することが含まれます。

これは一部のプロジェクトではうまく機能する可能性がありますが、いくつかの制限があります。

  • テスト環境は非常に複雑になる可能性があります。 データベースをどこかで実行し続ける必要があります。 多くの場合、これにはCIサーバーをセットアップするための追加の作業が必要です。
  • データベースと操作は比較的遅くなる可能性があります。 データベースはネットワーク接続を使用し、操作にはファイルシステムのアクティビティが必要になるため、何千ものテストをすばやく実行するのは簡単ではない場合があります。
  • データベースは状態を保持しており、テストにはあまり便利ではありません。 テストは互いに独立している必要がありますが、共通のDBを使用すると、1つのテストが他のテストに影響を与える可能性があります。

一方、実際のデータベースを使用すると、テスト環境が可能な限り本番環境に近くなります。 これは、このアプローチの特別な利点と見なすことができます。

実際のインメモリデータベースの使用

テストに実際のデータベースを使用することには、いくつかの課題があるようです。 ただし、実際のデータベースを使用する利点はあまりにも優れているため、受け継ぐことはできません。 どうすれば課題を回避し、アドバンテージを維持できますか?

別のプラットフォームの優れたソリューションを再利用し、それをNode.jsの世界に適用することは、ここに行く方法です。

Javaプロジェクトは、この目的のためにインメモリデータベース(H2など)でDBUnitを広く使用しています。

DBUnitはJUnit(Javaテストランナー)と統合されており、各テスト/テストスイートなどのデータベース状態を定義できます。これにより、上記の制約がなくなります。

  • DBUnitとH2はJavaライブラリであるため、追加の環境を設定する必要はありません。 すべてJVMで実行されます。
  • インメモリデータベースにより、この状態管理が非常に高速になります。
  • DBUnitを使用すると、データベースの構成が非常に簡単になり、それぞれの場合にデータベースの状態を明確に保つことができます。
  • H2はSQLデータベースであり、MySQLと部分的に互換性があるため、ほとんどの場合、アプリケーションは本番データベースと同様にH2を使用できます。

これらの概念から、Node.jsとMongoDBに似たものを作成することにしました:Mongo-unit。

Mongo-unitは、NPMまたはYarnを使用してインストールできるNode.jsパッケージです。 MongoDBをメモリ内で実行します。 Mochaとうまく統合し、データベースの状態を管理するためのシンプルなAPIを提供することで、統合テストを容易にします。

ライブラリは、一般的なオペレーティングシステム用のビルド済みのMongoDBバイナリを含むmongodb-prebuiltNPMパッケージを使用します。 これらのMongoDBインスタンスは、メモリ内モードで実行できます。

Mongoユニットのインストール

プロジェクトにmongo-unitを追加するには、次のコマンドを実行できます。

 npm install -D mongo-unit

また

yarn add mongo-unit

そして、それだけです。 このパッケージを使用するために、コンピューターにMongoDBをインストールする必要はありません。

統合テストにMongoユニットを使用する

タスクを管理するための単純な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がメモリ内で実行されているため、データベースデータを完全に制御でき、テストパフォーマンスをあまり損なうことはありません。

これにより、統合テストに最適な単体テスト手法を適用することもできます。

  • 各テストを他のテストから独立させます。 各テストの前に新しいデータをロードし、各テストで完全に独立した状態を提供します。
  • 各テストに最低限必要な状態を使用します。 データベース全体にデータを入力する必要はありません。 特定のテストごとに最低限必要なデータを設定するだけで済みます。
  • 1つの接続をデータベースに再利用できます。 テストのパフォーマンスが向上します。

ボーナスとして、mongo-unitに対してアプリケーション自体を実行することもできます。 これにより、モックされたデータベースに対してアプリケーションのエンドツーエンドのテストを行うことができます。

Seleniumを使用したエンドツーエンドのテスト

エンドツーエンドのテストには、SeleniumWebDriverとHermioneE2Eテストランナーを使用します。

まず、ドライバーとテストランナーをブートストラップします。

 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 Engineeringブログでさらに読む:

  • Node.js / TypeScript REST APIの構築、パート3:MongoDB、認証、および自動テスト