Шутка шутит / шпионит за цепями Мангуста (поиск, сортировка, ограничение, пропуск)
Что я хочу сделать:
- шпионить за вызовами методов, прикованными к
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);