org.silverpeas.core.test.extention.SilverTestEnv.java Source code

Java tutorial

Introduction

Here is the source code for org.silverpeas.core.test.extention.SilverTestEnv.java

Source

/*
 * Copyright (C) 2000 - 2018 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception.  You should have received a copy of the text describing
 * the FLOSS exception, and it is also available here:
 * "https://www.silverpeas.org/legal/floss_exception.html"
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.silverpeas.core.test.extention;

import org.apache.commons.lang3.reflect.FieldUtils;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
import org.mockito.internal.util.MockUtil;
import org.silverpeas.core.SilverpeasRuntimeException;
import org.silverpeas.core.admin.user.model.User;
import org.silverpeas.core.admin.user.service.GroupProvider;
import org.silverpeas.core.admin.user.service.UserProvider;
import org.silverpeas.core.silvertrace.SilverpeasTrace;
import org.silverpeas.core.test.TestBeanContainer;
import org.silverpeas.core.test.util.MavenTestEnv;
import org.silverpeas.core.test.util.lang.TestSystemWrapper;
import org.silverpeas.core.test.util.log.TestSilverpeasTrace;
import org.silverpeas.core.thread.ManagedThreadPool;
import org.silverpeas.core.util.lang.SystemWrapper;
import org.silverpeas.core.util.logging.LoggerConfigurationManager;
import org.silverpeas.core.util.logging.SilverLoggerProvider;

import javax.annotation.PostConstruct;
import javax.enterprise.concurrent.ManagedThreadFactory;
import javax.enterprise.inject.AmbiguousResolutionException;
import javax.inject.Inject;
import javax.inject.Qualifier;
import javax.inject.Singleton;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.mockito.Mockito.*;

/**
 * Prepares the environment specific to Silverpeas to run unit tests.
 * <p>Firstly, it mocks the container of beans and set ups it for the tests with some of the
 * common beans in Silverpeas: {@link UserProvider}, {@link GroupProvider}, {@link SystemWrapper},
 * {@link ManagedThreadPool}, {@link org.silverpeas.core.silvertrace.SilverTrace}, and the logging
 * system.
 * </p>
 * <p>
 * Secondly it scans for fields and parameters annotated with {@link TestManagedBean} and
 * {@link TestManagedMock} to register them automatically into the bean container used in tests.
 * If the class of a {@link TestManagedBean} annotated field is qualified by a {@link Qualifier}
 * annotated annotation, then it is registered under that qualifier also. For any parameter
 * annotated with {@link TestManagedMock}, it is first resolved by looking for an already registered
 * mock in the bean container (otherwise it is mocked and registered as for fields).
 * </p>
 * <p>
 * Thirdly it scans for fields annotated with {@link TestedBean} to scan it for injection point in
 * order to resolve those dependencies either by settings them with a bean already registered into
 * the bean container or by mocking it.
 * </p>
 * <p>
 *  Finally it looks for a method in the test class that is annotated with {@link RequesterProvider}
 *  to execute it and to set the returned {@link User} instance as the default requester to use
 *  in all the tests of the class.
 * </p>
 * @author mmoquillon
 */
public class SilverTestEnv implements TestInstancePostProcessor, ParameterResolver, BeforeEachCallback {

    /**
     * Injects in the unit test class all the fields that are annotated with one of the supported
     * annotations by {@link SilverTestEnv} extension ({@link TestManagedMock}, {@link TestManagedBean},
     * {@link TestedBean}, ...). Each of such annotated beans will be either mocked or instantiated
     * with their default constructor and then registered into the bean container used for the unit
     * tests.
     * <p>
     * <strong>Be caution:</strong> any {@link TestedBean} annotated fields should be declared
     * lastly for their dependencies to have a change to be set with any previous declared
     * {@link TestManagedMock} and {@link TestManagedBean} annotated field values.
     * </p>
     * @param testInstance the instance of the test class.
     * @param context the context of the extension.
     * @throws Exception if an error occurs while injecting the fields.
     */
    @Override
    public void postProcessTestInstance(final Object testInstance, final ExtensionContext context)
            throws Exception {
        reset(TestBeanContainer.getMockedBeanContainer());
        mockCommonBeans(testInstance);
        TestManagedBeans testManagedBeans = testInstance.getClass().getAnnotation(TestManagedBeans.class);
        if (testManagedBeans != null) {
            for (Class<?> type : testManagedBeans.value()) {
                Object bean = instantiate(type);
                Objects.requireNonNull(bean);
                mockInjectedDependency(bean);
                invokePostConstruction(bean);
                manageBean(bean, type);
            }
        }
        TestManagedMocks testManagedMocks = testInstance.getClass().getAnnotation(TestManagedMocks.class);
        if (testManagedMocks != null) {
            for (Class<?> type : testManagedMocks.value()) {
                Object mock = mock(type);
                registerInBeanContainer(mock);
            }
        }
        loopInheritance(testInstance.getClass(), type -> {
            Field[] fields = type.getDeclaredFields();
            for (Field field : fields) {
                processTestManagedBeanAnnotation(field, testInstance);
                processMockedBeanAnnotation(field, testInstance);
                processTestedBeanAnnotation(field, testInstance);
            }
        });
    }

    /**
     * Is the parameter in a test's method is supported by this extension for value injection?
     * @param parameterContext the context of the parameter.
     * @param extensionContext the context of the extension.
     * @return true if the parameter is either annotated with @{@link TestManagedBean} or with
     * {@link TestManagedMock}
     */
    @Override
    public boolean supportsParameter(final ParameterContext parameterContext,
            final ExtensionContext extensionContext) {
        return parameterContext.isAnnotated(TestManagedMock.class)
                || parameterContext.isAnnotated(TestManagedBean.class)
                || parameterContext.getParameter().getType().equals(MavenTestEnv.class);
    }

    /**
     * Resolves the parameter referred by the parameter context by valuing it according to its
     * annotation: if annotated with {@link TestManagedBean}, the parameter will be instantiated with
     * its default constructor; if annotated with {@link TestManagedMock}, the parameter will be mocked.
     * @param parameterContext the context of the parameter.
     * @param extensionContext the context of the extension.
     * @return the value of the parameter to inject.
     */
    @Override
    public Object resolveParameter(final ParameterContext parameterContext,
            final ExtensionContext extensionContext) {
        Object bean;
        final Parameter parameter = parameterContext.getParameter();
        if (parameter.getType().equals(MavenTestEnv.class)) {
            bean = new MavenTestEnv(extensionContext.getRequiredTestInstance());
        } else if (parameterContext.isAnnotated(TestManagedMock.class)) {
            bean = TestBeanContainer.getMockedBeanContainer().getBeanByType(parameter.getType());
            if (bean == null) {
                bean = mock(parameter.getType());
                registerInBeanContainer(bean);
            }
        } else if (parameterContext.isAnnotated(TestManagedBean.class)) {
            bean = TestBeanContainer.getMockedBeanContainer().getBeanByType(parameter.getType());
            if (bean == null) {
                bean = instantiate(parameter.getType());
                manageBean(bean, parameter.getType());
            }
        } else {
            bean = null;
        }
        return bean;
    }

    /**
     * Prepares the unit test environment before executing any test. Some beans are mocked by default
     * ({@link GroupProvider}, {@link UserProvider}, {@link ManagedThreadFactory}, and so on.)
     * If the unit test defines a method annotated with {@link RequesterProvider}, then it is
     * invoked to get the user to set as the default requester.
     * @param context the context of the extension.
     * @throws Exception if an error occurs while preparing the test environement.
     */
    @Override
    public void beforeEach(final ExtensionContext context) throws Exception {
        Method[] methods = context.getRequiredTestClass().getDeclaredMethods();
        for (Method method : methods) {
            RequesterProvider requesterProvider = method.getAnnotation(RequesterProvider.class);
            if (requesterProvider != null && User.class.isAssignableFrom(method.getReturnType())) {
                method.setAccessible(true);
                User requester = (User) method.invoke(context.getRequiredTestInstance());
                UserProvider mock = TestBeanContainer.getMockedBeanContainer().getBeanByType(UserProvider.class);
                when(mock.getCurrentRequester()).thenReturn(requester);
                break;
            }
        }
    }

    private void processTestManagedBeanAnnotation(final Field field, final Object testInstance)
            throws IllegalAccessException {
        if (field.isAnnotationPresent(TestManagedBean.class)) {
            final Object bean = setupInstanceField(field, testInstance);
            manageBean(bean, field.getType());
        }
    }

    private void processMockedBeanAnnotation(final Field field, final Object testInstance)
            throws IllegalAccessException {
        if (field.isAnnotationPresent(TestManagedMock.class)) {
            field.setAccessible(true);
            Object bean = mock(field.getType());
            field.setAccessible(true);
            field.set(testInstance, bean);
            registerInBeanContainer(bean);
        }
    }

    private void processTestedBeanAnnotation(final Field field, final Object testInstance)
            throws IllegalAccessException {
        if (field.isAnnotationPresent(TestedBean.class)) {
            final Object bean = setupInstanceField(field, testInstance);
            registerInBeanContainer(bean);
        }
    }

    private Object setupInstanceField(final Field field, final Object testInstance) throws IllegalAccessException {
        field.setAccessible(true);
        Object bean = field.get(testInstance);
        if (bean == null) {
            bean = instantiate(field.getType());
            field.set(testInstance, bean);
        }
        Objects.requireNonNull(bean);
        mockInjectedDependency(bean);
        invokePostConstruction(bean);
        return bean;
    }

    private void mockInjectedDependency(final Object bean) throws IllegalAccessException {
        loopInheritance(bean.getClass(), typeToLookup -> {
            Field[] beanFields = typeToLookup.getDeclaredFields();
            for (Field dependency : beanFields) {
                if (dependency.isAnnotationPresent(Inject.class)) {
                    Object mock = TestBeanContainer.getMockedBeanContainer().getBeanByType(dependency.getType());
                    if (mock == null) {
                        mock = mock(dependency.getType());
                    }
                    dependency.setAccessible(true);
                    dependency.set(bean, mock);
                }
            }
        });
    }

    private <T> T instantiate(final Class<? extends T> beanType) {
        T bean;
        try {
            Constructor<? extends T> constructor = beanType.getDeclaredConstructor();
            constructor.setAccessible(true);
            bean = constructor.newInstance();
        } catch (NoSuchMethodException | InvocationTargetException | InstantiationException
                | IllegalAccessException e) {
            bean = null;
        }
        return bean;
    }

    private void invokePostConstruction(final Object bean) {
        try {
            Method[] methods = bean.getClass().getDeclaredMethods();
            for (Method method : methods) {
                if (method.isAnnotationPresent(PostConstruct.class)) {
                    method.setAccessible(true);
                    method.invoke(bean);
                    break;
                }
            }
        } catch (InvocationTargetException | IllegalAccessException e) {
            throw new SilverpeasRuntimeException(e);
        }
    }

    private void manageBean(final Object bean, final Class<?> beanType) {
        Annotation[] qualifiers = Stream.of(beanType.getDeclaredAnnotations())
                .filter(a -> a.annotationType().getAnnotationsByType(Qualifier.class).length > 0)
                .toArray(Annotation[]::new);
        registerInBeanContainer(bean, qualifiers);
    }

    private void mockCommonBeans(final Object testInstance) {
        mockUserProvider();
        mockGroupProvider();
        mockSystemWrapper(testInstance);
        mockLoggingSystem();
        mockManagedThreadFactory();
    }

    private void mockUserProvider() {
        UserProvider userProvider = mock(UserProvider.class);
        doCallRealMethod().when(userProvider).getCurrentRequester();
        when(TestBeanContainer.getMockedBeanContainer().getBeanByType(UserProvider.class)).thenReturn(userProvider);
    }

    private void mockGroupProvider() {
        GroupProvider groupProvider = mock(GroupProvider.class);
        when(TestBeanContainer.getMockedBeanContainer().getBeanByType(GroupProvider.class))
                .thenReturn(groupProvider);
    }

    private void mockSystemWrapper(final Object testInstance) {
        TestSystemWrapper testSystemWrapper = new TestSystemWrapper();
        testSystemWrapper.initFor(testInstance);
        when(TestBeanContainer.getMockedBeanContainer().getBeanByType(SystemWrapper.class))
                .thenReturn(testSystemWrapper);
    }

    private void mockLoggingSystem() {
        when(TestBeanContainer.getMockedBeanContainer().getBeanByType(SilverpeasTrace.class))
                .thenReturn(new TestSilverpeasTrace());

        StubbedLoggerConfigurationManager configurationManager = new StubbedLoggerConfigurationManager();
        when(TestBeanContainer.getMockedBeanContainer().getBeanByType(LoggerConfigurationManager.class))
                .thenReturn(configurationManager);

        StubbedSilverLoggerProvider loggerProvider = new StubbedSilverLoggerProvider(configurationManager);
        when(TestBeanContainer.getMockedBeanContainer().getBeanByType(SilverLoggerProvider.class))
                .thenReturn(loggerProvider);
    }

    private void mockManagedThreadFactory() {
        try {
            Constructor<ManagedThreadPool> managedThreadPoolConstructor = ManagedThreadPool.class
                    .getDeclaredConstructor();
            managedThreadPoolConstructor.setAccessible(true);
            ManagedThreadPool managedThreadPool = managedThreadPoolConstructor.newInstance();
            ManagedThreadFactory managedThreadFactory = Thread::new;
            FieldUtils.writeField(managedThreadPool, "managedThreadFactory", managedThreadFactory, true);
            when(TestBeanContainer.getMockedBeanContainer().getBeanByType(ManagedThreadPool.class))
                    .thenReturn(managedThreadPool);
        } catch (IllegalAccessException | NoSuchMethodException | InstantiationException
                | InvocationTargetException e) {
            throw new SilverpeasRuntimeException(e);
        }
    }

    @SuppressWarnings({ "unchecked" })
    private <T> T registerInBeanContainer(T bean, Annotation... qualifiers) {
        final Class<T> clazz;
        if (MockUtil.isMock(bean) || MockUtil.isSpy(bean)) {
            clazz = MockUtil.getMockHandler(bean).getMockSettings().getTypeToMock();
        } else {
            clazz = (Class<T>) bean.getClass();
        }
        Class<?>[] types = Arrays.copyOf(clazz.getInterfaces(), clazz.getInterfaces().length + 1);
        types[types.length - 1] = clazz;

        for (Class type : types) {
            putInTestBeanContainer(bean, type, qualifiers);
            if (qualifiers.length > 1) {
                Stream.of(qualifiers).forEach(q -> putInTestBeanContainer(bean, type, q));
            }
        }
        return bean;
    }

    @SuppressWarnings({ "unchecked" })
    private <T> void putInTestBeanContainer(final T bean, final Class type, Annotation... qualifiers) {
        Set existing = TestBeanContainer.getMockedBeanContainer().getAllBeansByType(type, qualifiers);
        if (!existing.isEmpty()) {
            if (!type.isAnnotationPresent(Singleton.class)) {
                final HashSet all = new HashSet(existing);
                all.add(bean);
                when(TestBeanContainer.getMockedBeanContainer().getAllBeansByType(type, qualifiers))
                        .thenReturn(all);
                when(TestBeanContainer.getMockedBeanContainer().getBeanByType(type, qualifiers))
                        .thenThrow(new AmbiguousResolutionException("A bean of type " + type + " already exist!"));
            }
        } else {
            when(TestBeanContainer.getMockedBeanContainer().getBeanByType(type, qualifiers)).thenReturn(bean);
            when(TestBeanContainer.getMockedBeanContainer().getAllBeansByType(type, qualifiers))
                    .thenReturn(Stream.of(bean).collect(Collectors.toSet()));
        }
    }

    private void loopInheritance(final Class<?> fromType, final TypeConsumer consumer)
            throws IllegalAccessException {
        Class<?> type = fromType;
        while (type != null && !type.isInterface() && !type.equals(Object.class)) {
            consumer.consume(type);
            type = type.getSuperclass();
        }
    }

    @FunctionalInterface
    private interface TypeConsumer {
        void consume(final Class<?> type) throws IllegalAccessException;
    }

    private class StubbedLoggerConfigurationManager extends LoggerConfigurationManager {
        StubbedLoggerConfigurationManager() {
            super();
            loadAllConfigurationFiles();
        }
    }

    private class StubbedSilverLoggerProvider extends SilverLoggerProvider {

        StubbedSilverLoggerProvider(final LoggerConfigurationManager loggerConfigurationManager) {
            super(loggerConfigurationManager);
        }
    }
}