MongoDBをインメモリで動かしてユニットテスト
はじめに
MongoDB へのクエリ部分をユニットテストしたいなーと思って、インメモリで MongoDB を動かしてくれるJest preset
のshelfio/jest-mongodbを使ってみました。
公式ドキュメント(Using with MongoDB · Jest)がそこそこわかる人向けな感じなので、本当に動くんかなと思いながら試して苦労しました。
試行錯誤しながら動かした結果を記載します。
Node.js x MongoDB 使ってバックエンド開発してて、DB へのクエリ部分もしっかりユニットテストしたい人の役に立てたら良いかなと思います。
ここにコードを置いてます。
optimisuke/hello-jest-mongodb: MongoDB をインメモリでテスト
環境構築
Docker
MongoDB を Docker で動かすためdocker-compose.yml
を準備しておきます。
MongoDB に接続する Node.js を動かすためにbackend
という名前でこっちも記載しておきます。
Node.js はローカルで動かしても MongoDB にアクセスできるようにポートを開けておきます。
接続情報をMONGO_URL
として環境変数で準備してますが、ここでベタガキしちゃってます。
アンチパターンな気がするので、本番環境ではいい感じに設定必要です。
version: "3.1"
services:
backend:
build: ./backend/
depends_on:
- mongo
environment:
- MONGO_URL=mongodb://testuser:password@mongo:27017/hoge?authSource=admin
volumes:
- ./backend:/app
working_dir: /app
tty: true
networks:
- dev_network
# command: bash -c "npm install && npm start"
mongo:
image: mongo
restart: always
networks:
- dev_network
ports:
- 27017:27017
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: password
volumes:
- mongo_db_data:/data/db
- ./mongodb/init.js:/docker-entrypoint-initdb.d/init.js:ro
volumes:
mongo_db_data:
networks:
dev_network:
backend
のイメージは Dockerfile をビルドしてます。
USER
の設定しないと permission のエラー出たので、調べながらコピペして書きました。
結局、基本的にローカルで動かしてたので、徒労気味でした。
FROM node:18-slim
RUN npm i npm@latest -g
RUN mkdir /app && chown node:node /app
WORKDIR /app
USER node
COPY package.json package-lock.json* ./
RUN npm ci && npm cache clean --force
ENV PATH /app/node_modules/.bin:$PATH
MongoDB の初期設定のために、js ファイルをマウントしてます。
接続時の権限を絞るためにユーザーを作成してます。
ここも、環境変数で読み込む等の気配りが必要ですがベタガキしちゃってます。要注意。
db = db.getSiblingDB("admin");
db.auth("root", "password");
db.createUser({
user: "testuser",
pwd: "password",
roles: [
{
role: "readWrite",
db: "hoge",
},
],
});
db = db.getSiblingDB("hoge");
db.createCollection("test");
const doc1 = { name: "Hoge", age: 36 };
const doc2 = { name: "Fuga", age: 32 };
db.test.insertMany([doc1, doc2]);
ローカルとコンテナと両方使って確認してたので、node_modules
が被らないように、.dockerignore
に記載しました。
node_modules
.git
Node.js
依存してるパッケージを色々インストールします。
npm init --yes
npx tsc --init
npm install mongodb
npm install -D ts-node
npm install -D typescript @types/node
npm install -D @shelf/jest-mongodb
npm install -D jest @types/jest ts-jest
package.json
はこんな感じです。
{
"name": "hello-jest-mongodb",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"start": "npx ts-node src/index.ts",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@shelf/jest-mongodb": "^4.1.3",
"@types/jest": "^29.2.2",
"@types/node": "^18.11.9",
"jest": "^29.2.2",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1",
"typescript": "^4.8.4"
},
"dependencies": {
"mongodb": "^4.11.0"
}
}
tsconfig.json
はデフォルトのままです。
{
"compilerOptions": {
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"module": "commonjs", /* Specify what module code is generated. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"strict": true, /* Enable all strict type-checking options. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}
Jest
jest の設定はこんな感じで、ここでやっと@shelf/jest-mongodb
が出てきます。
module.exports = {
preset: "@shelf/jest-mongodb",
transform: {
"^.+\\.(ts|tsx)$": "ts-jest",
},
};
jest-mongodb
の設定はこんな感じです。
実際の MongoDB の名前と合わせるため、dbName
にhoge
と入れてますが、必要ない気がしてます。実際、コメントアウトしてもテスト通りました。ただ、合わせた方が気持ち良いので、設定してます。
module.exports = {
mongodbMemoryServerOptions: {
binary: {
version: "4.0.3",
skipMD5: true,
},
autoStart: false,
instance: {
dbName: "hoge",
},
},
};
コード
コードは 3 つのファイルに分けて書いてます。
最初はこんな感じ。
mongoDB へのコネクションを確立して、クエリして、クローズする感じです。そこそこシンプルに書いたつもりです。
import { MongoDB } from "./mongo";
import { TestQuery } from "./testQuery";
const main = async () => {
const mongoDB = new MongoDB();
await mongoDB.connect();
console.log("MongoDB Connected.");
const collections = mongoDB.getCollections();
const testQuery = new TestQuery(collections);
const users = await testQuery.getUsers();
console.log(users);
const user = await testQuery.getUserByName("Hoge");
console.log(user);
await mongoDB.close();
};
main();
下記サイトを参考に、TypeScript の型を活用するために、コレクションをまとめる型を作りました。現在一つしかないので、ややこしくしてるだけですが、将来楽になるはずです(なってほしい)。
MongoDB + Node.js + TypeScript が強力だった | kohsweblog
あとは、ローカルで動かしているときは、環境変数を設定しなくても動くようにしてます。
collections
とclient
を| undefined
にしてるのが気になってます。もうちょい、うまいこと書く方法ないんやろか。
import { Collection, MongoClient, ObjectId } from "mongodb";
export type Test = {
_id?: ObjectId;
name: string;
age: number;
};
export type Collections = {
test: Collection<Test>;
};
export class MongoDB {
collections: Collections | undefined;
client: MongoClient | undefined;
connect = async () => {
const URL =
process.env.MONGO_URL ||
"mongodb://testuser:password@127.0.0.1:27017/hoge?authSource=admin";
this.client = await MongoClient.connect(URL);
const db = this.client.db("hoge");
// this.collections!.test =
const test = db.collection<Test>("test");
this.collections = { test: test };
};
getCollections = () => {
return this.collections!;
};
close = async () => {
await this.client?.close();
};
}
最後に、MongoDB へのクエリ部分です。ようやくです。
コンストラクタで、コレクションをもらう感じにして DI 的なことをやってます。これにより、テスト時に MongoDB をインメモリのものに簡単に置き換えられます。
import { Collections, Test } from "./mongo";
export class TestQuery {
collections: Collections;
constructor(collections: Collections) {
this.collections = collections;
}
getUsers = async () => {
const result = (await this.collections.test.find().toArray()) as Test[];
return result;
};
getUserByName = async (name: string) => {
const result = await this.collections.test.findOne(
{ name: name },
{ projection: { _id: 0 } }
);
return result as Test;
};
}
動かすとこんな感じです。
$ npm start
> hello-jest-mongodb@1.0.0 start
> npx ts-node src/index.ts
MongoDB Connected.
[
{
_id: new ObjectId("63652a85ca98ee33ec63e882"),
name: 'Hoge',
age: 36
},
{
_id: new ObjectId("63652a85ca98ee33ec63e883"),
name: 'Fuga',
age: 32
}
]
{ name: 'Hoge', age: 36 }
テスト
テストコードはこんな感じです。
beforeAll
に書かれてる接続部分が、シンプルなので、とっつきやすい気がしてます。
TestQuery()
のコンストラクタにインラインメモリで動く MongoDB 上に作ったコレクションを渡してあげることで、いい感じにテストできるようにしてます。
複数のテスト項目があるときは、どこでデータ入れるか等をもうちょいケアしてあげる必要がありそうです。
import { Collections, Test } from "./mongo";
import { TestQuery } from "./testQuery";
import { MongoClient, Db } from "mongodb";
describe("試しにテスト", () => {
let connection: MongoClient;
let db: Db;
let collections: Collections;
beforeAll(async () => {
console.log("beforeAll");
connection = await MongoClient.connect((global as any).__MONGO_URI__);
db = connection.db();
db.createCollection("test");
collections = { test: db.collection<Test>("test") };
});
beforeEach(async () => {
console.log("beforeEach");
const test = db.collection("test");
const doc1 = { name: "インメモリほげ", age: 36 };
const doc2 = { name: "インメモリふが", age: 32 };
test.insertMany([doc1, doc2]);
});
afterAll(async () => {
console.log("afterAll");
await connection.close();
});
test("指定したusernameのユーザーが取得できる", async () => {
console.log("test start");
const testQuery = new TestQuery(collections);
const result = await testQuery.getUserByName("インメモリほげ");
console.log(result);
expect(result).toStrictEqual({ name: "インメモリほげ", age: 36 });
});
});
テストを実行すると、こんな感じに出力されます。いい感じ。
$ npm test
> hello-jest-mongodb@1.0.0 test
> jest
console.log
beforeAll
at src/testQuery.test.ts:12:13
console.log
beforeEach
at src/testQuery.test.ts:21:13
console.log
test start
at src/testQuery.test.ts:35:13
console.log
{ name: 'インメモリほげ', age: 36 }
at src/testQuery.test.ts:40:13
console.log
afterAll
at src/testQuery.test.ts:30:13
PASS src/testQuery.test.ts
試しにテスト
✓ 指定したusernameのユーザーが取得できる (22 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.826 s, estimated 2 s
Ran all test suites.
おわりに
いい感じに、インメモリでサクッと動作確認できたので満足です。
テスト駆動開発したいので、インメモリでサクッと動く MongoDB を使って、ユニットテストできるのはかなり良さげでした。
MongoDB 使ってる時点で、そんな難しいクエリが必要ない気もするから、本当に DB 接続したテストって必要なのかってところではありますが。
書き方は、もうちょいリファクタリングするかもですが、活用していきたいです。
今回触れてませんが、下記サイトを見ると、もっとシンプルにnodkz/mongodb-memory-serverを使って、テストしてるので、そっちの方が依存先減らせて良いのかもです。いつか試してみたいです。
MongoDB + Jest のテスト方法 | kohsweblog
参考
jest-mongodb
Using with MongoDB · Jest
shelfio/jest-mongodb: Jest preset for MongoDB in-memory server
MongoDB + Jest のテスト方法 | kohsweblog
Express + MongoDB で作成した API サーバーを Jest でテストする - Qiita
Jest
TypeScript のテストを Jest (ts-jest) でやってみる - Qiita
kulshekhar/ts-jest: A Jest transformer with source map support that lets you use Jest to test projects written in TypeScript.
MongoDB
はじめての MongoDB with Docker
Node.js から MongoDB に接続してみる - Qiita
Discussion