sinon-mongo-ts
Forked sinon-mongo library by Daniel with added typings for sinon.mongo and smaller fixes like transaction support.
Extend sinon.js with stubs for testing code that uses the MongoDB Node.js driver
Installation
$ yarn add -D sinon-mongo-ts
sinon-mongo expects sinon >=6.3.0 and mongodb >=4.X as peer-dependencies.
If you use mongodb 3.X, please install version 1.1.0 of sinon-mongo
Usage
Simply import "sinon-mongo-ts"
to extend sinon with a sinon.mongo
object.
const sinon = require("sinon")
import "sinon-mongo-ts"
Then use sinon.mongo
to create stubs of various classes in the mongo API.
Important! when stubbing collections don't create stub while defining collection like:
const mockCollection = sinon.mongo.collection({
findOne: sinon
.stub()
.withArgs({ name: "foo" })
.resolves({ value: { a: "mock object" } }),
})
mockCollection.findOne
.withArgs({ name: "bar" })
.resolves({ value: { a: "another object" } })
const result = await mockCollection.findOne({ name: "foo" })
console.log(result)
const result2 = await mockCollection.findOne({ name: "bar" })
console.log(result2)
const result3 = await mockCollection.findOne({ name: "anything" })
console.log(result3)
It's caused by sinon
, and quick solution for it for now is
const mockCollection = sinon.mongo.collection()
mockCollection.findOne
.withArgs({ name: "foo" })
.resolves({ value: { a: "mock object" } })
mockCollection.findOne
.withArgs({ name: "bar" })
.resolves({ value: { a: "another object" } })
const result = await mockCollection.findOne({ name: "foo" })
console.log(result)
const result2 = await mockCollection.findOne({ name: "bar" })
console.log(result2)
const result3 = await mockCollection.findOne({ name: "anything" })
console.log(result3)
Examples / Best Typescript practices
const mockCollection = sinon.mongo.collection()
mockCollection.findOneAndUpdate
.withArgs({ name: "foo" })
.resolves({ value: { a: "mock object" } })
mockCollection.findOne.withArgs({ name: "foo" }).resolves({ a: "mock object" })
const mockDb = sinon.mongo.db({
customers: mockCollection,
})
const mockMongoClient = sinon.mongo.mongoClient({
reporting: sinon.mongo.db(),
})
mockMongoClient.db.withArgs("myDbName").returns({ the: "mock database" })
mockMongoClient.connect().then((mongoClient) => mongoClient.db("myDbName"))
Mock transaction (sinon-mongo-ts
only)
const session = mockMongoClient.startSession()
try {
await session.withTransaction(async () => {
console.log("session")
})
} catch (e) {
console.error("error: ", e)
} finally {
session.endSession()
}
$or query matcher (sinon-mongo-ts
only)
$orMatch
takes same arguments as provided to $or: [...]
query, but without $or:
each object in $orMatch
array must equals searched query object, it won't match partially
if couple "rows" will match, only last one will be returned
import { $orMatch } from "sinon-mongo-ts"
const mockUsers = sinon.mongo.collection()
mockUsers.findOne
.withArgs(
$orMatch([
{ username: "user", balance: { locked: false, amount: 100 } },
{ email: "user@email.com" },
])
)
.resolves("first")
const mockDb = sinon.mongo.db({
users: mockUsers,
})
let query = {
$or: [
{ username: "user4", balance: { locked: true, amount: 400 } },
{ username: "user", balance: { locked: false, amount: 100 } },
],
}
let result = await mockDb.collection("users").findOne(query)
console.log(result)
API
sinon.mongo.collection
Use this API to create stubs of the MongoDB Collection type.
Every method available in the MongoDB Collection type is defaulted as a sinon stub, whose behaviour you can further customise.
sinon.mongo.collection(methodStubs[optional])
const mockCollection = sinon.mongo.collection();
mockCollection.findOne.withArgs(...).resolves(...);
const mockCollection2 = sinon.mongo.collection({
findOne: sinon.stub().withArgs(...).resolves(...);
});
mockCollection2.findOneAndUpdate.withArgs(...).resolves(...);
sinon.assert.calledOnce(mockColletion2.insertOne);
sinon.mongo.db
Use this API to create stubs of the MongoDB Db type.
Every method available in the MongoDB Db type is defaulted as a sinon stub, whose behaviour you can further customise.
sinon.mongo.db(collectionMap[optional], methodStubs[optional])
const mockDb = sinon.mongo.db();
mockDb.collection.withArgs(...).resolves(...);
mockDb.dropCollection.withArgs(...).resolves(...);
const mockDb2 = sinon.mongo.db({
customers: mockCustomersCollection,
organizations: mockOrganizationsCollection
});
const mockDb3 = sinon.mongo.db({}, {
dropCollection: sinon.stub().withArgs(...).resolves(...);
});
mockDb3.listCollections.resolves(...);
sinon.mongo.mongoClient
Use this API to create stubs of the MongoDB MongoClient type.
Every method available in the MongoDB MongoClient type is defaulted as a sinon stub, whose behaviour you can further customise.
sinon.mongo.mongoClient(databaseMap[optional], methodStubs[optional])
const mockMongoClient = sinon.mongo.mongoClient();
mockMongoClient.db.withArgs(...).resolves(...);
const mockMongoClient2 = sinon.mongo.db({
default: mockDefaultDatabase,
reporting: mockReportingDatabase
});
const mockMongoClient3 = sinon.mongo.db({}, {
isConnected: sinon.stub().withArgs(...).returns(...);
});
mockMongoClient3.close.resolves();
sinon.mongo.documentArray
When testing code that uses some of the collection operations that return multiple documents, like find, you can use this helper API to quickly stub its toArray()
result, resolving to a promise with the required array.
sinon.mongo.documentArray(documents[(optional, Array | Object)])
return collection.find({ name: "foo" }).toArray()
const mockCollection = sinon.mongo.collection()
mockCollection.find
.withArgs({ name: "foo" })
.returns(
sinon.mongo.documentArray([
{ the: "first document" },
{ the: "second document" },
])
)
sinon.mongo.documentArray()
sinon.mongo.documentArray({ the: "single document" })
The returned documentArray
stub includes stub methods for skip
, limit
and sort
(all of them sinon stubs themselves) that you can use to test code like:
return collection
.find({}, { email: 1, name: 1 })
.skip(30)
.limit(10)
.sort({ name: 1 })
.toArray()
sinon.mongo.documentStream
When testing code that uses some of the collection operations that return multiple documents, like find, you can use this helper API to quickly stub its stream()
result, returning a readable stream that emits the provided documents.
sinon.mongo.documentStream(documents[(optional, Array | Object)])
return collection.find({ name: "foo" })
return collection.find({ name: "foo" }).stream()
const mockCollection = sinon.mongo.collection()
mockCollection.find
.withArgs({ name: "foo" })
.returns(
sinon.mongo.documentStream([
{ the: "first document" },
{ the: "second document" },
])
)
sinon.mongo.documentStream()
sinon.mongo.documentStream({ the: "single document" })
Examples
The following sections include full examples of what might be typical code using mongo and its unit tests using sinon-mongo.
Express controller
Let's say you have an express controller that talks directly to the database through an injected req.db
:
const mongodb = require("mongodb")
module.exports = {
get(req, res, next) {
return req.db
.collection("customers")
.findOne({ _id: mongodb.ObjectId(req.params.id) })
.then((cust) => res.send(cust))
.catch(next)
},
post(req, res, next) {
return req.db
.collection("customers")
.updateOne({ _id: mongodb.ObjectId(req.params.id) }, { $set: req.body })
.then(() => res.sendStatus(204))
.catch(next)
},
}
Then a test using sinon-mongo could look like:
const mongodb = require("mongodb")
const sinon = require("sinon")
require("sinon-mongo")
const sampleController = require("../src/sample-controller")
describe("the sample controller", () => {
let mockRequest
let mockResponse
let mockId
let mockCustomerCollection
beforeEach(() => {
mockId = mongodb.ObjectId()
mockRequest = {
params: { id: mockId.toString() },
body: { the: "mock body" },
}
mockResponse = {
send: sinon.spy(),
sendStatus: sinon.spy(),
}
mockCustomerCollection = sinon.mongo.collection()
mockRequest.db = sinon.mongo.db({
customers: mockCustomerCollection,
})
})
it("returns a customer by id", () => {
const mockCustomer = { a: "mock customer" }
mockCustomerCollection.findOne
.withArgs({ _id: mockId })
.resolves(mockCustomer)
return sampleController.get(mockRequest, mockResponse).then(() => {
sinon.assert.calledWith(mockResponse.send, mockCustomer)
})
})
it("updates a customer by id", () => {
mockCustomerCollection.updateOne
.withArgs({ _id: mockId }, { $set: mockRequest.body })
.resolves()
return sampleController.post(mockRequest, mockResponse).then(() => {
sinon.assert.calledOnce(mockCustomerCollection.updateOne)
sinon.assert.calledWith(mockResponse.sendStatus, 204)
})
})
})
Classic Repository
In this example, let's assume we have a classic repository module as:
const mongodb = require("mongodb")
module.exports = (db) => ({
findCustomersInOrganization(orgName) {
return db.collection("customers").find({ orgName }).toArray()
},
updateCustomer(id, updates) {
return db
.collection("customers")
.findOneAndUpdate({ _id: mongodb.ObjectId(id) }, { $set: updates })
.then((res) => res.value)
},
})
Notice how the db is manually injected, so in order to use this repository module you would const repo = require('./sample-repository')(dbInstance)
.
This makes easy to inject a mock db when writing a test:
const expect = require('chai').expect;
const mongodb = require('mongodb');
const sinon = require('sinon');
require('sinon-mongo');
const sampleRepository = require('../src/sample-repository');
describe('the sample repository', () => {
let mockId;
let mockDb;
let mockCustomerCollection;
let repository;
beforeEach(() => {
mockId = mongodb.ObjectId();
mockCustomerCollection = sinon.mongo.collection();
mockDb = sinon.mongo.db({
customers: mockCustomerCollection
});
repository = sampleRepository(mockDb);
});
it('returns all the customers for the given org name', () => {
const mockCustomers = [{a: 'mock customer'}, {another: 'mock customer'}];
mockCustomerCollection.find
.withArgs({ orgName: 'mockOrgName' })
.returns(sinon.mongo.documentArray(mockCustomers));
return repository.findCustomersInOrganization('mockOrgName').then(customers => {
expect(customers).to.be.eql(mockCustomers);
});
});
it('updates a customer by its id', () => {
const mockUpdates = {the: 'updated properties'};
const mockUpdatedCustomer = {the: 'updated customer'};
mockCustomerCollection.findOneAndUpdate
.withArgs({ _id: sinon.match(val => mockId.equals(val) }, { $set: mockUpdates })
.resolves({ value: mockUpdatedCustomer });
return repository.updateCustomer(mockId, mockUpdates).then(updatedCustomer => {
expect(updatedCustomer).to.be.eql(mockUpdatedCustomer);
});
});
});
A typical variant would be using a helper in the repository to retrieve the database, rather than manually injecting it. In that case you would use something like proxyquire to write your test and inject the mock db:
const getDb = require('./some/getdb-utility');
...
module.exports = db => ({
findCustomersInOrganization(orgName){
return db
.collection('customers')
.find({ orgName })
.toArray();
},
...
});
beforeEach(() => {
...
...
repository = proxyquire('../src/sample-repository', {
'./some/getdb-utility': () => mockDb
});
});
License
MIT © Daniel Jimenez Garcia