Mockito: @InjectMocks

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

Процесс внедрения mock’ов разделен на несколько этапов:

  1. Поиск полей, отмеченных аннотацией @InjectMocks
  2. Поиск мок объектов в тесте
  3. Внедрение найденных моков в поля, помеченные аннотацией @InjectMocks
1
2
3
4
5
6
7
8
Set<Field> mockDependentFields = new HashSet<Field>();
Set<Object> mocks = newMockSafeHashSet();
        
while (clazz != Object.class) {
    new InjectMocksScanner(clazz).addTo(mockDependentFields);
    new MockScanner(testClassInstance, clazz).addPreparedMocks(mocks);
    clazz = clazz.getSuperclass();
}

В Mockito используется собственный утилитарный класс Sets, с помощью которого создаются внутренние реализации множеств. В данном случае заимпортирован один из его статичных методов newMockSafeHashSet. Метод создает реализацию под названием org.mockito.internal.util.collections.HashCodeAndEqualsSafeSet. Создана отдельная реализация Set интерфейса лишь для того, чтобы скрыть особенность реализации моков. А именно, есть некоторая проблема в стаббинге методов Object.equals и Object.hashCode, и они могут выкидывать NullPointerException. Истинную причину выясним при разборе процесса subbing’а. При работе в коллекциях эти методы интенсивно используются при поиске и добавлении элементов, поэтому мы оборачиваем мок объект в этот wrapper, который использует свои методы equals и hashCode. Оборачивание моков происходит в момент добавления элементов в множество.

Вернемся к примеру. В множество mocks у нас будут добавлены все найденные mock-объекты. Стоит заметить, что в Mockito много переменных модифицируются, будучи переданными через аргументы. Так и в данном случае коллекции наполяются именно так. Immutability здесь явно не хватает. org.mockito.internal.configuration.injection.scanner.InjectMocksScanner реализует алгоритм поиска полей, отмеченных аннотацией @InjectMocks. Основные два метода класса:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private Set<Field> scan() {
    Set<Field> mockDependentFields = new HashSet<Field>();
    Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields) {
        if (null != field.getAnnotation(InjectMocks.class)) {
            assertNoAnnotations(field, Mock.class, MockitoAnnotations.Mock.class, Captor.class);
            mockDependentFields.add(field);
        }
    }

    return mockDependentFields;
}

void assertNoAnnotations(final Field field, final Class... annotations) {
    for (Class annotation : annotations) {
        if (field.isAnnotationPresent(annotation)) {
            new Reporter().unsupportedCombinationOfAnnotations(annotation.getSimpleName(), InjectMocks.class.getSimpleName());
        }
    }
}

Именно результат метода scan и добавляет в наше множество. В цикле пробегаем по полям класса-теста, проверяем есть ли у поля аннотация @InjectMocks. Стоит отметить, что используются системные java классы полей и классов. Пока, по-моему, ничего сверхъестественного. Далее, если метод аннотирован, то проверяем не аннотирован ли он еще и стаббирующими аннотациями (@Mock, @Captor), т.к. это противоречит логике и не может быть допущено. В случае нарушения правил выбрасывается RuntimeException. Если все в порядке, то мы можем добавить поле в наше множество.

org.mockito.internal.configuration.injection.scanner.MockScanner реализуется второй этап алгоритма внедрения моков - поиск мок объектов в тесте. Основные методы класса:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private Set<Object> scan() {
    Set<Object> mocks = newMockSafeHashSet();
    for (Field field : clazz.getDeclaredFields()) {
        // mock or spies only
        FieldReader fieldReader = new FieldReader(instance, field);

        Object mockInstance = preparedMock(fieldReader.read(), field);
        if (mockInstance != null) {
            mocks.add(mockInstance);
        }
    }
    return mocks;
}

private Object preparedMock(Object instance, Field field) {
    if (isAnnotatedByMockOrSpy(field)) {
        return instance;
    } else if (isMockOrSpy(instance)) {
        mockUtil.maybeRedefineMockName(instance, field.getName());
        return instance;
    }
    return null;
}

private boolean isAnnotatedByMockOrSpy(Field field) {
    return null != field.getAnnotation(Spy.class)
            || null != field.getAnnotation(Mock.class)
            || null != field.getAnnotation(MockitoAnnotations.Mock.class);
}

private boolean isMockOrSpy(Object instance) {
    return mockUtil.isMock(instance)
            || mockUtil.isSpy(instance);
}

Класс выполнен в стиле InjectMocksScanner, хотя общего интерфейса у классов нет. Но это не страшно, т.к. выполняют они сугубо утилитарные функции. Результат метода scan помещается в множество найденных мок объектов для внедрения. За чтение значений полей класса из конкретных объектов отвечает FieldReader. Основная строчка его метода read:

1
return field.get(target)

В методе MockScanner#preparedMock определяется, является ли полученный из поля объект mock’ом. Определяется он по наличию в поле аннотаций @Mock или @Spy. Если аннотаций нет, то есть вероятность, что объекты были стаббированы вручную. Это проверяется в методе MockScanner#isMockOrSpy. В случае, если объект все-таки является Mock объектом, то возможно требуется его переименование. Необходимость такого решения возможно выяснится позднее, когда мы разберем процесс создания Mock объектов. На данный момент это выглядит каким то костылем.

Осталось дело за малым - внедрить мок-объекты. Отправная точка org.mockito.internal.configuration#DefaultInjectionEngine - объект, который инициализирует обработчиков-внедренцев. И запускает процесс инжектирования. Внедрением занимаются org.mockito.internal.configuration.injection.MockInjectionStrategy объекты. На данный момент моки инжектируются через конструктор (ConstructorInjection), поля класса и сеттеры (PropertyAndSetterInjection).

Поле, помеченное аннотацией @InjectMocks, может быть помечено ещё и аннотацией @Spy, что заставляет, в случае неуспешного внедрения моков в объект, создавать из него Spy-объект. Хотя в целом JavaDoc полный, этот факт скрыт от глаз пользователей.

Для обработки @Spy аннотации, если не получилось создать объект, в который внедрялись моки, используется SpyOnInjectedFieldsHandler. Основной метод DefaultInjectionEngine - #injectMocksOnFields:

1
2
3
4
5
6
7
8
public void injectMocksOnFields(Set<Field> needingInjection, Set<Object> mocks, Object testClassInstance) {
    MockInjection.onFields(needingInjection, testClassInstance)
            .withMocks(mocks)
            .tryConstructorInjection()
            .tryPropertyOrFieldInjection()
            .handleSpyAnnotation()
            .apply();
}

Как видно, объект-конфигуратор процесса внедрения - org.mockito.internal.configuration.injection.MockInjection. Он определяет внутри себя реализации той или иной стратегии. Поэтому его методы tryConstructorInjection, tryPropertyOrFieldInjection и handleSpyAnnotation возвращают именно те классы стратегий, которые мы рассмотрели выше. Метод onFields возвращает объект OngoingMockInjection - внутренний класс класса MockInjection. Его тип нас не должен интересовать, т.к. проектировался этот объект для использования здесь и сейчас в виде Builder’а, что обеспечивается возвращением из каждого его метода ссылки на самого себя. Этот объект-builder сохраняет в конструкторе поля с аннотацией @InjectMocks. Далее метод withMocks сохраняет список mock объектов. Странный подход к инициализации базовых данных. Практичнее было бы внести сохранение списка mock объектов в конструктор, т.к. инжектирование без них смысла не имеет. А так как порядок выполнения Builder методов не регулируется, то можно пропустить этот метод. Далее 3’мя следующими методами объявляем стратегии инжектирования, которые хотим использовать. Стратегии инжектирования образуют цепочку стратегий по примеру паттерна Composite. Вызывая основной метод первого из них, он обрабатывает основную логику, затем вызывается аналогичный метод связанного с ним объекта-внедренца. И так до последнего. На практике это реализуется следующим образом:

1
private MockInjectionStrategy injectionStrategies = MockInjectionStrategy.nop();

В OngoingMockInjection создается поле с пустой стратегией обработчиком, где nop() - фабричный метод возвращающий пустую реализацию:

1
2
3
4
5
6
7
public static final MockInjectionStrategy nop() {
    return new MockInjectionStrategy() {
        protected boolean processInjection(Field field, Object fieldOwner, Set<Object> mockCandidates) {
            return false;
        }
    };
}

processInjection - основной абстрактный метод MockInjectionStrategy, в котором определяется логика внедрения моков. Методы добавления следующего в цепочке обработчика выглядят следующим образом:

1
2
3
4
5
6
7
8
9
public OngoingMockInjection tryConstructorInjection() {
    injectionStrategies.thenTry(new ConstructorInjection());
    return this;
}

public OngoingMockInjection tryPropertyOrFieldInjection() {
    injectionStrategies.thenTry(new PropertyAndSetterInjection());
    return this;
}

Где метод thenTry передает добавляемый объект следующему в цепочке объекту, пока не будет достигнут последний объект в цепочке. Он то и примет к себе новое звено:

1
2
3
4
5
6
7
8
public MockInjectionStrategy thenTry(MockInjectionStrategy strategy) {
    if(nextStrategy != null) {
        nextStrategy.thenTry(strategy);
    } else {
        nextStrategy = strategy;
    }
    return strategy;
}

Метод OngoingMockInjection#apply, который завершает цепочку методов Builder’а, выполняет метод injectionStrategies.process, первого из цепочки стратегий обработки объекта для каждого поля, аннотированного @InjectMocks:

1
2
3
4
5
6
public boolean process(Field onField, Object fieldOwnedBy, Set<Object> mockCandidates) {
    if(processInjection(onField, fieldOwnedBy, mockCandidates)) {
        return true;
    }
    return relayProcessToNextStrategy(onField, fieldOwnedBy, mockCandidates);
}

Где осуществляется вызов метода с логикой обработки, а затем в методе relayProcessToNextStrategy передается управление следующему объекту в цепи.

Метод OngoingMockInjection#handleSpyAnnotation прикрепляет финальный обработчик @Spy аннотаций в другой MockInjectionStrategy#nop(), который также вызывается для каждого поля в методе apply после вызова цепочки стратегий внедрения.

Рассмотрим алгоритм внедрения через конструктор:

ConstructorInjection определяет внутри себя реализацию ConstructorArgumentResolver’а, где определяется алгоритм получения mock’ов из найденного заранее множества, которые могут быть приравнены к типам аргументов конструктора.

1
2
3
4
5
6
7
public Object[] resolveTypeInstances(Class<?>... argTypes) {
    List<Object> argumentInstances = new ArrayList<Object>(argTypes.length);
    for (Class<?> argType : argTypes) {
        argumentInstances.add(objectThatIsAssignableFrom(argType));
    }
    return argumentInstances.toArray();
}

Где метод objectThatIsAssignableFrom перебирает множество mock объектов и ищет первое из них, которое удовлетворяет условию:

1
argType.isAssignableFrom(mock.getClass())

Выбор конструктора, определение его параметров и создание нового экземпляра класса для поля, аннотированного @InjectMocks, осуществляется в классе org.mockito.internal.util.reflection.FieldInitializer. Внутри определены две реализации ConstructorInstantiator для обработки - конструктор с параметрами и без параметров. В ConstructorInjection используется ParameterizedConstructorInstantiator, так как внедрение mock объектов в конструктор без параметров смысла не имеет. Соответственно, первое что делается - это проверка - не создан ли объект в целевом поле.

1
2
3
4
5
6
7
8
private FieldInitializationReport acquireFieldInstance() throws IllegalAccessException {
    Object fieldInstance = field.get(fieldOwner);
    if(fieldInstance != null) {
        return new FieldInitializationReport(fieldInstance, false, false);
    }

    return instantiator.instantiate();
}

Значение объекта получается уже знакомым способом через метод get. Если объект уже в поле создан, то нет смысла идти дальше - возвращаем результат о неуспешном создании объекта. FieldInitializationReport - класс с данными результатов создания класса. instantiator - это и есть один из экземпляров ConstructorInstantiator. По методу instantiate выполняется основная логика поиска соответствующего конструктора и создания самого объекта. Основное содержание метода:

1
2
3
4
5
6
7
8
constructor = biggestConstructor(field.getType());
changer.enableAccess(constructor);

final Object[] args = argResolver.resolveTypeInstances(constructor.getParameterTypes());
Object newFieldInstance = constructor.newInstance(args);
new FieldSetter(testClass, field).set(newFieldInstance);

return new FieldInitializationReport(field.get(testClass), false, true);

С помощью метода biggestConstructor (кстати, неудачное название, т.к. не отражает действия) определяется конструктор с наибольшим количеством параметров. Реализация тривиальна.

С помощью метода changer.enableAccess изменяем доступность конструктора:

1
constructor.setAccessible(true);

При этом сохраняем старое значение accesible, чтобы вернуть его в конце метода в блоке finally:

1
2
3
4
5
finally {
    if(constructor != null) {
        changer.safelyDisableAccess(constructor);
    }
}

argResolver - реализация ConstructorArgumentResolver, которая определяется внутри ConstructorInjection (о нем мы говорили выше). С помощью него мы получаем массив mock объектов в нужном нам порядке и создаем с помощью них новый экземпляр класса целевого поля. FieldSetter - противоположность FieldReader'а. Вставляет значение в поле, при этом также меняя видимость поля. В конце возвращается FieldInitializationReport с отчетом об успешном создании объекта. В дальнейшем из этого объекта получится флаг fieldWasInitializedUsingContructorArgs,и если он равен true, то вызов следующих обработчиков в цепочке прекращается.

Остальные обработчики работают по такому же принципу. Так что нет особого смысла их детально рассматривать.

На этом рассмотрение аннотаций заканчивается. В комментариях отвечу на любые вопросы.

Comments