Method Interceptors
Once TestNG has calculated in what order the test methods will be invoked, these methods are split in two groups:
-
Methods run sequentially. These are all the test methods that have dependencies or dependents. These methods will be run in a specific order.
-
Methods run in no particular order. These are all the methods that don’t belong in the first category. The order in which these test methods are run is random and can vary from one run to the next (although by default, TestNG will try to group test methods by class).
In order to give you more control on the methods that belong to the second category, TestNG provides {javadocs-base-url}/org/testng/IMethodInterceptor.html[org.testng.IMethodInterceptor]
The list of methods passed in parameters are all the methods that can be run in any order. Your intercept method is expected to return a similar list of IMethodInstance
, which can be either of the following:
-
The same list you received in parameter but in a different order.
-
A smaller list of
IMethodInstance
objects. -
A bigger list of
IMethodInstance
objects.
Once you have defined your interceptor, you pass it to TestNG as a listener. For example:
java -classpath "testng-jdk15.jar:test/build" org.testng.TestNG -listener test.methodinterceptors.NullMethodInterceptor
-testclass test.methodinterceptors.FooTest
For the equivalent ant syntax, see the listeners attribute in the ant documentation.
For example, here is a Method Interceptor that will reorder the methods so that test methods that belong to the group "fast" are always run first:
public List<IMethodInstance> intercept(List<IMethodInstance> methods, ITestContext context) {
List<IMethodInstance> result = new ArrayList<IMethodInstance>();
for (IMethodInstance m : methods) {
Test test = m.getMethod().getConstructorOrMethod().getAnnotation(Test.class);
Set<String> groups = new HashSet<String>();
for (String group : test.groups()) {
groups.add(group);
}
if (groups.contains("fast")) {
result.add(0, m);
}
else {
result.add(m);
}
}
return result;
}
Interceptors for Data Providers
Once TestNG has invoked your data provider, it basically has the data set that is required to be fed via multiple iterations to a @Test
annotated test method.
But there can be situations wherein you may want to modify the data set before the data set is fed to the test method.
This is where the data provider aware interceptors come into picture.
TestNG provides a listener named {javadocs-base-url}/org/testng/IDataProviderInterceptor.html[IDataProviderInterceptor] which lets you do this.
Here’s a sample implementation.
import org.testng.IDataProviderInterceptor;
import org.testng.IDataProviderMethod;
import org.testng.ITestContext;
import org.testng.ITestNGMethod;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
public class SampleDataInterceptor implements IDataProviderInterceptor {
@Override
public Iterator<Object[]> intercept(
Iterator<Object[]> original, IDataProviderMethod dataProviderMethod,
ITestNGMethod method, ITestContext iTestContext) {
// The test method would have custom attributes. From them look for a custom attribute
// whose name is "filter". It's value would the fully qualified class name that
// can be instantiated using reflection and then used to filter the data provider
// provided data set.
Optional<String> found = Arrays.stream(method.getAttributes())
.filter(it -> "filter".equalsIgnoreCase(it.name()))
.flatMap(it -> Arrays.stream(it.values()))
.findFirst();
if (found.isPresent()) {
String clazzName = found.get();
Predicate<Object> predicate = predicate(clazzName);
Spliterator<Object[]> split = Spliterators.spliteratorUnknownSize(original, Spliterator.ORDERED);
return StreamSupport.stream(split,false)
.filter(predicate)
.collect(Collectors.toList())
.iterator();
}
return original;
}
private static Predicate<Object> predicate(String clazzName) {
try {
return (Predicate<Object>) Class.forName(clazzName).getDeclaredConstructor().newInstance();
} catch (Exception e) {
return input -> true;
}
}
}
Here’s how the filtering capability can look like. This filtering would basically prune through the data set and only allow even numbers to be used.
import java.util.function.Predicate;
public class MyFilter implements Predicate<Object> {
@Override
public boolean test(Object object) {
if (object.getClass().isArray()) {
Object number = ((Object[]) object)[0];
return (Integer) number % 2 == 0;
}
return false;
}
}
Here’s how a test case that consumes this listener can look like:
import org.testng.annotations.CustomAttribute;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(SampleDataInterceptor.class)
public class SampleDataDrivenTestCase {
@Test(dataProvider = "numbers",
attributes = {
@CustomAttribute(name = "filter", values = {"org.testng.demo.MyFilter" })
}
)
public void passingTest(int i) {
System.err.println("Value = " + i);
}
@DataProvider(name = "numbers")
public Object[][] getNumbers() {
return new Object[][]{{1}, {2}, {3}, {4}};
}
}
Here’s how the execution output can look like:
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Value = 2
Value = 4
===============================================
Default Suite
Total tests run: 2, Passes: 2, Failures: 0, Skips: 0
===============================================