DEV Community

Alessio Michelini
Alessio Michelini

Posted on

How to test mongoose models with jest and mockingoose

A bit of introduction

Most of the times when you are getting introduced to Unit Testing, after an brief explaination showing what are unit tests and maybe the famous pyramid explaining the differences between unit tests, integration tests and E2E tests, you will be presented with your first test, possibly using the same library we are going to use today, Jest, and you will see something like this:

// sum.js
const add = (a, b) => {
  return a + b;
}

module.exports = {
  add,
};

// sum.test.js
const { add } = require('./sum');

it('should sum numbers', () => {
  expect(add(1, 2)).toBe(3);
}
Enter fullscreen mode Exit fullscreen mode

The above test is clear and easy to understand, but the reality is that, while this can be applied to a lot of cases, things get very complicated when you have to start to mock things like dependencies, API calls, etc...
And one very tricky case is when you have to test a function that invoke some models from an ODM like Mongoose, like doing some CRUD operations against a database.
In some cases a solution could be to use an actual test database, so you don't mock anything but you use real data. The only problem with that is that is assuming that you must have a database at your disposal to run unit tests, and that's not always possible, plus you have to clean the database, and a pletora of other problems.
Another solution could be to use a database that lives only in memory and only for the duration of your tests, like the excellent mongodb-memory-server package.
But while this will work most of the times, if you deploy your code on any CI/CD you might encounter problems (and I did!).
Also a key factor of unit testing, is that you shouldn't rely on external services run them, unlike E2E tests for example.
What you should do is to mock most of the dependencies you need, as your goal is to just test the function and no deeper than that.

Solving the problem with mockingoose

Prerequisites

  • You already know how to use Jest
  • You already know hot Mongoose models works
  • You have a good knowledge of how a Node.js with a framework like Express.js application works

The models

So let's say that we have a couple of models, the classic Books and Authors, and our Books model will look something like this:

// models/books.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const BooksSchema = new Schema({
  title: {
    type: String,
    trim: true
  },
  author: {
    type: Schema.Types.ObjectId,
    ref: 'authors'
  },
  year: {
    type: String,
  }
});

module.exports = mongoose.model('books', BooksSchema);
Enter fullscreen mode Exit fullscreen mode

The service

So, often you see examples where they have a route where you have an endpoint and how that endpoint is resolved, calling the model, fetching the data and returning a response.
The problem here is that you rarely do that, as you want to abstract the logic away from the router, and for various reasons, like avoiding to have huge files, keep the code DRY, and perhaps reuse the same code in different context, not only as a resolver for an API endpoint.
I'm not going too much into details, but what I normally do is to have a router file, that list the various routes for a specific module of my APIs, each route calls a controller, and the controller calls a service. The controller is just a bridge saying "this route wants to do X, I'll ask the data to a service and then return back the response to the route.
And the core logic, like fetch the list of books will live in the service, which has to query the model, and return the data.
So my Books service will be something like this:

// services/books.js

const Books = require('../models/books');

const fetchBooks = () => Books
  .find({})
  .populate('author')
  .exec();

const fetchBook = id => Books
  .findById(id)
  .populate('author')
  .exec();

const createBook = ({title, author, year}) => {
  const book = new Books({
    title,
    author,
    year,
  });
  return book.save();
}

module.exports = {
  fetchBooks,
  fetchBook,
  createBook,
};

Enter fullscreen mode Exit fullscreen mode

Note: the code above is brutally minimal just to keep the example as lean as possible.

As you see, our service will include the Books model, and it will use it to do operations on the database ODM.

Testing the service

Install mockingoose

The first thing is to install mockingoose with npm i mockingoose -D.

Create your test

Now you want to create your test file, for example books.test.js.
Then you will need to import the mockingoose, the model and the functions you are going to test into the file:

const mockingoose = require('mockingoose');
const BooksModel = require('../models/books');
const {
  fetchBooks,
  fetchBook,
  createBook,
} = require('./books');
Enter fullscreen mode Exit fullscreen mode

Now to let the magic happen, we need to wrap our model with mockingoose, and then tell to the mocked model, what it supposed to return, for example if you want to return a list of books:

mockingoose(BooksModel).toReturn([
  {
    title: 'Book 1',
    author: {
      firstname: 'John',
      lastname: 'Doe'
    },
    year: 2021,
  },
  {
    title: 'Book 2',
    author: {
      firstname: 'Jane',
      lastname: 'Doe'
    },
    year: 2022,
  }
], 'find');
Enter fullscreen mode Exit fullscreen mode

As you can the toReturn function expects two values, the first one is the data you want the model to return, the second one is which operations, like find, findOne, update, etc... and in our case we are going to call the find one as we need to fetch the list of books.
So the complete test for fetching the book will look something like this:

// books.test.js

const mockingoose = require('mockingoose');
const BooksModel = require('../models/books');
const {
  fetchBooks,
  fetchBook,
  createBook,
} = require('./books');

describe('Books service', () => {
  describe('fetchBooks', () => {
    it ('should return the list of books', async () => {
      mockingoose(BooksModel).toReturn([
        {
          title: 'Book 1',
          author: {
            firstname: 'John',
            lastname: 'Doe'
          },
          year: 2021,
        },
        {
          title: 'Book 2',
          author: {
            firstname: 'Jane',
            lastname: 'Doe'
          },
          year: 2022,
        }
      ], 'find');
      const results = await fetchBooks();
      expect(results[0].title).toBe('Book 1');
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Similarly if you want to test the fetchBook method, which fetches only one document, it will be something like this:

describe('fetchBook', () => {
  it ('should return a book', async () => {
    mockingoose(BooksModel).toReturn(
      {
        _id: 1,
        title: 'Book 1',
        author: {
          firstname: 'John',
          lastname: 'Doe'
        },
        year: 2021,
      }, 'findOne');
    const results = await fetchBook(1);
    expect(results.title).toBe('test');
  });
});
Enter fullscreen mode Exit fullscreen mode

The nice thing of this library is that it will also support if you call chained operations like exec or populate for example, so you don't need to worry about them.

Run the tests

So now if you run your tests with npm run test, you should see your fist tests running successfully:

Screenshot 2021-09-07 at 12.07.47

Final thoughts

Testing your real world application can be challenging sometimes, especially when you get lost in mocking most of the application data, but with tools like mockingoose makes my life much easier, and it works fine on CI/CD as well!
For more details on how to use this library, please visit the github project page.

Top comments (3)

Collapse
 
pmirandaarias profile image
Paulo Miranda • Edited

In the 2nd test, you can call:
const results = await fetchBook(1123123);
And it will pass too the test, because you are defining what you want to be return in the findOnemethod, it doesn't matter which parameter you pass to fetchBook, example:

Image description

actually it will pass too with:
const results = await fetchBook();

Collapse
 
deesouza profile image
Diego Souza

Your second example will return false, because you're testing if title book is test, but in the mock is Book 1.

Am I alright?

Collapse
 
tomnyson profile image
Le Hong Son

good