Package org.jukito

Source Code of org.jukito.JukitoRunner

/**
* Copyright 2013 ArcBees Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package org.jukito;

import com.google.inject.Binding;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Scope;
import com.google.inject.TypeLiteral;
import com.google.inject.internal.Errors;
import com.google.inject.spi.DefaultBindingScopingVisitor;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.mockito.internal.runners.util.FrameworkUsageValidator;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
* TODO: Rework this documentation
* <p/>
* This class implements the mockito runner but allows Guice dependency
* injection. To setup the guice environment, the test class can have an inner
* static class deriving from {@link TestModule}. This last class will let you bind
* {@link TestSingleton} and {@link TestEagerSingleton} and the runner will make sure these
* singletons are reset at every invocation of a test case.
* <p/>
* This code not very clean as it is cut & paste from
* {@link org.mockito.internal.runners.JUnit45AndHigherRunnerImpl}, but it's
* unclear how we could make otherwise.
* <p/>
* Most of the code here is inspired from: <a href=
* "http://cowwoc.blogspot.com/2008/10/integrating-google-guice-into-junit4.html"
* > http://cowwoc.blogspot.com/2008/10/integrating-google-guice-into-junit4.
* html</a>
* <p/>
* Depends on Mockito.
*/
public class JukitoRunner extends BlockJUnit4ClassRunner {

    private static final boolean useAutomockingIfNoEnvironmentFound = true;
    private Injector injector;

    public JukitoRunner(Class<?> klass) throws InitializationError,
            InvocationTargetException, InstantiationException, IllegalAccessException {
        super(klass);
        ensureInjector();
    }

    public JukitoRunner(Class<?> klass, Injector injector) throws InitializationError,
            InvocationTargetException, InstantiationException, IllegalAccessException {
        // refactor needed here cos ensureInjector is run without reason here.
        super(klass);
        this.injector = injector;
    }

    /**
     * Creates an injector from a test module.
     * Override this to use something like Netflix Governator.
     *
     * @param testModule the test module
     * @return a newly created injector
     */
    protected Injector createInjector(TestModule testModule) {
        return Guice.createInjector(testModule);
    }

    private void ensureInjector()
            throws InstantiationException, IllegalAccessException {
        if (injector != null) {
            return;
        }
        Class<?> testClass = getTestClass().getJavaClass();
        TestModule testModule = getTestModule(testClass);
        testModule.setTestClass(testClass);

        JukitoModule jukitoModule = null; // Only non-null if it's a JukitoModule
        if (testModule instanceof JukitoModule) {
            jukitoModule = (JukitoModule) testModule;

            // Create a module just for the purpose of collecting bindings
            TestModule testModuleForCollection = getTestModule(testClass);
            BindingsCollector collector = new BindingsCollector(testModuleForCollection);
            collector.collectBindings();
            jukitoModule.setBindingsObserved(collector.getBindingsObserved());
        }
        injector = this.createInjector(testModule);
        if (jukitoModule != null && jukitoModule.getReportWriter() != null) {
            // An output report is desired
            BindingsCollector collector = new BindingsCollector(jukitoModule);
            collector.collectBindings();
            jukitoModule.printReport(collector.getBindingsObserved());
        }
    }

    private TestModule getTestModule(Class<?> testClass) throws InstantiationException, IllegalAccessException {
        Set<Class<? extends Module>> useModuleClasses = getUseModuleClasses(testClass);
        if (!useModuleClasses.isEmpty()) {
            return createJukitoModule(useModuleClasses);
        }

        TestModule testModule = null;
        for (Class<?> innerClass : testClass.getDeclaredClasses()) {
            if (TestModule.class.isAssignableFrom(innerClass)) {
                assert testModule == null :
                        "More than one TestModule inner class found within test class \""
                                + testClass.getName() + "\".";
                testModule = (TestModule) innerClass.newInstance();
            }
        }
        if (testModule != null) {
            return testModule;
        }

        if (useAutomockingIfNoEnvironmentFound) {
            return new JukitoModule() {
                @Override
                protected void configureTest() {
                }
            };
        } else {
            return new TestModule() {
                @Override
                protected void configureTest() {
                }
            };
        }
    }

    /**
     * Gets Guice modules registered with {@link UseModules} from test class and all super classes.
     *
     * @param testClass the test class running
     * @return set of Guice modules
     */
    private Set<Class<? extends Module>> getUseModuleClasses(Class<?> testClass) {
        Class<?> currentClass = testClass;
        Set<Class<? extends Module>> modules = new HashSet<Class<? extends Module>>();
        while (currentClass != null) {
            UseModules useModules = currentClass.getAnnotation(UseModules.class);
            if (useModules != null) {
                Collections.addAll(modules, useModules.value());
            }
            currentClass = currentClass.getSuperclass();
        }
        return modules;
    }

    private JukitoModule createJukitoModule(final Iterable<Class<? extends Module>> moduleClasses) {
        return new JukitoModule() {
            @Override
            protected void configureTest() {
                for (Class<? extends Module> mClass : moduleClasses) {
                    try {
                        install(mClass.newInstance());
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
    }

    @Override
    public void run(final RunNotifier notifier) {
        // add listener that validates framework usage at the end of each test
        notifier.addListener(new FrameworkUsageValidator(notifier));
        super.run(notifier);
    }

    @Override
    protected Object createTest() throws Exception {
        TestScope.clear();
        instantiateEagerTestSingletons();
        return injector.getInstance(getTestClass().getJavaClass());
    }

    @Override
    protected Statement methodInvoker(final FrameworkMethod method, final Object test) {
        return new InjectedStatement(method, test, injector);
    }

    @Override
    protected Statement withBefores(FrameworkMethod method, Object target,
                                    Statement statement) {
        try {
            ensureInjector();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        List<FrameworkMethod> befores = getTestClass().getAnnotatedMethods(
                Before.class);
        return befores.isEmpty() ? statement : new InjectedBeforeStatements(statement,
                befores, target, injector);
    }

    @Override
    protected Statement withAfters(FrameworkMethod method, Object target,
                                   Statement statement) {
        try {
            ensureInjector();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        List<FrameworkMethod> afters = getTestClass().getAnnotatedMethods(
                After.class);
        return afters.isEmpty() ? statement : new InjectedAfterStatements(statement,
                afters, target, injector);
    }

    @Override
    protected List<FrameworkMethod> computeTestMethods() {
        try {
            ensureInjector();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        List<FrameworkMethod> testMethods = getTestClass().getAnnotatedMethods(Test.class);
        List<FrameworkMethod> result = new ArrayList<FrameworkMethod>(testMethods.size());
        for (FrameworkMethod method : testMethods) {
            Method javaMethod = method.getMethod();
            Errors errors = new Errors(javaMethod);
            List<Key<?>> keys = GuiceUtils.getMethodKeys(javaMethod, errors);
            errors.throwConfigurationExceptionIfErrorsExist();

            List<List<Binding<?>>> bindingsToUseForParameters = new ArrayList<List<Binding<?>>>();
            for (Key<?> key : keys) {
                if (All.class.equals(key.getAnnotationType())) {
                    All allAnnotation = (All) key.getAnnotation();
                    TypeLiteral<?> typeLiteral = key.getTypeLiteral();
                    List<Binding<?>> bindings = getBindingsForParameterWithAllAnnotation(allAnnotation, typeLiteral);
                    bindingsToUseForParameters.add(bindings);
                }
            }
            // Add an injected method for every combination of binding
            addAllBindingAssignations(bindingsToUseForParameters, 0,
                    new ArrayList<Binding<?>>(bindingsToUseForParameters.size()),
                    javaMethod, result);
        }
        return result;
    }

    @Override
    protected String testName(FrameworkMethod method) {
        org.jukito.Description annotation = method.getMethod().getAnnotation(org.jukito.Description.class);
        if (annotation != null) {
            return annotation.value();
        } else {
            return super.testName(method);
        }
    }

    /**
     * Computes a list of all bindings which match a {@link All} annotation.
     *
     * @param allAnnotation the annotation to match
     * @param typeLiteral the type of the bindings.
     * @return the computed list.
     */
    private List<Binding<?>> getBindingsForParameterWithAllAnnotation(All allAnnotation, TypeLiteral<?> typeLiteral) {
        List<Binding<?>> result = new ArrayList<Binding<?>>();
        String bindingName = allAnnotation.value();
        if (All.DEFAULT.equals(bindingName)) {
            // If the annotation is with the default name bind all bindings
            result.addAll(injector.findBindingsByType(typeLiteral));
        } else {
            // Else bind only those bindings which have a key with the same name
            for (Binding<?> binding : injector.findBindingsByType(typeLiteral)) {
                if (NamedUniqueAnnotations.matches(bindingName, binding.getKey().getAnnotation())) {
                    result.add(binding);
                }
            }
        }
        return result;
    }

    /**
     * This method looks at all possible way to assign the bindings in
     * {@code bindingsToUseForParameters}, starting at index {@code index}.
     * If {@code index} is larger than the number of elements in {@code bindingsToUseForParameters}
     * then the {@code currentAssignation} with {@javaMethod} is added to {@code result}.
     *
     * @param result
     * @param javaMethod
     * @param bindingsToUseForParameters
     * @param index
     * @param currentAssignation
     */
    private void addAllBindingAssignations(
            List<List<Binding<?>>> bindingsToUseForParameters, int index,
            List<Binding<?>> currentAssignation,
            Method javaMethod, List<FrameworkMethod> result) {

        if (index >= bindingsToUseForParameters.size()) {
            List<Binding<?>> assignation = new ArrayList<Binding<?>>(currentAssignation.size());
            assignation.addAll(currentAssignation);
            result.add(new InjectedFrameworkMethod(javaMethod, assignation));
            return;
        }

        for (Binding<?> binding : bindingsToUseForParameters.get(index)) {
            if (binding.getKey().getAnnotation() == null) {
                // As TestModule.bindMany() annotates the bindings, the un-annotated bindings are typically unwanted
                // mocks automatically bound by Jukito.
                continue;
            }

            currentAssignation.add(binding);
            if (currentAssignation.size() != index + 1) {
                throw new AssertionError("Size of currentAssignation list is wrong.");
            }
            addAllBindingAssignations(bindingsToUseForParameters, index + 1,
                    currentAssignation,
                    javaMethod, result);
            currentAssignation.remove(index);
        }
    }

    private void instantiateEagerTestSingletons() {
        DefaultBindingScopingVisitor<Boolean> isEagerTestScopeSingleton =
                new DefaultBindingScopingVisitor<Boolean>() {
                    public Boolean visitScope(Scope scope) {
                        return scope == TestScope.EAGER_SINGLETON;
                    }
                };
        for (Binding<?> binding : injector.getBindings().values()) {
            boolean instantiate = false;
            if (binding != null) {
                Boolean result = binding.acceptScopingVisitor(isEagerTestScopeSingleton);
                if (result != null && result) {
                    instantiate = true;
                }
            }
            if (instantiate) {
                binding.getProvider().get();
            }
        }
    }

    /**
     * Adds to {@code errors} for each method annotated with {@code @Test},
     * {@code @Before}, or {@code @After} that is not a public, void instance
     * method with no arguments.
     */
    protected void validateInstanceMethods(List<Throwable> errors) {
        validatePublicVoidMethods(After.class, false, errors);
        validatePublicVoidMethods(Before.class, false, errors);
        validateTestMethods(errors);

        if (computeTestMethods().size() == 0) {
            errors.add(new Exception("No runnable methods"));
        }
    }

    /**
     * Adds to {@code errors} for each method annotated with {@code @Test}that
     * is not a public, void instance method with no arguments.
     */
    protected void validateTestMethods(List<Throwable> errors) {
        validatePublicVoidMethods(Test.class, false, errors);
    }

    /**
     * Adds to {@code errors} if any method in this class is annotated with
     * the provided {@code annotation}, but:
     * <ul>
     * <li>is not public, or
     * <li>returns something other than void, or
     * <li>is static (given {@code isStatic is false}), or
     * <li>is not static (given {@code isStatic is true}).
     */
    protected void validatePublicVoidMethods(Class<? extends Annotation> annotation,
                                             boolean isStatic, List<Throwable> errors) {
        List<FrameworkMethod> methods = getTestClass().getAnnotatedMethods(annotation);

        for (FrameworkMethod eachTestMethod : methods) {
            eachTestMethod.validatePublicVoid(isStatic, errors);
        }
    }

    /**
     * Access the Guice injector.
     *
     * @return The Guice {@link Injector}.
     */
    protected Injector getInjector() {
        return injector;
    }
}
TOP

Related Classes of org.jukito.JukitoRunner

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.