A TestNG user recently requested an interesting feature: method priorities.
The idea is to be able to assign a priority to a test method (or an entire class) to guarantee that it will be run before other methods with a higher priority. Note that this is slightly different from dependent methods, since it’s just about ordering: if a method with a priority 2 fails, methods with higher priorities will still be run.
As it turns out, this is very easy to implement with one of TestNG 5.8 newest features: method interceptors.
TestNG lets you declare a class implementing the interface IMethodInterceptor, whose signature is:
public interface IMethodInterceptor extends ITestNGListener { List<IMethodInstance> intercept(List<IMethodInstance> methods, ITestContext context); }
TestNG passes the list of methods it’s about to run so you get a chance to reorder them. Implementing method priorities with this interface is very simple.
Let’s start with what we want this feature to look like. In the following example, I declare three kinds of priorities:
- Priorities at the method level.
- A priority at the class level.
- No priority. When no priority is defined on a method, we want to look for a priority on the class first, and if none can be found, the priority will receive a default value of 0.
Here is our test:
@Priority(10) public class PriorityTest { @Test public void b1() { System.out.println("Default priority");} @Priority(-3) @Test public void a1() { System.out.println("Priority -3");} @Priority(-3) @Test public void a2() { System.out.println("Priority -3");} @Priority(3) @Test public void c1() { System.out.println("Priority 3");} @Priority(3) @Test public void c2() { System.out.println("Priority 3");} @Priority(4) @Test public void d1() { System.out.println("Priority 4");} }
Next, we implement the @Priority annotation:
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface Priority { int value() default 0; }
Fairly straightforward: we make the annotation valid on methods and types only, and we use the shortcut value() annotation so we can specify @Priority(1).
Now comes the implementation of the IMethodInterceptor:
public class PriorityInterceptor implements IMethodInterceptor { public List<IMethodInstance> intercept(List<IMethodInstance> methods, ITestContext context) { Comparator<IMethodInstance> comparator = new Comparator<IMethodInstance>() { private int getPriority(IMethodInstance mi) { int result = 0; Method method = mi.getMethod().getMethod(); Priority a1 = method.getAnnotation(Priority.class); if (a1 != null) { result = a1.value(); } else { Class> cls = method.getDeclaringClass(); Priority classPriority = cls.getAnnotation(Priority.class); if (classPriority != null) { result = classPriority.value(); } } return result; } public int compare(IMethodInstance m1, IMethodInstance m2) { return getPriority(m1) - getPriority(m2); } }; IMethodInstance[] array = methods.toArray(new IMethodInstance[methods.size()]); Arrays.sort(array, comparator); return Arrays.asList(array); }
The heart of this implementation is our Comparator, which tries to find a @Priority annotation the method it receives in parameter. If it can’t find any, it looks on the declaring class, and failing to find one there as well, assigns it a default value. The intercept() method simply sorts the methods with this comparator and returns it.
Now, let’s run our test without using that interceptor:
<suite name="Example"> <test name="Method interceptor example" > <classes> <class name="priority.PriorityTest" /> </classes> </test> </suite>
As expected, the output is fairly random:
Priority 3 Default priority Priority 3 Priority -3 Priority -3 Priority 4
Now we declare our interceptor in the testng.xml file:
<suite name="Example"> <listeners> <listener class-name="priority.PriorityInterceptor" /> </listeners> <test name="Method interceptor example" > <classes> <class name="priority.PriorityTest" /> </classes> </test> </suite>
And we get the expected outcome, with our method without any @Priority receiving the class priority (10) since it didn’t define any itself:
Priority -3 Priority -3 Priority 3 Priority 3 Priority 4 Default priority
To verify that our class priority implementation works, we can remove the @Priority class annotation. In this case, our method without any @Priority should receive a priority of 0:
Priority -3 Priority -3 Default priority Priority 3 Priority 3 Priority 4
As you can see, Method Interceptors allow you to replace TestNG’s ordering of test methods with your own. It’s particularly powerful if used in conjunction with annotations, as shown in this example, but it’s also possible to imagine other external configuration factors to customize TestNG’s running engine to your liking.