-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit f01a69c
Showing
8 changed files
with
4,157 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
test.* | ||
dist/.mongot | ||
node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
# Mongohat | ||
Simple MongoDB testing library for NodeJs applications. Inspired by [`mongo-unit`](https://www.npmjs.com/package/mongo-unit). | ||
It works with `mongodb-memory-server` as a main dependency. | ||
The major focus of the package is to simplify the loading of seed data for each test suite, and can refresh the | ||
the database to bring back its initial state before the test suite making it easy to test several scenario before calling a `.refresh()` | ||
on the `Mongohat` instance. | ||
|
||
## Mongohat methods | ||
The following methods are exposed to make test writing easy and fun again :) | ||
|
||
### New Mongohat Instance | ||
Every instance created can connect to existing database, and if the database name does not exist it creates | ||
a new one. | ||
Do not call this in a loop. | ||
```js | ||
MongohatOption { | ||
dbName: string; | ||
dbPath: string; | ||
dbPort?: number; | ||
useReplicaSet?: boolean; | ||
version?: string; | ||
} | ||
const mongohat = new Mongohat("DATEBASE-NAME", option); | ||
``` | ||
`option` here is of type `MongohatOption` | ||
|
||
### `.start(verbose)` | ||
This method starts the in-memory server instance. If `verbose` is true then thelogs from `mongodb-memory-server` are directly piped to the | ||
console output | ||
### `.load([{...}, {...}], retainPreviousData = false)` | ||
This loads in the initial data into the in-memory server. If `retainPreviousData` is set to true (default is false), then the loaded data is | ||
added to the existing data, otherwise it overrides it giving `Mongohat` a new state. | ||
```js | ||
await mongohat.load({ | ||
inventory: [ | ||
{ | ||
productName: "test", | ||
qty: 5 | ||
}, | ||
{ | ||
productName: "test", | ||
qty: 2 | ||
}, | ||
... | ||
{ | ||
productName: "John", | ||
qty: 8 | ||
}, | ||
], | ||
products: [ | ||
{ | ||
... | ||
}, | ||
], | ||
}); | ||
``` | ||
|
||
### `.getCollection(collectionName)` | ||
This returns a collection object of type `Collection<Document>`, and can used to further interact with the collection | ||
```js | ||
const inventory = mongohat.getCollection("inventory"); | ||
await inventory.insertOne({ | ||
_id: new ObjectId("56d9bf92f9be48771d6fe5b1"), | ||
productName: "Collection Insert", | ||
qty: 78, | ||
} as unknown as Document); | ||
|
||
``` | ||
### `.refresh()` | ||
This method can be called in the `afterEach` or `beforeEach` hook of the test suite (or as applicable to your scenario). | ||
```js | ||
beforeEach(async() => { | ||
await mongohat.refresh(); | ||
}) | ||
``` | ||
It reverts the state of the test data to the state after the last `.load()` was called. | ||
|
||
### `.getDBUrl()` | ||
This method returns the dynamic connection string assigned to the instance of the mongodb running in-memory. | ||
If called before a client is well instantiated it throws an exception. | ||
This should be called after the `.start()` method has completed. | ||
```js | ||
const mongohat = new Mongohat("<DATABASE-NAME>"); | ||
await mongohat.start(false); | ||
... | ||
process.env.DB_URL = mongohat.getDBUrl() | ||
// refresh your config here | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { Collection, MongoClient } from "mongodb"; | ||
import { MongoMemoryServer, MongoMemoryReplSet } from "mongodb-memory-server"; | ||
export interface MongohatOption { | ||
dbName: string; | ||
dbPath: string; | ||
dbPort?: number; | ||
useReplicaSet?: boolean; | ||
version?: string; | ||
} | ||
export declare class Mongohat { | ||
protected context: string; | ||
protected dataDir: string; | ||
protected defaultTempDir: string; | ||
protected defaultPort: number; | ||
protected config: MongohatOption; | ||
protected mongod: MongoMemoryServer | MongoMemoryReplSet; | ||
protected dbUrl: string; | ||
protected debug: any; | ||
protected testData: any; | ||
protected client: MongoClient; | ||
/** | ||
* | ||
*/ | ||
constructor(contextName: string, option?: MongohatOption); | ||
private initMongo; | ||
start(verbose: boolean): Promise<void> | Promise<string>; | ||
private checkTempDirExist; | ||
private killPreviousMongoProcess; | ||
private getFreePort; | ||
private prepareMongoOptions; | ||
load(data: any, retainPreviousData?: boolean): Promise<import("mongodb").InsertManyResult<import("bson").Document>[]>; | ||
getCollection(collectionName: string): Collection<Document>; | ||
refresh(): Promise<import("mongodb").InsertManyResult<import("bson").Document>[]>; | ||
clean(data?: {}): Promise<(boolean | void)[]>; | ||
drop(): Promise<boolean>; | ||
private dropDB; | ||
private delay; | ||
getDBUrl(): string; | ||
stop(): Promise<void>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Mongohat = void 0; | ||
const mongodb_1 = require("mongodb"); | ||
const mongodb_memory_server_1 = require("mongodb-memory-server"); | ||
const fs = require("fs"); | ||
const ps = require("ps-node"); | ||
const Debug = require("debug"); | ||
const portfinder = require("portfinder"); | ||
class Mongohat { | ||
/** | ||
* | ||
*/ | ||
constructor(contextName, option) { | ||
this.dataDir = "/.Mongohat"; | ||
this.defaultPort = 27777; | ||
this.context = contextName; | ||
this.defaultTempDir = `${__dirname}${this.dataDir}`; | ||
this.config = { | ||
dbName: this.context && this.context.trim().length > 0 | ||
? this.context | ||
: `Mongohat-test`, | ||
dbPath: this.defaultTempDir, | ||
dbPort: this.defaultPort, | ||
useReplicaSet: false, | ||
}; | ||
this.debug = Debug("Mongohat"); | ||
if (option) | ||
this.config = Object.assign(Object.assign({}, this.config), option); | ||
} | ||
initMongo() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
this.mongod = this.config.useReplicaSet | ||
? yield mongodb_memory_server_1.MongoMemoryReplSet.create(this.prepareMongoOptions()) | ||
: yield mongodb_memory_server_1.MongoMemoryServer.create(this.prepareMongoOptions()); | ||
this.dbUrl = this.mongod.getUri(); | ||
this.client = yield mongodb_1.MongoClient.connect(this.dbUrl, { | ||
useUnifiedTopology: true, | ||
}); | ||
this.debug(`Mongohat DB connection accessible via ${this.dbUrl}`); | ||
}); | ||
} | ||
start(verbose) { | ||
if (verbose) { | ||
Debug.enable("Mongohat"); | ||
Debug.enable("*"); | ||
} | ||
this.debug("Starting Mongohat..."); | ||
if (this.dbUrl) { | ||
return Promise.resolve(this.dbUrl); | ||
} | ||
this.checkTempDirExist(this.defaultTempDir); | ||
return this.killPreviousMongoProcess(this.defaultTempDir) | ||
.then(() => this.getFreePort(this.config.dbPort)) | ||
.then(() => this.initMongo()); | ||
} | ||
checkTempDirExist(dir) { | ||
try { | ||
if (fs.existsSync(dir)) { | ||
fs.rmSync(dir, { recursive: true }); | ||
} | ||
fs.mkdirSync(dir); | ||
} | ||
catch (error) { | ||
console.error("Unable to create db folder", dir, error); | ||
if (error.code !== "EEXIST") { | ||
throw error; | ||
} | ||
} | ||
} | ||
killPreviousMongoProcess(dataPath) { | ||
return new Promise((resolve, reject) => { | ||
ps.lookup({ | ||
psargs: ["-A"], | ||
command: "mongod", | ||
arguments: dataPath, | ||
}, (err, resultList) => { | ||
if (err) { | ||
console.log("ps-node error", err); | ||
return reject(err); | ||
} | ||
resultList.forEach((process) => { | ||
if (process) { | ||
console.log("KILL PID: %s, COMMAND: %s, ARGUMENTS: %s", process.pid, process.command, process.arguments); | ||
ps.kill(process.pid); | ||
} | ||
}); | ||
return resolve(); | ||
}); | ||
}); | ||
} | ||
getFreePort(possiblePort) { | ||
portfinder.setBasePort(possiblePort); | ||
return new Promise((resolve, reject) => portfinder.getPort((err, port) => { | ||
if (err) { | ||
this.debug(`cannot get free port: ${err}`); | ||
reject(err); | ||
} | ||
else { | ||
resolve(port); | ||
} | ||
})); | ||
} | ||
prepareMongoOptions() { | ||
const mongoOption = { | ||
autoStart: false, | ||
}; | ||
if (this.config.version) { | ||
mongoOption.binary = { version: this.config.version }; | ||
} | ||
if (this.config.useReplicaSet) { | ||
mongoOption.instanceOpts = [ | ||
{ | ||
port: this.config.dbPort, | ||
dbPath: this.config.dbPath, | ||
storageEngine: "wiredTiger", | ||
}, | ||
]; | ||
mongoOption.replSet = { | ||
dbName: this.config.dbName, | ||
storageEngine: "wiredTiger", | ||
}; | ||
} | ||
else { | ||
mongoOption.instance = { | ||
port: this.config.dbPort, | ||
dbPath: this.config.dbPath, | ||
dbName: this.config.dbName, | ||
storageEngine: "ephemeralForTest", | ||
}; | ||
} | ||
return mongoOption; | ||
} | ||
load(data, retainPreviousData = false) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (!this.client) { | ||
throw new Error("The client has not been instantiated."); | ||
} | ||
if (!retainPreviousData) { | ||
yield this.clean(data); | ||
} | ||
this.testData = data; | ||
const db = this.client.db(this.config.dbName); | ||
const queries = Object.keys(data).map((col) => { | ||
const collection = db.collection(col); | ||
return collection.insertMany(data[col]); | ||
}); | ||
return Promise.all(queries); | ||
}); | ||
} | ||
getCollection(collectionName) { | ||
if (!this.client) { | ||
throw new Error("The client has not been instantiated."); | ||
} | ||
const db = this.client.db(this.config.dbName); | ||
return db.collection(collectionName); | ||
} | ||
refresh() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (!this.testData || Object.keys(this.testData).length === 0) { | ||
console.info("Test Data is empty. Nothing to refresh."); | ||
return; | ||
} | ||
return this.load(this.testData); | ||
}); | ||
} | ||
clean(data = {}) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (!this.client) { | ||
throw new Error("The client has not been instantiated."); | ||
} | ||
this.testData = {}; | ||
if (!data || Object.keys(data).length === 0) { | ||
return this.dropDB(); | ||
} | ||
const db = this.client.db(this.config.dbName); | ||
const queries = Object.keys(data).map((col) => { | ||
const collection = db.collection(col); | ||
return collection | ||
.drop() | ||
.catch((e) => console.info("Info: Collection not found.", col)); | ||
}); | ||
return Promise.all(queries); | ||
}); | ||
} | ||
drop() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (!this.client) { | ||
throw new Error("The client has not been instantiated."); | ||
} | ||
this.testData = {}; | ||
return this.client.db(this.config.dbName).dropDatabase(); | ||
}); | ||
} | ||
dropDB() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
this.testData = {}; | ||
const db = this.client.db(this.config.dbName); | ||
return db.collections().then((collections) => { | ||
const requests = collections.map((col) => col | ||
.drop() | ||
.catch((e) => console.info("Info: Collection not found.", col))); | ||
return Promise.all(requests); | ||
}); | ||
}); | ||
} | ||
delay(time) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
return new Promise(resolve => setTimeout(resolve, time)); | ||
}); | ||
} | ||
getDBUrl() { | ||
if (!this.client) { | ||
throw new Error("The client has not been instantiated."); | ||
} | ||
return this.dbUrl; | ||
} | ||
stop() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (!this.client) { | ||
throw new Error("The client has not been instantiated."); | ||
} | ||
yield this.client.close(true); | ||
yield this.mongod.stop(true); | ||
this.dbUrl = null; | ||
console.log('Killing MongoDB process...'); | ||
yield this.killPreviousMongoProcess(this.defaultTempDir); | ||
yield this.delay(100); | ||
}); | ||
} | ||
} | ||
exports.Mongohat = Mongohat; |
Oops, something went wrong.