Skip to content

Commit 23603e6

Browse files
authored
Add support for registering custom listeners (#7)
The new `testng.listeners` configuration parameter now allows to register custom listeners as a comma-separated list of fully-qualified class names. Fixes #5.
1 parent 64aef98 commit 23603e6

File tree

7 files changed

+218
-1
lines changed

7 files changed

+218
-1
lines changed

README.adoc

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ the output directory for reports (default: "test-output")
127127
`testng.useDefaultListeners` (boolean)::
128128
whether TestNG's default report generating listeners should be used (default: `false`)
129129
+
130+
`testng.listeners` (comma-separated list of fully-qualified class names)::
131+
custom listeners that should be registered when executing tests (default: none)
132+
+
130133
`testng.verbose` (integer)::
131134
TestNG's level of verbosity (default: 0)
132135

@@ -192,6 +195,60 @@ tasks.test {
192195
----
193196
====
194197

198+
=== Registering custom listeners
199+
200+
.Console Launcher
201+
[%collapsible]
202+
====
203+
[source]
204+
----
205+
$ java -cp 'lib/*' org.junit.platform.console.ConsoleLauncher \
206+
-cp bin/main -cp bin/test \
207+
--include-engine=testng --scan-classpath=bin/test \
208+
--config=testng.listeners=com.acme.MyCustomListener1,com.acme.MyCustomListener2
209+
----
210+
====
211+
212+
.Gradle
213+
[%collapsible]
214+
====
215+
[source,kotlin,subs="attributes+"]
216+
.build.gradle[.kts]
217+
----
218+
tasks.test {
219+
useJUnitPlatform()
220+
systemProperty("testng.listeners", "com.acme.MyCustomListener1, com.acme.MyCustomListener2")
221+
}
222+
----
223+
====
224+
225+
.Maven
226+
[%collapsible]
227+
====
228+
[source,xml,subs="attributes+"]
229+
----
230+
<project>
231+
<!-- ... -->
232+
<build>
233+
<plugins>
234+
<plugin>
235+
<artifactId>maven-surefire-plugin</artifactId>
236+
<version>{surefire-version}</version>
237+
<configuration>
238+
<properties>
239+
<configurationParameters>
240+
testng.listeners = com.acme.MyCustomListener1, com.acme.MyCustomListener2
241+
</configurationParameters>
242+
</properties>
243+
</configuration>
244+
</plugin>
245+
</plugins>
246+
</build>
247+
<!-- ... -->
248+
</project>
249+
----
250+
====
251+
195252

196253
== Limitations
197254

build.gradle.kts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ dependencies {
9090

9191
testImplementation("org.junit.jupiter:junit-jupiter")
9292
testImplementation("org.junit.platform:junit-platform-testkit")
93+
testImplementation("org.mockito:mockito-junit-jupiter:3.11.2")
9394
testImplementation("org.apache.maven:maven-artifact:3.8.1") {
9495
because("ComparableVersion is used to reason about tested TestNG version")
9596
}
@@ -144,7 +145,9 @@ tasks {
144145
javaLauncher.set(java8Launcher)
145146
classpath = configuration + sourceSets.testFixtures.get().output
146147
testClassesDirs = sourceSets.testFixtures.get().output
147-
useTestNG()
148+
useTestNG {
149+
listeners.add("example.listeners.SystemPropertyProvidingListener")
150+
}
148151
}
149152
register<Test>("testFixturesJUnitPlatform_${versionSuffix}") {
150153
javaLauncher.set(java8Launcher)
@@ -153,6 +156,7 @@ tasks {
153156
useJUnitPlatform {
154157
includeEngines("testng")
155158
}
159+
systemProperty("testng.listeners", "example.listeners.SystemPropertyProvidingListener")
156160
testLogging {
157161
events = EnumSet.allOf(TestLogEvent::class.java)
158162
}

src/main/java/org/junit/support/testng/engine/TestNGTestEngine.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212

1313
import static org.testng.internal.RuntimeBehavior.TESTNG_MODE_DRYRUN;
1414

15+
import java.util.Arrays;
1516
import java.util.List;
1617

18+
import org.junit.platform.commons.JUnitException;
19+
import org.junit.platform.commons.support.ReflectionSupport;
1720
import org.junit.platform.engine.ConfigurationParameters;
1821
import org.junit.platform.engine.EngineDiscoveryRequest;
1922
import org.junit.platform.engine.EngineExecutionListener;
@@ -108,6 +111,8 @@ public TestDescriptor discover(EngineDiscoveryRequest request, UniqueId uniqueId
108111
* <dd>the output directory for reports (default: "test-output")</dd>
109112
* <dt>{@code testng.useDefaultListeners} (boolean)</dt>
110113
* <dd>whether TestNG's default report generating listeners should be used (default: {@code false})</dd>
114+
* <dt>{@code testng.listeners} (comma-separated list of fully-qualified class names)</dt>
115+
* <dd>custom listeners that should be registered when executing tests (default: none)</dd>
111116
* <dt>{@code testng.verbose} (integer)</dt>
112117
* <dd>TestNG's level of verbosity (default: 0)</dd>
113118
* </dl>
@@ -208,6 +213,18 @@ void configure(TestNG testNG, ConfigurationParameters config) {
208213
testNG.setVerbose(config.get("testng.verbose", Integer::valueOf).orElse(0));
209214
testNG.setUseDefaultListeners(config.getBoolean("testng.useDefaultListeners").orElse(false));
210215
config.get("testng.outputDirectory").ifPresent(testNG::setOutputDirectory);
216+
config.get("testng.listeners").ifPresent(listeners -> Arrays.stream(listeners.split(",")) //
217+
.map(ReflectionSupport::tryToLoadClass) //
218+
.map(result -> result.getOrThrow(
219+
cause -> new JUnitException("Failed to load custom listener class", cause))) //
220+
.map(listenerClass -> {
221+
if (!ITestNGListener.class.isAssignableFrom(listenerClass)) {
222+
throw new JUnitException("Custom listener class must implement "
223+
+ ITestNGListener.class.getName() + ": " + listenerClass.getName());
224+
}
225+
return (ITestNGListener) ReflectionSupport.newInstance(listenerClass);
226+
}) //
227+
.forEach(testNG::addListener));
211228
}
212229
};
213230

src/test/java/org/junit/support/testng/engine/ReportingIntegrationTests.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
import example.basics.TimeoutTestCase;
4343
import example.configuration.FailingBeforeClassConfigurationMethodTestCase;
4444
import example.dataproviders.DataProviderMethodTestCase;
45+
import example.listeners.SystemPropertyProvidingListener;
46+
import example.listeners.SystemPropertyReadingTestCase;
4547

4648
import org.junit.jupiter.api.Test;
4749
import org.junit.jupiter.params.ParameterizedTest;
@@ -302,4 +304,19 @@ void reportsParallelInvocations() {
302304
event(container("method:test()"), finishedSuccessfully()), //
303305
event(testClass(testClass), finishedSuccessfully()));
304306
}
307+
308+
@Test
309+
void registersCustomListeners() {
310+
var testClass = SystemPropertyReadingTestCase.class;
311+
312+
var results = testNGEngine() //
313+
.selectors(selectClass(testClass)) //
314+
.configurationParameter("testng.listeners", SystemPropertyProvidingListener.class.getName()).execute();
315+
316+
results.allEvents().debug().assertEventsMatchLooselyInOrder( //
317+
event(testClass(testClass), started()), //
318+
event(test("method:test()"), started()), //
319+
event(test("method:test()"), finishedSuccessfully()), //
320+
event(testClass(testClass), finishedSuccessfully()));
321+
}
305322
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.support.testng.engine;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
15+
import static org.mockito.Mockito.when;
16+
import static org.mockito.quality.Strictness.LENIENT;
17+
18+
import java.util.Optional;
19+
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.platform.engine.ConfigurationParameters;
22+
import org.junit.support.testng.engine.TestNGTestEngine.Phase;
23+
import org.mockito.Mock;
24+
import org.mockito.junit.jupiter.MockitoSettings;
25+
import org.testng.TestNG;
26+
27+
@MockitoSettings(strictness = LENIENT)
28+
public class TestNGTestEngineTest {
29+
30+
TestNG testNG = new TestNG();
31+
32+
@Test
33+
void configuresListenersFromConfigurationParameter(@Mock ConfigurationParameters configurationParameters) {
34+
when(configurationParameters.get("testng.listeners")) //
35+
.thenReturn(Optional.of(MyTestListener.class.getName() + " , " + AnotherTestListener.class.getName()));
36+
37+
Phase.EXECUTION.configure(testNG, configurationParameters);
38+
39+
assertThat(testNG.getTestListeners()) //
40+
.hasAtLeastOneElementOfType(MyTestListener.class) //
41+
.hasAtLeastOneElementOfType(AnotherTestListener.class);
42+
}
43+
44+
@Test
45+
void throwsExceptionForMissingClasses(@Mock ConfigurationParameters configurationParameters) {
46+
when(configurationParameters.get("testng.listeners")) //
47+
.thenReturn(Optional.of("acme.MissingClass"));
48+
49+
assertThatThrownBy(() -> Phase.EXECUTION.configure(testNG, configurationParameters)) //
50+
.hasMessage("Failed to load custom listener class") //
51+
.hasRootCauseExactlyInstanceOf(ClassNotFoundException.class) //
52+
.hasRootCauseMessage("acme.MissingClass");
53+
}
54+
55+
@Test
56+
void throwsExceptionForClassesOfWrongType(@Mock ConfigurationParameters configurationParameters) {
57+
when(configurationParameters.get("testng.listeners")) //
58+
.thenReturn(Optional.of(Object.class.getName()));
59+
60+
assertThatThrownBy(() -> Phase.EXECUTION.configure(testNG, configurationParameters)) //
61+
.hasMessage("Custom listener class must implement org.testng.ITestNGListener: java.lang.Object");
62+
}
63+
64+
static class MyTestListener extends DefaultListener {
65+
}
66+
67+
static class AnotherTestListener extends DefaultListener {
68+
}
69+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package example.listeners;
12+
13+
import org.testng.IClassListener;
14+
import org.testng.ITestClass;
15+
16+
public class SystemPropertyProvidingListener implements IClassListener {
17+
18+
public static final String SYSTEM_PROPERTY_KEY = "test.class";
19+
20+
@Override
21+
public void onBeforeClass(ITestClass testClass) {
22+
System.setProperty(SYSTEM_PROPERTY_KEY, testClass.getName());
23+
}
24+
25+
@Override
26+
public void onAfterClass(ITestClass testClass) {
27+
System.clearProperty(SYSTEM_PROPERTY_KEY);
28+
}
29+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package example.listeners;
12+
13+
import static example.listeners.SystemPropertyProvidingListener.SYSTEM_PROPERTY_KEY;
14+
import static org.testng.Assert.assertEquals;
15+
16+
import org.testng.annotations.Test;
17+
18+
public class SystemPropertyReadingTestCase {
19+
20+
@Test
21+
public void test() {
22+
assertEquals(System.getProperty(SYSTEM_PROPERTY_KEY), SystemPropertyReadingTestCase.class.getName());
23+
}
24+
}

0 commit comments

Comments
 (0)