Mockito: Обработка методов

| Комментарии

Вместе с mock’ом создается обработчик его событий. Это один обработчик на все методы мока. В итоге, мы имеем объект класса MockHandlerImpl, который обернут wrapper’ами.

В момент вызова метода mock объекта вызывается MockHandlerImpl#handle. Но этот метод вызывается не напрямую а из объекта MethodInterceptor. Именно с этими обработчиками работает созданный CGLib библиотекой proxy объект. В Mockito есть собственная реализация MethodInterceptor - MethodInterceptorFilter, которая хранит внутри себя обработчик MockHandlerImpl и настройки вызова. Итак, что же происходит в момент вызова метода mock объекта? Вызывается метод MethodInterceptorFilter#intercept, куда передается ссылка на объект, в котором был вызван метод, ссылка на объект вызванного метода, массив переданных аргументов и объект MethodProxy, который позволяет получить ссылку на реальный метод mock объекта. MethodInterceptorFilter отделяет реализацию получения необходимых параметров вызова от самого обработчика. Далее все эти параметры помещаются в независимый от механизма создания proxy класс Invocation. Этот объект передается методу обработчика MockHandlerImpl#handle, который выполняет основную логику стаббированных методов.

Стоит понимать, что в Mockito есть как минимум 3 ситуации, когда вызов метода одного и того же mock объекта должен интерпретироваться по разному:

  1. в конструкции when(mock.method(matcher))
  2. в конструкции verify(mock).method(matcher)
  3. прямой вызов mock.method(args...) в тестируемом объекте

В обработчике(метод handle) обрабатываются все эти ситуации. В одной из статей мы уже рассматривали объекты ответов - Answer, которые определяются в конструкции when. В конструкции verify хранятся условия проверки. Плюс ко всему, есть matcher’ы, которые тоже накапливаются. Есть и другие параметры, которые должны быть переданы в обработчик. Странное решение, размывающее логику обработчика - это хранение этих артефактов в singleton объектах, доступ к которым имеют все компоненты системы одновременно. Основным таким объектом является MockingProgress. Этот объект мы уже рассматривали со стороны оповещения слушателей о событии создания mock’а. Он хранит в себе настройки verify блока и список matcher’ов. Плюс ко всему, т.к. эти объекты по большому счету нужны только в момент обработки stub’а, то нужно следить чтобы хранимые объекты не попали случайно в другой stub, поэтому Mockito хранит текущее состояние процесса стаббинга. Состояние определяется по наличию тех или иных объектов в реализации MockingProgress. К примеру, если объект VerificationMode не null, то объект в состоянии проверки. После извлечения хранимых объектов в них проставляется null, и объект готов к следующим процессам. Под процессом над стаббированным методом понимается один из трех процессов перечисленных в списке вызова обработчика метода mock’а выше в статье.

В каждом MockHandler’е создается InvocationContainer, который хранит в себе все ответы. В этот же контейнер переносятся все matcher’ы при их обработке. Этот же объект хранит информацию, которая проверяется в VerificationMode. Если задан VerificationMode, то выполняется проверка. Иначе вызывается метод инкрементирующий статистические данные о вызове методов.

Стоит заметить, что matcher’ы - объекты, которые позволяют задавать аргументы стаббируемых методов в виде выражений, реализуются на базе библиотеки Hamcrest. Насколько я знаю, многие тестирующие framework’и используют эту библиотеку. Mockito лишь расширяет её своими matcher’ами.

Далее вызывается поиск Answer объекта по объекту Invocation:

1
StubbedInvocationMatcher stubbedInvocation = invocationContainerImpl.findAnswerFor(invocation);

Где ищется соответствующий объект ответа для данного мока, для данного метода с аргумента, matcher которого соответствует переданным аргументам. Напомню, что вся эта информация хранится в объекте invocation с одной стороны и в объекте ответа(точнее в его обертке, которая используется во внутренней реализации) с другой.

Если ответ найден, то он вызывается с передачей ему объекта invocation. Иначе получаем ответ по-умолчанию и вызываем его:

1
2
3
4
5
6
7
8
if (stubbedInvocation != null) {
    stubbedInvocation.captureArgumentsFrom(invocation);
    return stubbedInvocation.answer(invocation);
} else {
    Object ret = mockSettings.getDefaultAnswer().answer(invocation);
    invocationContainerImpl.resetInvocationForPotentialStubbing(invocationMatcher);
    return ret;
}

Объект, возвращаемый ответом, и есть результат метода mock’а. Он и возвращается из функции.

Это последняя статья цикла Mockito. Мы увидели и хардкод и дублирование и, возможно, неудачные архитектурные решения. Чего стоит абстрагирование создания mock объектов, где базовые общие классы тесно переплетены с логикой библиотки CGLib. Замена CGLib альтернативами не будет легким занятием. Но вместе с тем Mockito является типичным примером тестирующего framework’а, который позволяет понять принципы работы подобных библиотек.

Если остались вопросы, то не стесняйтесь их задавать в комментариях, twitter’e/Google Plus’e или по почте.

Comments