package org.easetech.easytest.runner;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.aopalliance.intercept.MethodInterceptor;
import org.easetech.easytest.annotation.DataLoader;
import org.easetech.easytest.annotation.Intercept;
import org.easetech.easytest.annotation.Param;
import org.easetech.easytest.loader.DataConverter;
import org.easetech.easytest.loader.Loader;
import org.easetech.easytest.loader.LoaderFactory;
import org.easetech.easytest.loader.LoaderType;
import org.easetech.easytest.util.DataContext;
import org.easetech.easytest.util.RunAftersWithOutputData;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Test;
import org.junit.experimental.theories.DataPoint;
import org.junit.experimental.theories.ParametersSuppliedBy;
import org.junit.experimental.theories.PotentialAssignment;
import org.junit.experimental.theories.PotentialAssignment.CouldNotGenerateValueException;
import org.junit.experimental.theories.internal.Assignments;
import org.junit.internal.AssumptionViolatedException;
import org.junit.runner.Runner;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.Suite;
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.junit.runners.model.TestClass;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.AopConfigException;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* A Spring based implementation of {@link Suite} that encapsulates the {@link EasyTestRunner} in order to provide users with clear
* indication of which test method is run and what is the input test data that the method is run with. For example, when
* a user runs the test method with name : <B><I>getTestData</I></B> with the following test data:
* <ul>
* <li><B>"libraryId=1 and itemId=2"</B></li>
* <li><B>"libraryId=2456 and itemId=789"</B></li><br>
* <br>
*
* then, {@link SpringTestRunner}, will provide the details of the executing test method in the JUnit supported IDEs
* like this:
*
* <ul>
* <li><B><I>getTestData{libraryId=1 ,itemId=2}</I></B></li>
* <li><B><I>getTestData{libraryId=2456 ,itemId=789}</I></B></li></br></br>
*
* This gives user the clear picture of which test was run with which input test data.
*
* For details on the actual Runner implementation, see {@link EasyTestRunner}
*
* @author Anuj Kumar
*
*
*/
public class SpringTestRunner extends Suite {
/**
* The list of files that are used by the {@link Loader}s {@link Loader#writeData(String, Map)}
* functionality to write the test data back to the file. This is also passed to the {@link RunAftersWithOutputData} method.
*/
private String[] dataFiles;
/**
* The instance of {@link Loader} that is currently being used.
* This is also passed to the {@link RunAftersWithOutputData}'s constructor which is responsible
* for calling the right {@link Loader#writeData(String, Map)} based on the {@link Loader} instance.
*
*/
private Loader dataLoader = null;
/**
* An instance of {@link Map} that contains the data to be written to the File
*/
private static Map<String, List<Map<String, Object>>> writableData = new HashMap<String, List<Map<String, Object>>>();
/**
* The default rowNum within the {@link #writableData}'s particular method data.
*/
private static int rowNum = 0;
/**
* The name of the method currently being executed. Used for populating the {@link #writableData} map.
*/
private String mapMethodName = "";
/**
* An instance of logger associated with the test framework.
*/
protected static final Logger PARAM_LOG = LoggerFactory.getLogger(SpringTestRunner.class);
/**
* A {@link BlockJUnit4ClassRunner} Runner implementation that adds support of input parameters as part of the
* {@link Test} annotation. This {@link BlockJUnit4ClassRunner} extension is modified for providing convenient Data
* Driven Testing support to its users. This Runner is capable of generating new instances of
* {@link FrameworkMethod} based on the test data for a given method. For eg. If there is a method
* "testMethod(String testData)" that has three sets of test data : [{"testData1"},{"testData2"},{"testData3"}],
* then this runner will generate three {@link FrameworkMethod} instances with the method names :<br>
* testMethod{testData1}<br>
* testMethod{testData2}<br>
* and<br>
* testMethod{testData3}<br>
* <br>
*
* <br>
* <B> A user can specify the test data at the class level, using the {@link DataLoader} annotation and override it
* at the method level. The Runner will take care of executing the test method with the right test data.</B><br>
* This is extremely beneficial in cases, where the user just wants to load the data once and then reuse it for all
* the test methods. If the user wants, then he can always override the test data at the method level by specifying
* the {@link DataLoader} annotation at the method level. <br>
* <br>
* In addition, this runner also introduces a new way for the user to specify the test data using {@link DataLoader}
* annotation.
*
* <br>
* <br>
* There is also a {@link Param} annotation to handle boiler plate tasks on behalf of the user as well as supports
* additional functionality that eases the life of the user. For eg. it supports Java PropertyEditors to
* automatically convert a String to the specified Object. It also supports passing a Map to the test method that
* contains all the available test data key / value pairs for easy consumption by the user. It also supports user
* defined custom Objects as parameters.<br>
* <br>
*
* @author Anuj Kumar
*/
private class EasyTestRunner extends SpringJUnit4ClassRunner {
/**
* Convenient class member to get the list of {@link FrameworkMethod} that this runner will execute.
*/
List<FrameworkMethod> frameworkMethods;
/**
* The actual instance of the test class.
* This is extremely handy in cases where we want to reflectively set instance fields on a test class.
*/
Object testInstance;
/**
*
* Construct a new DataDrivenTestRunner
*
* @param klass the test class whose test methods needs to be executed
* @throws InitializationError if any error occurs
*/
public EasyTestRunner(Class<?> klass) throws InitializationError {
super(klass);
try {
testInstance = getTestClass().getOnlyConstructor().newInstance();
getTestContextManager().prepareTestInstance(testInstance);
instrumentClass(getTestClass().getJavaClass());
} catch (Exception e) {
Assert.fail("Test failed while trying to instrument fileds in the class : " + getTestClass().getJavaClass());
}
}
/**
* 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 AopConfigException if an exception occurred
* @throws InstantiationException if an exception occurred
*/
protected void instrumentClass(Class<?> testClass) throws IllegalArgumentException, IllegalAccessException, AopConfigException, InstantiationException{
Field[] fields = testClass.getFields();
for(Field field : fields){
Intercept interceptor = field.getAnnotation(Intercept.class);
if(interceptor != null){
Class<? extends MethodInterceptor> interceptorClass = interceptor.interceptor();
//This is the field we want to enhance
Object fieldInstance = field.get(testInstance);
ProxyFactory factory = new ProxyFactory();
factory.setTarget(fieldInstance);
factory.addAdvice(interceptorClass.newInstance());
Object proxy = factory.getProxy();
try{
field.set(testInstance,proxy);
}catch(Exception e){
Assert.fail("Failed while trying to instrument the class for Intercept annotation with exception : " + e.getStackTrace());
}
}
}
}
/**
* Try to collect any initialization errors, if any.
*
* @param errors
*/
@Override
protected void collectInitializationErrors(List<Throwable> errors) {
super.collectInitializationErrors(errors);
validateDataPointFields(errors);
}
/**
* 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
*/
@Override
protected String testName(final FrameworkMethod method) {
return String.format("%s", method.getName());
}
/**
* 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}
*/
@Override
protected List<FrameworkMethod> computeTestMethods() {
if (frameworkMethods != null && !frameworkMethods.isEmpty()) {
return frameworkMethods;
}
// superMethodName variable comes from the enclosing DataDrivenTest class.
// It holds the name of the test method on which the DataDrivenTestRunner instance will work.
// if (superMethodName == null) {
// Assert.fail("Cannot compute Test Methods to run");
// }
List<FrameworkMethod> finalList = new ArrayList<FrameworkMethod>();
//Iterator<FrameworkMethod> testMethodsItr = super.computeTestMethods().iterator();
Class<?> testClass = getTestClass().getJavaClass();
for (FrameworkMethod methodWithData : methodsWithData) {
String superMethodName = DataConverter.getFullyQualifiedTestName(methodWithData.getName(), testClass);
for(FrameworkMethod method : super.computeTestMethods()) {
if (superMethodName.equals(DataConverter.getFullyQualifiedTestName(method.getName(), testClass))) {
// Load the data,if any, at the method level
loadData(null, method, getTestClass().getJavaClass());
List<Map<String, Object>> methodData = DataContext.getData().get(superMethodName);
if (methodData == null) {
Assert.fail("Method with name : " + superMethodName
+ " expects some input test data. But there doesnt seem to be any test "
+ "data for the given method. Please check the Test Data file for the method data. "
+ "Possible cause could be a spelling mismatch.");
}
for (Map<String, Object> testData : methodData) {
// Create a new FrameworkMethod for each set of test data
EasyFrameworkMethod easyMethod = new EasyFrameworkMethod(method.getMethod());
easyMethod.setName(method.getName().concat(testData.toString()));
finalList.add(easyMethod);
}
// Since the runner only ever handles a single method, we break out of the loop as soon as we
// have
// found our method.
break;
}
}
}
finalList.addAll(methodsWithNoData);
if (finalList.isEmpty()) {
Assert.fail("No method exists for the Test Runner");
}
frameworkMethods = finalList;
return finalList;
}
/**
* Validate the {@link DataPoint} filed level annotation.
*
* @param errors list of any errors while validating the {@link DataPoint} field.
*/
private void validateDataPointFields(List<Throwable> errors) {
Field[] fields = getTestClass().getJavaClass().getDeclaredFields();
for (Field each : fields)
if (each.getAnnotation(DataPoint.class) != null && !Modifier.isStatic(each.getModifiers()))
errors.add(new Error("DataPoint field " + each.getName() + " must be static"));
}
/**
* Validate that there could ever be only one constructor.
*
* @param errors list of any errors while validating the Constructor
*/
@Override
protected void validateConstructor(List<Throwable> errors) {
validateOnlyOneConstructor(errors);
}
/**
* Validate the test methods.
*
* @param errors list of any errors while validating test method
*/
@Override
protected void validateTestMethods(List<Throwable> errors) {
//Do Nothing as we now support public non void arg test methods
}
@Override
protected Object createTest() throws Exception {
return null;
}
/**
* Override the methodBlock to return custom {@link ParamAnchor}
*
* @param method the Framework Method
* @return a compiled {@link Statement} object to be evaluated
*/
@Override
public Statement methodBlock(final FrameworkMethod method) {
return new ParamAnchor(method, getTestClass());
}
/**
* 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.
*/
@Override
protected Statement withAfterClasses(Statement statement) {
List<FrameworkMethod> afters = getTestClass().getAnnotatedMethods(AfterClass.class);
// THere would always be atleast on method associated with the Runner, else validation would fail.
FrameworkMethod method = frameworkMethods.get(0);
// Only if the return type of the Method is not VOID, we try to determine the right loader and data files.
if (method.getMethod().getReturnType() != Void.TYPE) {
DataLoader loaderAnnotation = method.getAnnotation(DataLoader.class);
if (loaderAnnotation != null) {
determineLoader(loaderAnnotation);
} else {
loaderAnnotation = getTestClass().getJavaClass().getAnnotation(DataLoader.class);
if (loaderAnnotation != null) {
determineLoader(loaderAnnotation);
}
}
if (dataLoader == null) {
Assert.fail("The framework currently does not support the specified Loader type. "
+ "You can provide the custom Loader by choosing LoaderType.CUSTOM in TestData "
+ "annotation and providing your custom loader using DataLoader annotation.");
}
dataFiles = loaderAnnotation.filePaths();
} else {
dataLoader = null;
}
return new RunAftersWithOutputData(statement, afters, null, dataLoader, dataFiles, writableData);
}
/**
*
* Static inner class to support Statement evaluation.
*
*/
public class ParamAnchor extends Statement {
/**
* An instance of logger associated with the test framework.
*/
protected final Logger LOG = LoggerFactory.getLogger(EasyTestRunner.ParamAnchor.class);
private int successes = 0;
/**
* an instance of {@link FrameworkMethod} identifying the method to be tested.
*/
private FrameworkMethod fTestMethod;
/**
* An instance of {@link TestClass} identifying the class under test
*/
private TestClass fTestClass;
/**
* A List of {@link Assignments}. Each member in the list corresponds to a single set of test data to be
* passed to the test method. For eg. If the user has specified the test data in the CSV file as:<br>
* <br>
* <B>testGetItems,LibraryId,itemType,searchText</B> <br>
* ,4,journal,batman <br>
* ,1,ebook,potter <br>
* where: <li>testGetItems is the name of the method</li> <li>
* LibraryId,itemType,searchText are the names of the parameters that the test method expects</li> and <li>
* ,4,journal,batman</li> <li>,1,ebook,potter</li> are the actual test data <br>
* then this list will consists of TWO {@link Assignments} instances with values: <li>[[{LibraryId=4,
* itemType=journal, searchText=batman}]]</li> AND <li>[[{LibraryId=1, itemType=ebook, searchText=potter}]]
*
*/
private List<Assignments> listOfAssignments;
/**
* List of Invalid parameters
*/
private List<AssumptionViolatedException> fInvalidParameters = new ArrayList<AssumptionViolatedException>();
/**
*
* Construct a new ParamAnchor. The constructor performs the following operations:<br>
* <li>It sets the class variables method , testClass and initializes the instance of
* {@link #listOfAssignments}</li> <li>
* It searches for {@link DataLoader} annotation and if it finds one, it tries to get the right
* {@link Loader} from the {@link LoaderFactory}. If the {@link Loader} is not found, the test fails. If the
* Loader is found, it loads the data and makes it available to the entire test Thread using
* {@link DataContext}
*
* If the annotation {@link DataLoader} is not present, then the test assumes that the user wants to use
* {@link ParametersSuppliedBy} annotation and does nothing.
*
* @param method the method to run the test on
* @param testClass an instance of {@link TestClass}
*/
public ParamAnchor(FrameworkMethod method, TestClass testClass) {
fTestMethod = method;
fTestClass = testClass;
listOfAssignments = new ArrayList<Assignments>();
DataContext.setMethodName(DataConverter.getFullyQualifiedTestName(method.getName(),
testClass.getJavaClass()));
}
private TestClass getTestClass() {
return fTestClass;
}
@Override
public void evaluate() throws Throwable {
runWithAssignment(Assignments.allUnassigned(fTestMethod.getMethod(), getTestClass()));
LOG.debug("ParamAnchor evaluate");
if (successes == 0)
Assert.fail("Never found parameters that satisfied method assumptions. Violated assumptions: "
+ fInvalidParameters);
}
/**
* This method encapsulates the actual change in behavior from the traditional JUnit Theories way of
* populating and supplying the test data to the test method. This method creates a list of
* {@link Assignments} identified by {@link #listOfAssignments} and then calls
* {@link #runWithCompleteAssignment(Assignments)} for each {@link Assignments} element in the
* {@link #listOfAssignments}
*
* @param parameterAssignment an instance of {@link Assignments} identifying the parameters that needs to be
* supplied test data
* @throws Throwable if any exception occurs.
*/
protected void runWithAssignment(Assignments parameterAssignment) throws Throwable {
while (!parameterAssignment.isComplete()) {
List<PotentialAssignment> potentialAssignments = parameterAssignment.potentialsForNextUnassigned();
boolean isFirstSetOfArguments = listOfAssignments.isEmpty();
for (int i = 0; i < potentialAssignments.size(); i++) {
if (isFirstSetOfArguments) {
Assignments assignments = Assignments
.allUnassigned(fTestMethod.getMethod(), getTestClass());
listOfAssignments.add(assignments.assignNext(potentialAssignments.get(i)));
} else {
Assignments assignments = listOfAssignments.get(i);
try {
listOfAssignments.set(i, assignments.assignNext(potentialAssignments.get(i)));
} catch (IndexOutOfBoundsException e) {
listOfAssignments.add(assignments.assignNext(potentialAssignments.get(i)));
}
}
}
parameterAssignment = parameterAssignment.assignNext(null);
}
if (listOfAssignments.isEmpty()) {
LOG.debug("The list of Assignments is null. It normally happens when the user has not supplied any parameters to the test.");
LOG.debug(" Creating an instance of Assignments object with all its value unassigned.");
listOfAssignments.add(Assignments.allUnassigned(fTestMethod.getMethod(), getTestClass()));
}
for (Assignments assignments : listOfAssignments) {
runWithCompleteAssignment(assignments);
}
}
/**
* Run the test data with complete Assignments
*
* @param complete the {@link Assignments}
* @throws InstantiationException if an error occurs while instantiating the method
* @throws IllegalAccessException if an error occurs due to illegal access to the test method
* @throws InvocationTargetException if an error occurs because the method is not invokable
* @throws NoSuchMethodException if an error occurs because no such method with the given name exists.
* @throws Throwable any other error
*/
protected void runWithCompleteAssignment(final Assignments complete) throws InstantiationException,
IllegalAccessException, InvocationTargetException, NoSuchMethodException, Throwable {
new BlockJUnit4ClassRunner(getTestClass().getJavaClass()) {
@Override
protected void collectInitializationErrors(List<Throwable> errors) {
// do nothing
}
@Override
public Statement methodBlock(FrameworkMethod method) {
final Statement statement = super.methodBlock(method);
return new Statement() {
@Override
public void evaluate() throws Throwable {
try {
statement.evaluate();
handleDataPointSuccess();
} catch (AssumptionViolatedException e) {
handleAssumptionViolation(e);
} catch (Throwable e) {
throw e;
}
}
};
}
@Override
protected Statement methodInvoker(FrameworkMethod method, Object test) {
return methodCompletesWithParameters(method, complete, test);
}
@Override
public Object createTest() throws Exception {
return testInstance;
}
}.methodBlock(fTestMethod).evaluate();
}
/**
* This method is responsible for actually executing the test method as well as capturing the test data
* returned by the test method. The algorithm to capture the output data is as follows:
* <ol>
* After the method has been invoked explosively, the returned value is checked. If there is a return value:
* <li>We get the name of the method that is currently executing,
* <li>We find teh exact place in the test input data for which this method was executed,
* <li>We put the returned result in the map of input test data. The entry in the map has the key :
* {@link Loader#ACTUAL_RESULT} and the value is the returned value by the test method.
*
* We finally write the test data to the file.
*
* @param method an instance of {@link FrameworkMethod} that needs to be executed
* @param complete an instance of {@link Assignments} that contains the input test data values
* @param freshInstance a fresh instance of the class for which the method needs to be invoked.
* @return an instance of {@link Statement}
*/
private Statement methodCompletesWithParameters(final FrameworkMethod method, final Assignments complete,
final Object freshInstance) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
try {
final Object[] values = complete.getMethodArguments(true);
Object returnObj = method.invokeExplosively(freshInstance, values);
if (returnObj != null) {
LOG.debug("returnObj:" + returnObj);
if (!mapMethodName.equals(method.getMethod().getName())) {
mapMethodName = method.getMethod().getName();
rowNum = 0;
}
LOG.debug("mapMethodName:" + mapMethodName + " ,rowNum:" + rowNum);
if (writableData.get(mapMethodName) != null) {
LOG.debug("writableData.get(mapMethodName)" + writableData.get(mapMethodName)
+ " ,rowNum:" + rowNum);
writableData.get(mapMethodName).get(rowNum++).put(Loader.ACTUAL_RESULT, returnObj);
}
}
} catch (CouldNotGenerateValueException e) {
// ignore
}
}
};
}
protected void handleAssumptionViolation(AssumptionViolatedException e) {
fInvalidParameters.add(e);
}
protected void handleDataPointSuccess() {
successes++;
}
}
}
/**
* A List of {@link EasyTestRunner}s
*/
private final ArrayList<Runner> runners = new ArrayList<Runner>();
/**
* List of {@link FrameworkMethod} that does not have any external test data associated with them.
*/
private List<FrameworkMethod> methodsWithNoData = new ArrayList<FrameworkMethod>();
/**
* List of {@link FrameworkMethod} that does have any external test data associated with them.
*/
private List<FrameworkMethod> methodsWithData = new ArrayList<FrameworkMethod>();
/**
* Get the children Runners
*
* @return a list of {@link DataDrivenTestRunner}
*/
@Override
protected List<Runner> getChildren() {
return runners;
}
/**
*
* Construct a new {@link SpringTestRunner}. During construction, we will load the test data, and then we will create a list
* of {@link EasyTestRunner}. each instance of {@link DataDrivenTestRunner} in the list will correspond to a single method
* in the Test Class under test.<br>
* The algorithm is as follows:<br>
* <ul>
* <li>STEP 1: Load the test data. This will also do the check whether there exists a {@link DataLoader} annotation
* at the class level</li>
* <li>Iterate over each method.<br>
* For each method:
* <ol>
* <li>If method has {@link DataLoader} annotation, it means that there is test data associated with the test
* method.<br>
* In such a case add the method to the methodsWithData List.
* <li>If method does not have a {@link DataLoader} annotation, then:
* <ol>
* <li>Check if there already exists data for the method. This is possible as the data could have been loaded at the
* class level.<br>
* <li>If the data for the given method exists, add the method to the methodsWithData List.
* <li>If the data does not exists for the given test method, put it aside in a list of unused methods,
* </ol>
* </ol>
* Iteration over each method ends.<br>
*
* Finally create an instance of {@link EasyTestRunner} and make it use all the different types of methods we identified.<br>
* We need to identify methods with data and methods with no data primarily to group the test methods together
* as well as to efficiently create new test methods for each method that has test data associated with it.
* This whole process will happen for each of the test class that is part of the Suite.
*
* @param klass the test class
* @throws InitializationError if an initializationError occurs
*/
@SuppressWarnings("unchecked")
public SpringTestRunner(Class<?> klass) throws InitializationError {
super(klass, Collections.EMPTY_LIST);
Class<?> testClass = getTestClass().getJavaClass();
// Load the data at the class level, if any.
loadData(klass, null, testClass);
List<FrameworkMethod> availableMethods = getTestClass().getAnnotatedMethods(Test.class);
List<FrameworkMethod> methodsWithNoData = new ArrayList<FrameworkMethod>();
List<FrameworkMethod> methodsWithData = new ArrayList<FrameworkMethod>();
for (FrameworkMethod method : availableMethods) {
// Try loading the data if any at the method level
if (method.getAnnotation(DataLoader.class) != null) {
methodsWithData.add(method);
} else {
// Method does not have its own dataloader annotation
// Does method have data already loaded at the class level?
boolean methodDataLoaded = isMethodDataLoaded(DataConverter.getFullyQualifiedTestName(method.getName(),
testClass));
if (methodDataLoaded) {
methodsWithData.add(method);
} else {
methodsWithNoData.add(method);
}
}
}
// Finally create a runner for methods that do not have Data specified with them.
// These are potentially the methods with no method parameters and with @Test annotation.
if (!methodsWithNoData.isEmpty()) {
this.methodsWithNoData = methodsWithNoData;
}
if (!methodsWithData.isEmpty()) {
this.methodsWithData = methodsWithData;
}
runners.add(new EasyTestRunner(klass));
}
/**
* Returns a {@link Statement}: We override this method as it was being called twice
* for the same class. Looks like a bug in JUnit.
*/
@Override
protected Statement withBeforeClasses(Statement statement) {
return statement;
}
/**
* Check if the data for the given method is loaded or not.
*
* @param methodName the name of the method whose data needs to be checked.
* @return true if there exists data for the given method, else false.
*/
protected boolean isMethodDataLoaded(String methodName) {
boolean result = false;
if (DataContext.getData() == null || DataContext.getData().keySet() == null
|| DataContext.getData().keySet().isEmpty()) {
result = false;
} else {
Iterator<String> keyIterator = DataContext.getData().keySet().iterator();
while (keyIterator.hasNext()) {
result = methodName.equals(keyIterator.next()) ? true : false;
if (result) {
break;
}
}
}
return result;
}
/**
* Load the Data for the given class or method. This method will try to find {@link DataLoader} on either the class
* level or the method level. In case the annotation is found, this method will load the data using the specified
* loader class and then save it in the DataContext for further use by the system. We also create another copy of
* the input test data that we store in the {@link SpringTestRunner#writableData} field. This is done in order to
* facilitate the writing of the data that might be returned by the test method.
*
* @param testClass the class object, if any.
* @param method current executing method, if any.
* @param currentTestClass the currently executing test class. this is used to append in front of the method name to
* get unique method names as there could be methods in different classes with the same name and thus we
* want to avoid conflicts.
*/
protected void loadData(Class<?> testClass, FrameworkMethod method, Class<?> currentTestClass) {
if (testClass == null && method == null) {
Assert
.fail("The framework should provide either the testClass parameter or the method parameter in order to load the test data.");
}
// We give priority to Class Loading and then to method loading
DataLoader testData = null;
if (testClass != null) {
testData = testClass.getAnnotation(DataLoader.class);
} else {
testData = method.getAnnotation(DataLoader.class);
}
if (testData != null) {
determineLoader(testData);
if (dataLoader == null) {
Assert.fail("The framework currently does not support the specified Loader type. "
+ "You can provide the custom Loader by choosing LoaderType.CUSTOM in TestData "
+ "annotation and providing your custom loader using DataLoader annotation.");
} else {
Map<String, List<Map<String, Object>>> data = dataLoader.loadData(dataFiles);
// We also maintain the copy of the actual data for our write functionality.
writableData.putAll(data);
DataContext.setData(DataConverter.appendClassName(data, currentTestClass));
DataContext.setConvertedData(DataConverter.convert(data, currentTestClass));
}
}
}
/**
* Method that determines the right Loader and the right Data Files for the "write output data" functionality
* supported by the EasyTest Framework.
* @param testData an instance of {@link DataLoader} that helps in identifying the right {@link Loader} to write the data back to the file.
*/
private void determineLoader(DataLoader testData) {
dataFiles = testData.filePaths();
LoaderType loaderType = testData.loaderType();
// Loader
dataLoader = null;
if (LoaderType.CUSTOM.equals(loaderType)) {
PARAM_LOG.info("User specified to use custom Loader. Trying to get the custom loader.");
if (testData.loader() == null) {
Assert.fail("Specified the LoaderType as CUSTOM but did not specify loader"
+ " attribute. A loaderType of CUSTOM requires the loader " + "attribute specifying "
+ "the Custom Loader Class which implements Loader interface.");
} else {
try {
Class<? extends Loader> loaderClass = testData.loader();
dataLoader = loaderClass.newInstance();
} catch (Exception e) {
throw new RuntimeException("Exception occured while trying to instantiate a class of type :"
+ testData.loader(), e);
}
}
} else if (dataFiles.length == 0) {
// No files specified, implies user wants to load data with
// custom loader
if (testData.loader() == null) {
Assert.fail("Specified the LoaderType as CUSTOM but did not specify loader"
+ " attribute. A loaderType of CUSTOM requires the loader " + "attribute specifying "
+ "the Custom Loader Class which implements Loader interface.");
} else {
try {
Class<? extends Loader> loaderClass = testData.loader();
dataLoader = loaderClass.newInstance();
} catch (Exception e) {
throw new RuntimeException("Exception occured while trying to instantiate a class of type :"
+ testData.loader(), e);
}
}
} else {
// user has specified data files and the data fileType is also
// not custom.
dataLoader = LoaderFactory.getLoader(loaderType);
}
}
}