package com.softwaremill.common.test.util.reorder; import com.google.common.base.Joiner; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Lists; import org.testng.IMethodInstance; import org.testng.IMethodInterceptor; import org.testng.ITestContext; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Set; /** * TestNG Listener modifying order in which test methods are executed in each class. It uses @FirstTest, @LastTest and @TestOrder annotations. * <p/> * Beware that not all methods and classes can be reordered, only those which don't have any methods depended upon * (for more see javadoc for org.testng.IMethodInterceptor) * * Example ordering in single test class when this listener is used: * - method marked @FirstTest * - method marked @TestOrder(order = 1) * - method marked @TestOrder(order = 2) * - method marked@TestOrder(order = 3) * - test method without @FirstTest / @LastTest / @TestOrder annotations * - method marked @LastTest * * * @author Tomasz Dziurko */ public class TestNGReorderingListener implements IMethodInterceptor { public List<IMethodInstance> intercept(List<IMethodInstance> methods, ITestContext context) { ArrayListMultimap<Class, IMethodInstance> methodsInClass = distributeMethodsByClass(methods); validate(methodsInClass); return reorder(methodsInClass); } ArrayListMultimap<Class, IMethodInstance> distributeMethodsByClass(List<IMethodInstance> methods) { ArrayListMultimap<Class, IMethodInstance> methodsInClass = ArrayListMultimap.create(); for (IMethodInstance method : methods) { methodsInClass.put(method.getMethod().getRealClass(), method); } return methodsInClass; } private void validate(ArrayListMultimap<Class, IMethodInstance> methodsInClass) { List<String> problemsList = checkForInvalidUsagesInClasses(methodsInClass.keySet()); problemsList = checkForInvalidUsagesInMethods(problemsList, methodsInClass); if (problemsList.size() > 0) { throw new IllegalStateException("Invalid usage of " + TestNGReorderingListener.class.getSimpleName() + ". Problems:\n - " + Joiner.on("\n - ").join(problemsList)); } } List<String> checkForInvalidUsagesInClasses(Set<Class> classes) { List<String> classesAnnotatedWithFirstTest = Lists.newArrayList(); for (Class clazz : classes) { if(clazz.isAnnotationPresent(FirstTest.class)) { classesAnnotatedWithFirstTest.add(clazz.getSimpleName()); } } List<String> problemsList = Lists.newArrayList(); if(classesAnnotatedWithFirstTest.size() > 1) { problemsList.add("Classes " + Joiner.on(", ").join(classesAnnotatedWithFirstTest) + " are annotated with " + FirstTest.class.getSimpleName() +". Only one class can be annotated this way."); } return problemsList; } private List<String> checkForInvalidUsagesInMethods(List<String> problemsList, ArrayListMultimap<Class, IMethodInstance> methodsInClass) { for (Class classWithTests : methodsInClass.keySet()) { problemsList = checkMethodsInClass(classWithTests, methodsInClass.get(classWithTests), problemsList); } return problemsList; } List<String> checkMethodsInClass(Class classWithTests, List<IMethodInstance> methodsInClass, List<String> problemsList) { int firstTestCounter = 0; int lastTestCounter = 0; int dependsOnMethodsCounter = 0; for (IMethodInstance method : methodsInClass) { if (isAnnotationPresent(method, FirstTest.class) && isAnnotationPresent(method, LastTest.class)) { problemsList.add(classWithTests.getSimpleName() + ": single method can not be annotated with both " + FirstTest.class.getSimpleName() + " and " + LastTest.class.getSimpleName()); } if (isAnnotationPresent(method, FirstTest.class) && isAnnotationPresent(method, TestOrder.class)) { problemsList.add(classWithTests.getSimpleName() + ": single method can not be annotated with both " + FirstTest.class.getSimpleName() + " and " + TestOrder.class.getSimpleName()); } if (isAnnotationPresent(method, LastTest.class) && isAnnotationPresent(method, TestOrder.class)) { problemsList.add(classWithTests.getSimpleName() + ": single method can not be annotated with both " + LastTest.class.getSimpleName() + " and " + TestOrder.class.getSimpleName()); } if (isAnnotationPresent(method, FirstTest.class)) { firstTestCounter++; } if (isAnnotationPresent(method, LastTest.class)) { lastTestCounter++; } if (method.getMethod().getMethodsDependedUpon().length > 0) { dependsOnMethodsCounter++; } } return updateProblemsListIfNeeded(problemsList, classWithTests, firstTestCounter, lastTestCounter, dependsOnMethodsCounter); } private boolean isAnnotationPresent(IMethodInstance method, Class annotationClass) { return method.getMethod().getMethod().isAnnotationPresent(annotationClass); } private List<String> updateProblemsListIfNeeded(List<String> problemsList, Class classWithTests, int firstTestCounter, int lastTestCounter, int dependsOnMethodsCounter) { if (firstTestCounter > 1) { problemsList.add(classWithTests.getSimpleName() + ": more than one method marked with annotation " + FirstTest.class.getSimpleName()); } if (lastTestCounter > 1) { problemsList.add(classWithTests.getSimpleName() + ": more than one method marked with annotation " + LastTest.class.getSimpleName()); } if (firstTestCounter > 0 && dependsOnMethodsCounter > 0) { problemsList.add(classWithTests.getSimpleName() + ": " + FirstTest.class.getSimpleName() + " annotation can not be used in method with dependsOnMethods from @Test"); } return problemsList; } List<IMethodInstance> reorder(ArrayListMultimap<Class, IMethodInstance> methodsInClass) { methodsInClass = reorderMethodsInClasses(methodsInClass); ArrayList<IMethodInstance> reorderedMethodsList = reorderClasses(methodsInClass); return reorderedMethodsList; } private ArrayListMultimap<Class, IMethodInstance> reorderMethodsInClasses(ArrayListMultimap<Class, IMethodInstance> methodsInClass) { Comparator<IMethodInstance> comparator = createMethodComparator(); for (Class classWithTests : methodsInClass.keySet()) { Collections.sort(methodsInClass.get(classWithTests), comparator); } return methodsInClass; } private Comparator<IMethodInstance> createMethodComparator() { return new Comparator<IMethodInstance>() { public int compare(IMethodInstance methodOne, IMethodInstance methodTwo) { if(isAnnotationPresent(methodOne, FirstTest.class)) { return -1; } else if(isAnnotationPresent(methodTwo, FirstTest.class)) { return 1; } else if(isAnnotationPresent(methodOne, LastTest.class)) { return 1; } else if(isAnnotationPresent(methodTwo, LastTest.class)) { return -1; } else if(isAnnotationPresent(methodOne, TestOrder.class) && isAnnotationPresent(methodTwo, TestOrder.class)) { return extractOrderFromMethod(methodOne) < extractOrderFromMethod(methodTwo) ? -1 : 1; } else if(isAnnotationPresent(methodOne, TestOrder.class)) { return -1; } else if(isAnnotationPresent(methodTwo, TestOrder.class)) { return 1; } else { return 0; } } }; } private int extractOrderFromMethod(IMethodInstance methodOne) { return methodOne.getMethod().getMethod().getAnnotation(TestOrder.class).order(); } private ArrayList<IMethodInstance> reorderClasses(ArrayListMultimap<Class, IMethodInstance> methodsInClass) { List<Class> classesWithTests = Lists.newArrayList(methodsInClass.keySet()); Collections.sort(classesWithTests, createClassComparator()); ArrayList<IMethodInstance> reorderedMethodsList = Lists.newArrayList(); for(Class clazz : classesWithTests) { reorderedMethodsList.addAll(methodsInClass.get(clazz)); } return reorderedMethodsList; } private Comparator<Class> createClassComparator() { return new Comparator<Class>() { public int compare(Class classOne, Class classTwo) { if(classOne.isAnnotationPresent(FirstTest.class)) { return -1; } else if(classTwo.isAnnotationPresent(FirstTest.class)) { return 1; } else { return 0; } } }; } }