2.1.7 • Published 2 years ago

sinon-mongo-ts v2.1.7

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

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"
// sinon.mongo is now available!

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" } }),
})
// 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'}

It's caused by sinon, and quick solution for it for now is

// 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

Examples / Best Typescript practices

// ---- 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"))

Mock transaction (sinon-mongo-ts only)

// 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()
}

$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)
// "first"

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])

// 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);

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])

// 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(...);

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])

// 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();

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)])

// 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()

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)])

// 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" })

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(),
    }

    // 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)
    })
  })
})

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();

    // 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
  });
});

License

MIT © Daniel Jimenez Garcia

2.1.7

2 years ago

2.1.6

2 years ago

2.1.5

2 years ago

2.1.4

2 years ago

2.1.3

2 years ago

2.1.2

2 years ago

2.1.1

2 years ago

2.1.0

2 years ago