Шутка шутит / шпионит за цепями Мангуста (поиск, сортировка, ограничение, пропуск)

Что я хочу сделать:

  • шпионить за вызовами методов, прикованными к find() используется в определении метода статической модели
    • цепочечные методы: sort(), limit(), skip()

Образец вызова

  • цель: следить за аргументами, передаваемыми каждому из методов в определении метода статической модели:

    ... статический метод def

    const results = await this.find ({}). sort ({}). limit (). skip ();

    ... статический метод def

  • что сделал find() получить как аргументы: дополнено findSpy

  • что сделал sort() получить как аргументы: неполное
  • что сделал limit() получить как аргументы: неполное
  • что сделал skip() получить как аргументы: неполное

Что я пробовал:

  • mockingoose библиотека, но она ограничена find()
  • Я был в состоянии успешно издеваться find() сам метод, но не цепочечные вызовы, которые идут после него
    • const findSpy = jest.spyOn(models.ModelName, 'find');
  • исследования для насмешливых вызовов методов безуспешно

6 ответов

Я не смог найти решение нигде. Вот как я решил эту проблему. YMMV и если вы знаете лучший способ, пожалуйста, дайте мне знать!

Чтобы дать некоторый контекст, это является частью реализации REST API Medium.com, над которой я работаю как побочный проект.

Как я издевался над ними

  • У меня был смоделирован каждый цепочечный метод, и он был разработан так, чтобы возвращать сам макет объекта Model, чтобы он мог получить доступ к следующему методу в цепочке.
  • Последний метод в цепочке (пропустить) был предназначен для возврата результата.
  • В самих тестах я использовал Jest mockImplementation() метод для разработки его поведения для каждого теста
  • Все это можно было бы затем использовать 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(() => []),
};

Определение статического метода для тестирования

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

Полный Jest тесты, чтобы увидеть реализацию макета

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

для меня это работало так:

      AnyModel.find = jest.fn().mockImplementationOnce(() => ({
 limit: jest.fn().mockImplementationOnce(() => ({
       sort: jest.fn().mockResolvedValue(mock)
    }))
}))

Это сработало для меня:

      jest.mock("../../models", () => ({
     Action: {
         find: jest.fn(),
     },
}));

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

Все вышеперечисленное не сработало в моем случае, после некоторых проб и ошибок это сработало для меня:

const findSpy = jest.spyOn(tdataModel.find().sort({ _id: 1 }).skip(0).populate('fields'), 'limit')

ПРИМЕЧАНИЕ: вам нужно смоделировать запрос, в моем случае я использую NestJs: я сделал следующее:

       find: jest.fn().mockImplementation(() => ({
          sort: jest.fn().mockImplementation((...args) => ({
            skip: jest.fn().mockImplementation((...arg) => ({
              populate: jest.fn().mockImplementation((...arg) => ({
                limit: jest.fn().mockImplementation((...arg) => telemetryDataStub),
              })),
            })),
          })),
        })),
        findOne: jest.fn(),
        updateOne: jest.fn(),
        deleteOne: jest.fn(),
        create: jest.fn(),
        count: jest.fn().mockImplementation(() => AllTelemetryDataStub.length),

Вот как я сделал это с помощью sinonjs для звонка:

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

Это может подсказать вам, как это сделать с помощью 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);
Другие вопросы по тегам