Mockito: Обработка аннотаций

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

Как говорилось ранее, метод 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
private IMockitoConfiguration createConfig() {
    IMockitoConfiguration defaultConfiguration = new DefaultMockitoConfiguration();
    IMockitoConfiguration config = new ClassPathLoader().loadConfiguration();
    if (config != null) {
        return config;
    } else {
        return defaultConfiguration;
    }
}

Таким образом, в случае отсутствия внешней конфигурации загружается конфигурация по умолчанию. Стоит отметить, что GlobalConfiguration является singletone’ом для объектов одного потока, т.к. реализация конфигурации хранится в ThreadLocal переменной класса GlobalConfiguration:

1
2
3
4
5
6
7
private static ThreadLocal<IMockitoConfiguration> globalConfiguration = new ThreadLocal<IMockitoConfiguration>();

public GlobalConfiguration() {
    if (globalConfiguration.get() == null) {
        globalConfiguration.set(createConfig());
    }
}
В Mockito способ использования единого экземпляра объекта с помощью ThreadLocal переменных очень распространен и используется повсеместно. Так как тесты, в подавляющем своем большинстве, запускаются в одном потоке, то этот способ работает.

В DefaultMockitoConfiguration#getAnnotationEngine возвращается org.mockito.internal.configuration.InjectingAnnotationEngine - реализация AnnotationEngine, обрабатывающая стаббирующие аннотации (@Spy, @Mock, @Captor) и внедряющая их объекты в @InjectMocks объект.

InjectingAnnotationEngine - делегат. Дело в том, что аннотации добавлялись постепенно. С появлением аннотации InjectMocks появилась необходимость работать с проинициализированными объектами. Разработчики решили использовать в такой ситуации создающие AnnotationEngine внутри инжектируемого. Таким образом, инжектирующий AnnotationEngine делегирует создающим AnnotationEngine обязанность по созданию объектов, а сам затем занимается уже внедрением этих объектов.

Таким образом, в InjectingAnnotationEngine присутствуют два AnnotationEngine, которым делегируется обработка старых аннотаций, а именно:

  1. DefaultAnnotationEngine обрабатывает аннотации @Mock и @Captor
  2. SpyAnnotationEngine обрабатывает @Spy аннотации

Сам же класс InjectingAnnotationEngine обрабатывает аннотации @InjectMocks.

Основной метод InjectingAnnotationEngine#process сначала вызывает инициализацию стаббируемых объектов, делегируя эти функции другим реализациям, затем вторым шагом внедряет эти объекты в поле, аннотированное как @InjectMocks:

1
2
3
4
5
6
7
8
9
10
11
private void processIndependentAnnotations(final Class<?> clazz, final Object testInstance) {
    Class<?> classContext = clazz;
    while (classContext != Object.class) {
        //this will create @Mocks, @Captors, etc:
        delegate.process(classContext, testInstance);
        //this will create @Spies:
        spyAnnotationEngine.process(classContext, testInstance);

        classContext = classContext.getSuperclass();
    }
}

Цикл 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 поле немного сложнее. Его мы рассмотрим отдельно.

Comments