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
$ 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
Simply import "sinon-mongo-ts"
to extend sinon with a sinon.mongo
object.
const sinon = require("sinon")
import "sinon-mongo-ts"
// sinon.mongo is now available!
Then use sinon.mongo
to create stubs of various classes in the mongo API.
const mockCollection = sinon.mongo.collection({
findOne: sinon
.stub()
.withArgs({ name: "foo" })
.resolves({ value: { a: "mock object" } }),
})
// every call to mockCollection.findOne will result in {value: {a: 'mock object'}} promise returned
mockCollection.findOne
.withArgs({ name: "bar" })
.resolves({ value: { a: "another object" } })
// Will cause
const result = await mockCollection.findOne({ name: "foo" })
console.log(result)
// {a: 'mock object'}
const result2 = await mockCollection.findOne({ name: "bar" })
console.log(result2)
// {a: 'mock object'}
const result3 = await mockCollection.findOne({ name: "anything" })
console.log(result3)
// {a: 'mock object'}
// Leave collection empty or with empty stubs
const mockCollection = sinon.mongo.collection()
// Under collection definiton define queries
mockCollection.findOne
.withArgs({ name: "foo" })
.resolves({ value: { a: "mock object" } })
mockCollection.findOne
.withArgs({ name: "bar" })
.resolves({ value: { a: "another object" } })
// And then
const result = await mockCollection.findOne({ name: "foo" })
console.log(result)
// {a: 'mock object'}
const result2 = await mockCollection.findOne({ name: "bar" })
console.log(result2)
// {a: 'another object'}
const result3 = await mockCollection.findOne({ name: "anything" })
console.log(result3)
// undefined
// ---- stub collections ----
const mockCollection = sinon.mongo.collection()
// By default, every collection method is also a sinon stub
mockCollection.findOneAndUpdate
.withArgs({ name: "foo" })
.resolves({ value: { a: "mock object" } })
mockCollection.findOne.withArgs({ name: "foo" }).resolves({ a: "mock object" })
// ---- stub databases ----
const mockDb = sinon.mongo.db({
customers: mockCollection,
})
// You can define if needed queries through mockDb but this ones are not supported
// by typescript
// IE
//
// mockDb.collection("customers").findOne.withArgs({name: "bar"}).resolves({a: "another object"})
//
// will work but cause typescript error, best practice is to do changes through colelction
// definition.
// ---- stub MongoClients ---
const mockMongoClient = sinon.mongo.mongoClient({
// optionally provide a specific map of database names and stubs
reporting: sinon.mongo.db(),
})
// By default, every MongoClient method is also a sinon stub, including the db() method
mockMongoClient.db.withArgs("myDbName").returns({ the: "mock database" })
// The connect method stub is already setup so it resolves with the mongoClient and can be chained
mockMongoClient.connect().then((mongoClient) => mongoClient.db("myDbName"))
// Also with Typescript version I added stubbing basic transactions functionality
const session = mockMongoClient.startSession()
try {
await session.withTransaction(async () => {
console.log("session")
})
} catch (e) {
console.error("error: ", e)
} finally {
session.endSession()
}
$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)
// "first"
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])
// Basic usage:
const mockCollection = sinon.mongo.collection();
mockCollection.findOne.withArgs(...).resolves(...);
// Optionally provide method stubs.
// Equivalent to the earlier example:
const mockCollection2 = sinon.mongo.collection({
findOne: sinon.stub().withArgs(...).resolves(...);
});
// Methods that were not provided are still available as stubs
mockCollection2.findOneAndUpdate.withArgs(...).resolves(...);
sinon.assert.calledOnce(mockColletion2.insertOne);
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])
// Basic usage:
const mockDb = sinon.mongo.db();
mockDb.collection.withArgs(...).resolves(...);
mockDb.dropCollection.withArgs(...).resolves(...);
// Optionally provide a collections map to avoid manually setting the behaviour of the collection() method
const mockDb2 = sinon.mongo.db({
customers: mockCustomersCollection,
organizations: mockOrganizationsCollection
});
// Optionally provide method stubs
const mockDb3 = sinon.mongo.db({}, {
dropCollection: sinon.stub().withArgs(...).resolves(...);
});
// Method stubs that were not specifically provided are still defaulted as stubs
mockDb3.listCollections.resolves(...);
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])
// Basic usage:
const mockMongoClient = sinon.mongo.mongoClient();
mockMongoClient.db.withArgs(...).resolves(...);
// Optionally provide a database map to avoid manually setting the behaviour of the db() method
const mockMongoClient2 = sinon.mongo.db({
default: mockDefaultDatabase,
reporting: mockReportingDatabase
});
// Optionally provide method stubs
const mockMongoClient3 = sinon.mongo.db({}, {
isConnected: sinon.stub().withArgs(...).returns(...);
});
// Method stubs that were not specifically provided are still defaulted as stubs
mockMongoClient3.close.resolves();
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)])
// code you want to test:
return collection.find({ name: "foo" }).toArray()
// in test code:
const mockCollection = sinon.mongo.collection()
mockCollection.find
.withArgs({ name: "foo" })
.returns(
sinon.mongo.documentArray([
{ the: "first document" },
{ the: "second document" },
])
)
// You can return an empty array or an array of a single 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()
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)])
// code you want to test (both are equivalent):
return collection.find({ name: "foo" })
return collection.find({ name: "foo" }).stream()
// in test code:
const mockCollection = sinon.mongo.collection()
mockCollection.find
.withArgs({ name: "foo" })
.returns(
sinon.mongo.documentStream([
{ the: "first document" },
{ the: "second document" },
])
)
// You can return an empty stream or an stream that emits a single document:
sinon.mongo.documentStream()
sinon.mongo.documentStream({ the: "single document" })
The following sections include full examples of what might be typical code using mongo and its unit tests using sinon-mongo.
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(),
}
// inject mock db and collection into the request object
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)
})
})
})
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();
// inject mock db into the repository
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:
// in sample-repository.js
const getDb = require('./some/getdb-utility');
...
module.exports = db => ({
findCustomersInOrganization(orgName){
return db
.collection('customers')
.find({ orgName })
.toArray();
},
...
});
// In the unit test
beforeEach(() => {
...
// inject mock db into the repository
...
repository = proxyquire('../src/sample-repository', {
'./some/getdb-utility': () => mockDb
});
});
MIT © Daniel Jimenez Garcia