【问题标题】:Jest mocking / spying on Mongoose chained (find, sort, limit, skip) methods嘲笑/监视 Mongoose 链式(查找、排序、限制、跳过)方法
【发布时间】:2019-06-30 20:58:33
【问题描述】:

我想要做什么:

  • 监视链接到静态模型方法定义中使用的find() 的方法调用
    • 链式方法:sort()limit()skip()

示例调用

  • 目标:监视传递给静态模型方法定义中每个方法的参数:

    ...静态方法定义

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

    ...静态方法定义

  • find() 作为 args 收到了什么:用 findSpy 完成

  • sort() 收到了什么 args:不完整
  • limit() 作为 args 收到了什么:不完整
  • skip() 收到了什么 args:不完整

我尝试过的:

  • mockingoose 库,但仅限于 find()
  • 我已经能够成功地模拟 find() 方法本身,但不能模拟它之后的链式调用
    • const findSpy = jest.spyOn(models.ModelName, 'find');
  • 研究模拟链式方法调用没有成功

【问题讨论】:

    标签: unit-testing mongoose mocking jestjs chained


    【解决方案1】:

    我无法在任何地方找到解决方案。这就是我最终解决这个问题的方法。 YMMV,如果您知道更好的方法,请告诉我!

    为了提供一些背景信息,这是REST implementation of the Medium.com API 的一部分,我正在作为一个副项目工作。

    我是如何嘲笑他们的

    • 我对每个链接的方法进行了模拟,并将其设计为返回模型模拟对象本身,以便它可以访问链中的下一个方法。
    • 链中的最后一个方法(跳过)旨在返回结果。
    • 在测试本身中,我使用 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) },
            });
          });
        });
      });
    });
    

    【讨论】:

      【解决方案2】:

      这是我在调用中使用 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);
      

      【讨论】:

        【解决方案3】:

        这对我有用:

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

        【讨论】:

          【解决方案4】:

          jest.spyOn(Post, "find").mockImplementationOnce(() => ({
              sort: () => ({
                  limit: () => [{
                      id: '613712f7b7025984b080cea9',
                      text: 'Sample text'
                  }],
              }),
          }));

          【讨论】:

            猜你喜欢
            • 2019-11-14
            • 2020-04-23
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-09-29
            • 2019-07-21
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多