Package org.easetech.easytest.runner

Source Code of org.easetech.easytest.runner.DataDrivenTestRunner

package org.easetech.easytest.runner;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.inject.Inject;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.Factory;
import org.easetech.easytest.annotation.Converters;
import org.easetech.easytest.annotation.DataLoader;
import org.easetech.easytest.annotation.Duration;
import org.easetech.easytest.annotation.Intercept;
import org.easetech.easytest.annotation.Parallel;
import org.easetech.easytest.annotation.Provided;
import org.easetech.easytest.annotation.TestConfigProvider;
import org.easetech.easytest.annotation.TestProperties;
import org.easetech.easytest.converter.Converter;
import org.easetech.easytest.converter.ConverterManager;
import org.easetech.easytest.interceptor.Empty;
import org.easetech.easytest.interceptor.InternalInterceptor;
import org.easetech.easytest.interceptor.InternalInvocationhandler;
import org.easetech.easytest.interceptor.MethodIntercepter;
import org.easetech.easytest.loader.DataLoaderUtil;
import org.easetech.easytest.reports.data.DurationObserver;
import org.easetech.easytest.reports.data.ReportDataContainer;
import org.easetech.easytest.reports.data.TestResultBean;
import org.easetech.easytest.strategy.SchedulerStrategy;
import org.easetech.easytest.util.RunAftersWithOutputData;
import org.easetech.easytest.util.TestInfo;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.Description;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.manipulation.NoTestsRemainException;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.ParentRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.MultipleFailureException;
import org.junit.runners.model.Statement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
*
* Construct a new DataDrivenTestRunner. This runner is an extension of {@link BlockJUnit4ClassRunner}. The algorithm of
* constructing and making the runner ready for execution is as follows:<br>
* <ul>
* <li>Set the Scheduling Strategy. The {@link SchedulerStrategy} can be <b>PARALLEL</b> or <b>SERIAL</b> and is decided
* based on {@link Parallel} annotation</li>
* <li>Load the Test Bean Configurations that are defined at the class level using the {@link TestConfigProvider}
* annotation.<br>
* (For details on how he test bean is loaded look at {@link TestConfigUtil#loadTestBeanConfig(Class)}) method.</li>
* <li>Next, load the input test data, if any, at the class level. We mention if any because the input test data can be
* loaded per method as well instead of loading it at the class level.<br>
* (For details on how the input test data is loaded, look at {@link DataLoaderUtil#loadData}) method.</li>
* <li>Next, we registers the converters, if any, declared at the class level using {@link Converters} annotation</li>
* <li>We then move to inject the fields in the test class marked with {@link Provided} or {@link Inject} annotation
* with the test beans that were loaded in step 1 above.</li>
* <li>Next, we try to resolve any test properties that are specified on the test class using the {@link TestProperties}
* annotation.<br>
* Note that {@link TestProperties} annotation requires a filed of type {@link Properties} be defined at the class
* level. Look at {@link TestConfigUtil#loadResourceProperties(Class, Object)} method for details.</li>
* <li>We finally create a proxy for the class under test, if it is marked with {@link Intercept} annotation. This helps
* in intercepting the method calls to the class under test and perform any initialization/destruction logic
* before/after the method execution.</li>
*
* @author Anuj Kumar
*/
public class DataDrivenTestRunner extends BlockJUnit4ClassRunner {

    /**
     * An instance of logger associated with the test framework.
     */
    private final Logger LOG = LoggerFactory.getLogger(DataDrivenTestRunner.class);

    /**
     * An instance of {@link Map} that contains the data to be written to the File
     */
    private Map<String, List<Map<String, Object>>> writableData = new HashMap<String, List<Map<String, Object>>>();

    /**
     * Convenient class member to get the list of {@link FrameworkMethod} that this runner will execute.
     */
    private final List<FrameworkMethod> frameworkMethods;

    /**
     * The report container which holds all the reporting data
     */
    private final ReportDataContainer testReportContainer;
   
    /**
     * An observer that is responsible for capturing the Duration of a methd under test
     */
    private final DurationObserver durationObserver = new DurationObserver();

    /**
     * Look at {@link DataDrivenTestRunner} for details.
     *
     * @see DataDrivenTestRunner
     * @param klass
     * @throws InitializationError
     */
    public DataDrivenTestRunner(Class<?> klass) throws InitializationError {

        super(klass);
        setSchedulingStrategy();
        RunnerUtil.loadBeanConfiguration(getTestClass().getJavaClass());
        RunnerUtil.loadClassLevelData(klass, getTestClass(), writableData);
        try {
            // initialize report container class
            // TODO add condition whether reports must be switched on or off
            testReportContainer = new ReportDataContainer(getTestClass().getJavaClass());
            testReportContainer.setDurationList(durationObserver.getDurationList());
            frameworkMethods = computeMethodsForTest();

        } catch (Exception e) {
            LOG.error("Exception occured while instantiating the DataDrivenTestRunner. Exception is : ", e);
            throw new RuntimeException(e);
        }

    }
   

    /**
     * Set whether the tests should be run in parallel or serial.
     */
    protected void setSchedulingStrategy() {
        Class<?> testClass = getTestClass().getJavaClass();
        super.setScheduler(RunnerUtil.getScheduler(testClass));
    }
   
    /**
     * Overridden the compute test method to make it save the method list as class instance, so that the method does not
     * run multiple times. Also, this method now is responsible for creating multiple {@link FrameworkMethod} instances
     * for a given method with multiple test data. So, if a given test method needs to run three times with three set of
     * input test data, then this method will actually create three instances of {@link FrameworkMethod}. In order to
     * allow the user to override the default name, {@link FrameworkMethod} is extended with {@link EasyFrameworkMethod}
     * and {@link EasyFrameworkMethod#setName(String)} method introduced.
     *
     * @return list of {@link FrameworkMethod}
     */

    protected List<FrameworkMethod> computeMethodsForTest() {

        List<FrameworkMethod> finalList = RunnerUtil.testMethods(getTestClass(), testReportContainer, writableData);
        if (finalList.isEmpty()) {
            Assert.fail("No method exists for the Test Runner");
        }
        return finalList;
    }

    /**
     * Get the instance of the class under test
     *
     * @return the instance of class under test
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @throws IllegalArgumentException
     * @throws InvocationTargetException
     */
    protected Object getTestInstance() throws InstantiationException, IllegalAccessException, IllegalArgumentException,
        InvocationTargetException {
        return getTestClass().getOnlyConstructor().newInstance();
    }

    /**
     * @see TestConfigUtil#loadTestConfigurations(Class, Object)
     */
    protected void loadTestConfigurations(Object testInstance) {
        TestConfigUtil.loadTestConfigurations(getTestClass().getJavaClass(), testInstance);
    }

    /**
     * @see TestConfigUtil#loadResourceProperties
     */
    protected void loadResourceProperties(Object testInstance) {
        TestConfigUtil.loadResourceProperties(getTestClass().getJavaClass(), testInstance);
    }

    /**
     * Compute any test methods
     *
     * @return a list of {@link FrameworkMethod}s
     */
    protected List<FrameworkMethod> computeTestMethods() {
        return frameworkMethods;
    }

    /**
     * Override the filter method from {@link ParentRunner} so that individual tests can be run using EasyTest
     *
     * @param filter
     * @throws NoTestsRemainException
     */
    public void filter(Filter filter) throws NoTestsRemainException {

        for (Iterator<FrameworkMethod> iter = frameworkMethods.iterator(); iter.hasNext();) {
            FrameworkMethod each = iter.next();
            if (shouldRun(filter, each))
                try {
                    filter.apply(each);
                } catch (NoTestsRemainException e) {
                    iter.remove();
                }
            else
                iter.remove();
        }
        if (frameworkMethods.isEmpty()) {
            throw new NoTestsRemainException();
        }
    }

    private boolean shouldRun(Filter filter, FrameworkMethod each) {
        return filter.shouldRun(describeFiltarableChild(each));
    }

    private Description describeFiltarableChild(FrameworkMethod each) {
        return Description.createTestDescription(getTestClass().getJavaClass(), each.getMethod().getName(),
            each.getAnnotations());
    }

    /**
     * Instrument the class's field that are marked with {@link Intercept} annotation
     *
     * @param testClass the class under test
     * @throws IllegalArgumentException if an exception occurred
     * @throws IllegalAccessException if an exception occurred
     * @throws InstantiationException if an exception occurred
     */
    protected void instrumentClass(Class<?> testClass, Object testInstance) throws IllegalArgumentException,
        IllegalAccessException, InstantiationException {
        Field[] fields = testClass.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            Intercept interceptor = field.getAnnotation(Intercept.class);
            if (interceptor != null) {
                provideProxyWrapperFor(interceptor.interceptor(), null, field, testInstance);
            } else {
                Duration duration = field.getAnnotation(Duration.class);
                if (duration != null) {
                    provideProxyWrapperFor(duration.interceptor(), duration.timeInMillis(), field, testInstance);
                }
            }

        }
    }



    /**
     * Method responsible for registering the converters with the EasyTest framework
     *
     * @param converter the annotation {@link Converters}
     */
    @SuppressWarnings("rawtypes")
    public void registerConverter(Converters converter) {
        if (converter != null) {
            Class<? extends Converter>[] convertersToRegister = converter.value();
            if (convertersToRegister != null && convertersToRegister.length != 0) {
                for (Class<? extends Converter> value : convertersToRegister) {
                    ConverterManager.registerConverter(value);
                }
            }
        }

    }

   
    /**
     * Override the name of the test. In case of EasyTest, it will be the name of the test method concatenated with the
     * input test data that the method will run with.
     *
     * @param method the {@link FrameworkMethod}
     * @return an overridden test method Name
     */

    protected String testName(final FrameworkMethod method) {
        return RunnerUtil.getTestName(getTestClass(), method);
    }

    /**
     * Validate that there could ever be only one constructor.
     *
     * @param errors list of any errors while validating the Constructor
     */

    protected void validateConstructor(List<Throwable> errors) {
        validateOnlyOneConstructor(errors);
    }

    /**
     * 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.
     *
     * @deprecated unused API, will go away in future version
     */
    @Deprecated
    protected void validateInstanceMethods(List<Throwable> errors) {
        validatePublicVoidNoArgMethods(After.class, false, errors);
        validatePublicVoidNoArgMethods(Before.class, false, errors);
        validateTestMethods(errors);

        if (getTestClass().getAnnotatedMethods(Test.class).size() == 0)
            errors.add(new Exception("No runnable methods"));
    }

    /**
     * Validate the test methods.
     *
     * @param errors list of any errors while validating test method
     */

    protected void validateTestMethods(List<Throwable> errors) {
        // Do Nothing as we now support public non void arg test methods
    }

    protected Statement methodBlock(FrameworkMethod method) {
        return withTestResult((EasyFrameworkMethod) method, super.methodBlock(method));
    }

    protected Statement withTestResult(final EasyFrameworkMethod method, final Statement statement) {
        return new Statement() {

            @Override
            public void evaluate() throws Throwable {
                TestResultBean testResult = method.getTestResult();
                try {
                    statement.evaluate();
                    testResult.setPassed(Boolean.TRUE);
                } catch (Throwable e) {

                    if (e instanceof AssertionError) { // Assertion error
                        testResult.setPassed(Boolean.FALSE);
                        testResult.setResult(e.getMessage());
                        throw e;

                    } else { // Exception
                        testResult.setException(Boolean.TRUE);
                        testResult.setExceptionResult(e.toString());
                        throw e;

                    }

                }

            }
        };
    }

    /**
     * Returns a new fixture for running a test. Default implementation executes the test class's no-argument
     * constructor (validation should have ensured one exists).
     */
    protected Object createTest() throws Exception {
        Object testInstance = getTestClass().getOnlyConstructor().newInstance();
        loadTestConfigurations(testInstance);
        loadResourceProperties(testInstance);
        instrumentClass(getTestClass().getJavaClass(), testInstance);
       
        registerConverter(getTestClass().getJavaClass().getAnnotation(Converters.class));
        return testInstance;

    }

    /**
     * Returns a {@link Statement} that invokes {@code method} on {@code test}
     */
    protected Statement methodInvoker(FrameworkMethod method, Object testInstance) {
        registerConverter(method.getAnnotation(Converters.class));
       
        if (method.getAnnotation(Duration.class) != null) {
            try {
                handleDuration(method, testInstance);
            } catch (IllegalArgumentException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            } catch (InstantiationException e) {
                throw new RuntimeException(e);
            }
        }
        Statement statement = new InternalParameterizedStatement(null, (EasyFrameworkMethod) method, getTestClass(), testInstance);
       
        if(((EasyFrameworkMethod)method).getChildMethods()!= null && !((EasyFrameworkMethod)method).getChildMethods().isEmpty()) {
            for(EasyFrameworkMethod childMethod : ((EasyFrameworkMethod)method).getChildMethods()) {
                try {
                    handleDuration(childMethod, testInstance);
                } catch (IllegalArgumentException e) {
                    throw new RuntimeException(e);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                } catch (InstantiationException e) {
                    throw new RuntimeException(e);
                }
                statement = new InternalParameterizedStatement(statement, childMethod, getTestClass(), testInstance);
            }
        }
        return statement;
    }

    private void handleDuration(FrameworkMethod method, Object testInstance) throws IllegalArgumentException,
        IllegalAccessException, InstantiationException {
        Duration duration = method.getAnnotation(Duration.class);
        if (duration != null) {
            if (!duration.forClass().isAssignableFrom(Empty.class)) {
                interceptField(duration, getTestClass().getJavaClass(), testInstance);
            } else {
                Assert.fail("Duration annotation at the method level should have value for the 'forClass' attribute.");
            }

        }
    }

    private void interceptField(Duration duration, Class<?> testClass, Object testInstance)
        throws IllegalArgumentException, IllegalAccessException, InstantiationException {
        Field[] fields = testClass.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            if (field.getType().isAssignableFrom(duration.forClass())) {
                provideProxyWrapperFor(duration.interceptor(), duration.timeInMillis(), field, testInstance);

            }
        }
    }

    private void provideProxyWrapperFor(Class<? extends MethodIntercepter> interceptor, Long timeInMillies,
        Field field, Object testInstance) throws IllegalArgumentException, IllegalAccessException,
        InstantiationException {
        Object fieldInstance = field.get(testInstance);
        if(fieldInstance == null) {
            Assert.fail("Unable to get the instance value of field :" + field.getName() +
                ". Are you injecting the field using dependency injection support of Easytest? " +
                "Then, please check that the bean is configured in the configuration file. " +
                "If the problem persist, please contact anujkumar@easetech.org");
        }
        Object targetInstance = null;

        Object proxiedObject = null;
        Class<?> fieldType = field.getType();
        Class<? extends MethodIntercepter> interceptorClass = interceptor;
        if (fieldType.isInterface()) {
            if (Proxy.isProxyClass(fieldInstance.getClass())) {
                InternalInvocationhandler handler = (InternalInvocationhandler) Proxy
                    .getInvocationHandler(fieldInstance);
                targetInstance = handler.getTargetInstance();
            } else {
                targetInstance = fieldInstance;
            }
            proxiedObject = getJDKProxy(interceptorClass, timeInMillies, fieldType, targetInstance);
        } else {
            if (fieldInstance instanceof Factory) {
                Factory cglibFactory = (Factory) fieldInstance;
                InternalInterceptor internalInterceptor = (InternalInterceptor) cglibFactory.getCallback(0);
                targetInstance = internalInterceptor.getTargetInstance();
            } else {
                targetInstance = fieldInstance;
            }
            proxiedObject = getCGLIBProxy(interceptorClass, timeInMillies, fieldType, targetInstance);

        }
        try {
            if (proxiedObject != null) {
                field.set(testInstance, proxiedObject);
            }

        } catch (Exception e) {
            LOG.error("Failed while trying to instrument the class for Intercept annotation with exception : ", e);
            Assert.fail("Failed while trying to instrument the class for Intercept annotation with exception : " + e);
        }
    }

    private Object getJDKProxy(Class<? extends MethodIntercepter> interceptorClass, Long timeInMillis,
        Class<?> fieldType, Object fieldInstance) throws InstantiationException, IllegalAccessException {
        LOG.debug("The field of type :" + fieldType + " will be proxied using JDK dynamic proxies.");

        ClassLoader classLoader = RunnerUtil.determineClassLoader(fieldType, getTestClass().getJavaClass());

        Class<?>[] interfaces = { fieldType };
        // use JDK dynamic proxy
        InternalInvocationhandler handler = new InternalInvocationhandler();
        handler.setUserIntercepter(interceptorClass.newInstance());
        handler.setTargetInstance(fieldInstance);
        handler.setExpectedRunTime(timeInMillis);
        handler.addObserver(durationObserver);
        return Proxy.newProxyInstance(classLoader, interfaces, handler);
    }

    private Object getCGLIBProxy(Class<? extends MethodIntercepter> interceptorClass, Long timeInMillis,
        Class<?> fieldType, Object fieldInstance) throws InstantiationException, IllegalAccessException {
        LOG.debug("The field of type :" + fieldType + " will be proxied using CGLIB proxies.");
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(fieldType);
        InternalInterceptor cglibInterceptor = new InternalInterceptor();
        cglibInterceptor.setTargetInstance(fieldInstance);
        cglibInterceptor.setUserIntercepter(interceptorClass.newInstance());
        cglibInterceptor.setExpectedRunTime(timeInMillis);
        cglibInterceptor.addObserver(durationObserver);
        enhancer.setCallback(cglibInterceptor);
        return enhancer.create();
    }

    /**
     * Returns a {@link Statement}: run all non-overridden {@code @AfterClass} methods on this class and superclasses
     * before executing {@code statement}; all AfterClass methods are always executed: exceptions thrown by previous
     * steps are combined, if necessary, with exceptions from AfterClass methods into a {@link MultipleFailureException}
     * .
     *
     * This method is also responsible for writing the data to the output file in case the user is returning test data
     * from the test method. This method will make sure that the data is written to the output file once after the
     * Runner has completed and not for every instance of the test method.
     */

    protected Statement withAfterClasses(Statement statement) {
        List<FrameworkMethod> afters = getTestClass().getAnnotatedMethods(AfterClass.class);
        List<FrameworkMethod> testMethods = getTestClass().getAnnotatedMethods(Test.class);
        List<TestInfo> testInfoList = new ArrayList<TestInfo>();

        // populateTestInfo(testInfo);
        // THere would always be atleast one method associated with the Runner, else validation would fail.
        for (FrameworkMethod method : testMethods) {
            TestInfo testInfo = null;

            // Only if the return type of the Method is not VOID, we try to determine the right loader and data
            // files.
            DataLoader loaderAnnotation = method.getAnnotation(DataLoader.class);
            if (loaderAnnotation != null) {
                testInfo = DataLoaderUtil.determineLoader(loaderAnnotation, getTestClass());

            } else {
                loaderAnnotation = getTestClass().getJavaClass().getAnnotation(DataLoader.class);
                if (loaderAnnotation != null) {
                    testInfo = DataLoaderUtil.determineLoader(loaderAnnotation, getTestClass());
                }
            }
            if (testInfo != null) {
                testInfo.setMethodName(method.getName());
                testInfoList.add(testInfo);
            }

        }
       
        RunAftersWithOutputData runAftersWithOutputData = new RunAftersWithOutputData(statement, afters, null,
            testInfoList, writableData, testReportContainer);
        return runAftersWithOutputData;
    }

}
TOP

Related Classes of org.easetech.easytest.runner.DataDrivenTestRunner

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.