Jest mocking / spying on Mongoose chained (find, sort, limit, skip) methods Jest mocking / spying on Mongoose chained (find, sort, limit, skip) methods mongoose mongoose

Jest mocking / spying on Mongoose chained (find, sort, limit, skip) methods


I was not able to find a solution anywhere. Here is how I ended up solving this. YMMV and if you know of a better way please let me know!

To give some context this is part of a REST implementation of the Medium.com API I am working on as a side project.

How I mocked them

  • I had each chained method mocked and designed to return the Model mock object itself so that it could access the next method in the chain.
  • The last method in the chain (skip) was designed to return the result.
  • In the tests themselves I used the Jest mockImplementation() method to design its behavior for each test
  • All of these could then be spied on using expect(StoryMock.chainedMethod).toBeCalled[With]()
const StoryMock = {  getLatestStories, // to be tested  addPagination: jest.fn(), // already tested, can mock  find: jest.fn(() => StoryMock),  sort: jest.fn(() => StoryMock),  limit: jest.fn(() => StoryMock),  skip: jest.fn(() => []),};

Static method definition to be tested

/** * Gets the latest published stories * - uses limit, currentPage pagination * - sorted by descending order of publish date * @param {object} paginationQuery pagination query string params * @param {number} paginationQuery.limit [10] pagination limit * @param {number} paginationQuery.currentPage [0] pagination current page * @returns {object} { stories, pagination } paginated output using Story.addPagination */async function getLatestStories(paginationQuery) {  const { limit = 10, currentPage = 0 } = paginationQuery;  // limit to max of 20 results per page  const limitBy = Math.min(limit, 20);  const skipBy = limitBy * currentPage;  const latestStories = await this    .find({ published: true, parent: null }) // only published stories    .sort({ publishedAt: -1 }) // publish date descending    .limit(limitBy)    .skip(skipBy);  const stories = await Promise.all(latestStories.map(story => story.toResponseShape()));  return this.addPagination({ output: { stories }, limit: limitBy, currentPage });}

Full Jest tests to see implementation of the mock

const { mocks } = require('../../../../test-utils');const { getLatestStories } = require('../story-static-queries');const StoryMock = {  getLatestStories, // to be tested  addPagination: jest.fn(), // already tested, can mock  find: jest.fn(() => StoryMock),  sort: jest.fn(() => StoryMock),  limit: jest.fn(() => StoryMock),  skip: jest.fn(() => []),};const storyInstanceMock = (options) => Object.assign(  mocks.storyMock({ ...options }),  { toResponseShape() { return this; } }, // already tested, can mock); describe('Story static query methods', () => {  describe('getLatestStories(): gets the latest published stories', () => {    const stories = Array(20).fill().map(() => storyInstanceMock({}));    describe('no query pagination params: uses default values for limit and currentPage', () => {      const defaultLimit = 10;      const defaultCurrentPage = 0;      const expectedStories = stories.slice(0, defaultLimit);      // define the return value at end of query chain      StoryMock.skip.mockImplementation(() => expectedStories);      // spy on the Story instance toResponseShape() to ensure it is called      const storyToResponseShapeSpy = jest.spyOn(stories[0], 'toResponseShape');      beforeAll(() => StoryMock.getLatestStories({}));      afterAll(() => jest.clearAllMocks());      test('calls find() for only published stories: { published: true, parent: null }', () => {        expect(StoryMock.find).toHaveBeenCalledWith({ published: true, parent: null });      });      test('calls sort() to sort in descending publishedAt order: { publishedAt: -1 }', () => {        expect(StoryMock.sort).toHaveBeenCalledWith({ publishedAt: -1 });      });      test(`calls limit() using default limit: ${defaultLimit}`, () => {        expect(StoryMock.limit).toHaveBeenCalledWith(defaultLimit);      });      test(`calls skip() using <default limit * default currentPage>: ${defaultLimit * defaultCurrentPage}`, () => {        expect(StoryMock.skip).toHaveBeenCalledWith(defaultLimit * defaultCurrentPage);      });      test('calls toResponseShape() on each Story instance found', () => {        expect(storyToResponseShapeSpy).toHaveBeenCalled();      });      test(`calls static addPagination() method with the first ${defaultLimit} stories result: { output: { stories }, limit: ${defaultLimit}, currentPage: ${defaultCurrentPage} }`, () => {        expect(StoryMock.addPagination).toHaveBeenCalledWith({          output: { stories: expectedStories },          limit: defaultLimit,          currentPage: defaultCurrentPage,        });      });    });    describe('with query pagination params', () => {      afterEach(() => jest.clearAllMocks());      test('executes the previously tested behavior using query param values: { limit: 5, currentPage: 2 }', async () => {        const limit = 5;        const currentPage = 2;        const storyToResponseShapeSpy = jest.spyOn(stories[0], 'toResponseShape');        const expectedStories = stories.slice(0, limit);        StoryMock.skip.mockImplementation(() => expectedStories);        await StoryMock.getLatestStories({ limit, currentPage });        expect(StoryMock.find).toHaveBeenCalledWith({ published: true, parent: null });        expect(StoryMock.sort).toHaveBeenCalledWith({ publishedAt: -1 });        expect(StoryMock.limit).toHaveBeenCalledWith(limit);        expect(StoryMock.skip).toHaveBeenCalledWith(limit * currentPage);        expect(storyToResponseShapeSpy).toHaveBeenCalled();        expect(StoryMock.addPagination).toHaveBeenCalledWith({          limit,          currentPage,          output: { stories: expectedStories },        });      });      test('limit value of 500 passed: enforces maximum value of 20 instead', async () => {        const limit = 500;        const maxLimit = 20;        const currentPage = 2;        StoryMock.skip.mockImplementation(() => stories.slice(0, maxLimit));        await StoryMock.getLatestStories({ limit, currentPage });        expect(StoryMock.limit).toHaveBeenCalledWith(maxLimit);        expect(StoryMock.addPagination).toHaveBeenCalledWith({          limit: maxLimit,          currentPage,          output: { stories: stories.slice(0, maxLimit) },        });      });    });  });});


Here is how I did this with sinonjs for the call:

 await MyMongooseSchema.find(q).skip(n).limit(m)

It might give you clues to do this with Jest:

sinon.stub(MyMongooseSchema, 'find').returns(    {        skip: (n) => {            return {                limit: (m) => {                    return new Promise((                        resolve, reject) => {                            resolve(searchResults);                        });                }               }        }    });sinon.stub(MyMongooseSchema, 'count').resolves(searchResults.length);


This worked for me:

jest.mock("../../models", () => ({     Action: {         find: jest.fn(),     },}));Action.find.mockReturnValueOnce({    readConcern: jest.fn().mockResolvedValueOnce([        { name: "Action Name" },    ]),});