Как говорилось ранее, метод MockitoAnnotations#initMocks
инициализирует объекты, аннотированные одной из ключевых аннотаций (@Spy
, @Mock
, @Captor
, @InjectMocks
). Сегодня мы увидим как обрабатываются аннотации. Архитектура должна быть гибкой, чтобы позволить удобно добавлять новые аннотации.
В метод передается единственный аргумент - ссылка на объект теста. Именно по полям этого объекта будет произведен поиск аннотаций. Т.к. объект MockitoAnnotations
является частью внешнего API Mockito, то любые аргументы должны быть проверены на null
, что и сделано. В случае обнаружения null
выкидывается MockitoException
.
В Mockito все исключения собраны в утилитарном объекте org.mockito.exceptions.Reporter, каждый метод которого выкидывает определенное исключение. Вообще сомнительное решение, но в данном случае (в методе initMocks этот подход проигнорирован и исключение выбрасывается напрямую).
Далее в методе инициализируется объект, реализующий интерфейс org.mockito.configuration.AnnotationEngine
. Этот объект непосредственно ищет аннотации и вызывает создание соответствующего объекта. Различные реализации AnnotationEngine отвечают за разные группы аннотаций и, соответственно, подход к обработке этих групп отличается.
Реализация AnnotationEngine
определена в org.mockito.internal.configuration.GlobalConfiguration
. Это объект, хранящий базовую конфигурацию системы. Переопределить настройки можно только создав свой собственный объект, реализующий интерфейс org.mockito.internal.configuration.IMockitoConfiguration
. Mockito подцепит его автоматически, если назвать и расположить его так, чтобы его наименование соответствовало наименованию, которое хранится в публичном поле org.mockito.internal.configuration.ClassPathLoader#MOCKITO_CONFIGURATION_CLASS_NAME
. На данный момент это “org.mockito.configuration.MockitoConfiguration”. За загрузку custom’ной конфигурации отвечает класс ClassPathLoader
, который загружает её по захардкоденному наименованию. Вот так выглядит метод GlobalConfiguration#createConfig
:
1 2 3 4 5 6 7 8 9 |
|
Таким образом, в случае отсутствия внешней конфигурации загружается конфигурация по умолчанию. Стоит отметить, что GlobalConfiguration
является singletone’ом для объектов одного потока, т.к. реализация конфигурации хранится в ThreadLocal
переменной класса GlobalConfiguration
:
1 2 3 4 5 6 7 |
|
В Mockito способ использования единого экземпляра объекта с помощью ThreadLocal переменных очень распространен и используется повсеместно. Так как тесты, в подавляющем своем большинстве, запускаются в одном потоке, то этот способ работает.
В DefaultMockitoConfiguration#getAnnotationEngine
возвращается org.mockito.internal.configuration.InjectingAnnotationEngine
- реализация AnnotationEngine
, обрабатывающая стаббирующие аннотации (@Spy
, @Mock
, @Captor
) и внедряющая их объекты в @InjectMocks
объект.
InjectingAnnotationEngine - делегат. Дело в том, что аннотации добавлялись постепенно. С появлением аннотации InjectMocks появилась необходимость работать с проинициализированными объектами. Разработчики решили использовать в такой ситуации создающие AnnotationEngine внутри инжектируемого. Таким образом, инжектирующий AnnotationEngine делегирует создающим AnnotationEngine обязанность по созданию объектов, а сам затем занимается уже внедрением этих объектов.
Таким образом, в InjectingAnnotationEngine
присутствуют два AnnotationEngine
, которым делегируется обработка старых аннотаций, а именно:
DefaultAnnotationEngine
обрабатывает аннотации@Mock
и@Captor
SpyAnnotationEngine
обрабатывает@Spy
аннотации
Сам же класс InjectingAnnotationEngine
обрабатывает аннотации @InjectMocks
.
Основной метод InjectingAnnotationEngine#process
сначала вызывает инициализацию стаббируемых объектов, делегируя эти функции другим реализациям, затем вторым шагом внедряет эти объекты в поле, аннотированное как @InjectMocks
:
1 2 3 4 5 6 7 8 9 10 11 |
|
Цикл while обходит все родительские классы класса-теста, чтобы проинициализировать и их аннотации. Затем вызываются последовательно методы DefaultAnnotationEngine#process
и SpyAnnotationEngine#process
. В них передается ссылка на класс объекта теста и на объект теста. Реализации этих методов достаточно тривиальны и сводятся к обходу полей класса, полученных с помощью метода clazz.getDeclaredFields()
и затем для каждого поля обходится список аннотаций, полученных методом field.getAnnotations()
. DefaultAnnotationEngine#process
позволяет добавлять обработчики аннотаций.
Обработчик аннотаций реализует интерфейс FieldAnnotationProcessor<A>
, где под типом параметра A подставляется тип аннотации, например Mock
. Список поддерживаемых процессоров аннотаций хранится в Map’е по типу аннотации и заполняется в конструкторе DefaultAnnotationEngine
. FieldAnnotationProcessor#process
вызывает создание соответствующего объекта, как это делается пользователями библиотеки без использования аннотаций, т.е. с помощью вызова статических методов класса Mockito
или ArgumentCaptor
. SpyAnnotationEngine#process
реализует логику процессорa аннотаций внутри себя, т.к. создан для обработки только одной аннотации - @Spy
. Непонятно почему разработчики вынесли обработку этой аннотации в отдельный AnnotationEngine, а не добавили ещё один FieldAnnotationProcessor
.
Процесс внедрения mock-объектов в аннотированное @InjectMocks
поле немного сложнее. Его мы рассмотрим отдельно.