TestNG. Life without XML: changing test discovery logic.

Eduard Dubilyer
3 min readFeb 6, 2022

--

After working few years with Junit5, I recently started supporting a client with an existing TestNG-based project. It’s a very interesting process to return to the tool you used before after trying something new.

One of the first problems I faced is that test method discovery by multiple groups works according the ‘OR’ logic. It means that if have 3 tests:

@Test(groups = {"t1"}){...}
@Test(groups = {"t1", "t2"}){...}
@Test(groups = {"t2"}){...}

running TestNG with system property groups=t1,t2 will cause the execution of all 3 tests. And for this project I required to run the intersection of groups (i.e. only the second test annotated with both t1 and t2).

When you start googling about it you will probably find a few examples of using the beanshell syntax inside the testng.xml file. But as you see from the subject, I have 101 reason not to use the configuration file.

So my choice is to implement a custom method selector.

Step 1: Implementation:

In order to avoid a confusion with the standard testNG logic, I decided not to use the groups system property, so I will use tags instead.

public class ExpressionSelector implements IMethodSelector {

@Override
public boolean includeMethod(
IMethodSelectorContext iMethodSelectorContext,
ITestNGMethod iTestNGMethod,
boolean b) {
Optional<String> tagInput =
Optional.ofNullable(System.getProperty("tags"));
return tagInput.isEmpty() ||
asList(iTestNGMethod.getGroups())
.containsAll(asList(tagInput.get().split("&")));
}
@Override
public void setTestMethods(List<ITestNGMethod> list) {
}
}

So as you see if tags is empty it will run all tests, otherwise it will select only ones tagged by all tags (splitted by ‘&’).

Step 2. Configuration.

We already have a selector, but how to make it work? There’s no much documentation about it. The only thing we can found is:

-methodselectors: A comma-separated list of Java classes and method priorities that define method selectors.Lets you specify method selectors on the command line. For example: com.example.Selector1:3,com.example.Selector2:2

But… priority? What value should we get? Ok, let’s see.

I prepared a simple test class:

public class ApiTests {
@Test(groups = {"t1"})
void passedTest() {
System.out.println("IT'S T1 TEST");
Assert.assertTrue(true);
}

@Test(groups = {"t1", "t2"})
void passedTest2() {
System.out.println("IT'S T1 and T2 TEST");
Assert.assertTrue(true);
}

@Test(groups = {"t2"})
void failedTest3() {
System.out.println("IT'S T2 TEST");
Assert.assertTrue(true);
}

Now running the testNG (I will use Intellij plugin) with following params:

-methodselectors com.skippersoft.testng.ExpressionSelector:1

-Dtags=t1&t2,

I expected to run only one test, annotated with both t1 and t2 groups, but the selector was ignored and all tests were executed.

Ok, it’s a debug time… After putting a breakepoint onto org.testng.internal.RunInfo#addMethodSelector, we can realize that priority of the standard XmlMethodSelector is 10. Ok, we’ll bit it:

  • methodselectors com.skippersoft.testng.ExpressionSelector:11

After executing I got the desired output:

IT'S T1 and T2 TEST===============================================
Default Suite
Total tests run: 1, Passes: 1, Failures: 0, Skips: 0
===============================================

Step 3. Execution.

Nice, but what if I want to run it using a Maven Surefire plugin? That’s the way I used to run this project on a CI server. Remember, I said that there’s no much documentation about using a command line key? Ok, so for Surefire case it’s not documented at all ( or I don’t know how to google).

The first thing that came to mind was to inject it using Surefire properties as I do it for listeners:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<properties>
<property>
<name>methodselectors</name>
<value>com.skippersoft.testng.ExpressionSelector:11</value>
</property>
</properties>
</configuration>
</plugin>

And… it worked!

Probably I achieved the behavior I wanted to implement. Running:

mvn test -Dtags=t1&t2

executes only tests annotated by both t1 and t2 groups. ( So in the real life I can run sanity&UI and it will execute only the required subset ).

What’s next?

Probably the selector can be improved to get a real logical expression. Nothing should block us from running (Service2|ApiGateway)&Sanity&!Flaky :)

PS. You can find the sample code on this github repository.

Other articles of this cycle:

--

--

Eduard Dubilyer
Eduard Dubilyer

Written by Eduard Dubilyer

Automation Architect, Infra and Backend developer, CI/CD enthusiast, technical dreamer.