diff --git a/README.adoc b/README.adoc
index a479f0f..021ced2 100644
--- a/README.adoc
+++ b/README.adoc
@@ -127,6 +127,9 @@ the output directory for reports (default: "test-output")
`testng.useDefaultListeners` (boolean)::
whether TestNG's default report generating listeners should be used (default: `false`)
+
+`testng.listeners` (comma-separated list of fully-qualified class names)::
+custom listeners that should be registered when executing tests (default: none)
++
`testng.verbose` (integer)::
TestNG's level of verbosity (default: 0)
@@ -192,6 +195,60 @@ tasks.test {
----
====
+=== Registering custom listeners
+
+.Console Launcher
+[%collapsible]
+====
+[source]
+----
+$ java -cp 'lib/*' org.junit.platform.console.ConsoleLauncher \
+ -cp bin/main -cp bin/test \
+ --include-engine=testng --scan-classpath=bin/test \
+ --config=testng.listeners=com.acme.MyCustomListener1,com.acme.MyCustomListener2
+----
+====
+
+.Gradle
+[%collapsible]
+====
+[source,kotlin,subs="attributes+"]
+.build.gradle[.kts]
+----
+tasks.test {
+ useJUnitPlatform()
+ systemProperty("testng.listeners", "com.acme.MyCustomListener1, com.acme.MyCustomListener2")
+}
+----
+====
+
+.Maven
+[%collapsible]
+====
+[source,xml,subs="attributes+"]
+----
+
+
+
+
+
+ maven-surefire-plugin
+ {surefire-version}
+
+
+
+ testng.listeners = com.acme.MyCustomListener1, com.acme.MyCustomListener2
+
+
+
+
+
+
+
+
+----
+====
+
== Limitations
diff --git a/build.gradle.kts b/build.gradle.kts
index dc8b85c..4adb5a2 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -90,6 +90,7 @@ dependencies {
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.junit.platform:junit-platform-testkit")
+ testImplementation("org.mockito:mockito-junit-jupiter:3.11.2")
testImplementation("org.apache.maven:maven-artifact:3.8.1") {
because("ComparableVersion is used to reason about tested TestNG version")
}
@@ -144,7 +145,9 @@ tasks {
javaLauncher.set(java8Launcher)
classpath = configuration + sourceSets.testFixtures.get().output
testClassesDirs = sourceSets.testFixtures.get().output
- useTestNG()
+ useTestNG {
+ listeners.add("example.listeners.SystemPropertyProvidingListener")
+ }
}
register("testFixturesJUnitPlatform_${versionSuffix}") {
javaLauncher.set(java8Launcher)
@@ -153,6 +156,7 @@ tasks {
useJUnitPlatform {
includeEngines("testng")
}
+ systemProperty("testng.listeners", "example.listeners.SystemPropertyProvidingListener")
testLogging {
events = EnumSet.allOf(TestLogEvent::class.java)
}
diff --git a/src/main/java/org/junit/support/testng/engine/TestNGTestEngine.java b/src/main/java/org/junit/support/testng/engine/TestNGTestEngine.java
index 204eb91..257824b 100644
--- a/src/main/java/org/junit/support/testng/engine/TestNGTestEngine.java
+++ b/src/main/java/org/junit/support/testng/engine/TestNGTestEngine.java
@@ -12,8 +12,11 @@
import static org.testng.internal.RuntimeBehavior.TESTNG_MODE_DRYRUN;
+import java.util.Arrays;
import java.util.List;
+import org.junit.platform.commons.JUnitException;
+import org.junit.platform.commons.support.ReflectionSupport;
import org.junit.platform.engine.ConfigurationParameters;
import org.junit.platform.engine.EngineDiscoveryRequest;
import org.junit.platform.engine.EngineExecutionListener;
@@ -108,6 +111,8 @@ public TestDescriptor discover(EngineDiscoveryRequest request, UniqueId uniqueId
* the output directory for reports (default: "test-output")
* {@code testng.useDefaultListeners} (boolean)
* whether TestNG's default report generating listeners should be used (default: {@code false})
+ * {@code testng.listeners} (comma-separated list of fully-qualified class names)
+ * custom listeners that should be registered when executing tests (default: none)
* {@code testng.verbose} (integer)
* TestNG's level of verbosity (default: 0)
*
@@ -208,6 +213,18 @@ void configure(TestNG testNG, ConfigurationParameters config) {
testNG.setVerbose(config.get("testng.verbose", Integer::valueOf).orElse(0));
testNG.setUseDefaultListeners(config.getBoolean("testng.useDefaultListeners").orElse(false));
config.get("testng.outputDirectory").ifPresent(testNG::setOutputDirectory);
+ config.get("testng.listeners").ifPresent(listeners -> Arrays.stream(listeners.split(",")) //
+ .map(ReflectionSupport::tryToLoadClass) //
+ .map(result -> result.getOrThrow(
+ cause -> new JUnitException("Failed to load custom listener class", cause))) //
+ .map(listenerClass -> {
+ if (!ITestNGListener.class.isAssignableFrom(listenerClass)) {
+ throw new JUnitException("Custom listener class must implement "
+ + ITestNGListener.class.getName() + ": " + listenerClass.getName());
+ }
+ return (ITestNGListener) ReflectionSupport.newInstance(listenerClass);
+ }) //
+ .forEach(testNG::addListener));
}
};
diff --git a/src/test/java/org/junit/support/testng/engine/ReportingIntegrationTests.java b/src/test/java/org/junit/support/testng/engine/ReportingIntegrationTests.java
index ceed051..244c47f 100644
--- a/src/test/java/org/junit/support/testng/engine/ReportingIntegrationTests.java
+++ b/src/test/java/org/junit/support/testng/engine/ReportingIntegrationTests.java
@@ -42,6 +42,8 @@
import example.basics.TimeoutTestCase;
import example.configuration.FailingBeforeClassConfigurationMethodTestCase;
import example.dataproviders.DataProviderMethodTestCase;
+import example.listeners.SystemPropertyProvidingListener;
+import example.listeners.SystemPropertyReadingTestCase;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -302,4 +304,19 @@ void reportsParallelInvocations() {
event(container("method:test()"), finishedSuccessfully()), //
event(testClass(testClass), finishedSuccessfully()));
}
+
+ @Test
+ void registersCustomListeners() {
+ var testClass = SystemPropertyReadingTestCase.class;
+
+ var results = testNGEngine() //
+ .selectors(selectClass(testClass)) //
+ .configurationParameter("testng.listeners", SystemPropertyProvidingListener.class.getName()).execute();
+
+ results.allEvents().debug().assertEventsMatchLooselyInOrder( //
+ event(testClass(testClass), started()), //
+ event(test("method:test()"), started()), //
+ event(test("method:test()"), finishedSuccessfully()), //
+ event(testClass(testClass), finishedSuccessfully()));
+ }
}
diff --git a/src/test/java/org/junit/support/testng/engine/TestNGTestEngineTest.java b/src/test/java/org/junit/support/testng/engine/TestNGTestEngineTest.java
new file mode 100644
index 0000000..06c087c
--- /dev/null
+++ b/src/test/java/org/junit/support/testng/engine/TestNGTestEngineTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.support.testng.engine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.when;
+import static org.mockito.quality.Strictness.LENIENT;
+
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+import org.junit.platform.engine.ConfigurationParameters;
+import org.junit.support.testng.engine.TestNGTestEngine.Phase;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.testng.TestNG;
+
+@MockitoSettings(strictness = LENIENT)
+public class TestNGTestEngineTest {
+
+ TestNG testNG = new TestNG();
+
+ @Test
+ void configuresListenersFromConfigurationParameter(@Mock ConfigurationParameters configurationParameters) {
+ when(configurationParameters.get("testng.listeners")) //
+ .thenReturn(Optional.of(MyTestListener.class.getName() + " , " + AnotherTestListener.class.getName()));
+
+ Phase.EXECUTION.configure(testNG, configurationParameters);
+
+ assertThat(testNG.getTestListeners()) //
+ .hasAtLeastOneElementOfType(MyTestListener.class) //
+ .hasAtLeastOneElementOfType(AnotherTestListener.class);
+ }
+
+ @Test
+ void throwsExceptionForMissingClasses(@Mock ConfigurationParameters configurationParameters) {
+ when(configurationParameters.get("testng.listeners")) //
+ .thenReturn(Optional.of("acme.MissingClass"));
+
+ assertThatThrownBy(() -> Phase.EXECUTION.configure(testNG, configurationParameters)) //
+ .hasMessage("Failed to load custom listener class") //
+ .hasRootCauseExactlyInstanceOf(ClassNotFoundException.class) //
+ .hasRootCauseMessage("acme.MissingClass");
+ }
+
+ @Test
+ void throwsExceptionForClassesOfWrongType(@Mock ConfigurationParameters configurationParameters) {
+ when(configurationParameters.get("testng.listeners")) //
+ .thenReturn(Optional.of(Object.class.getName()));
+
+ assertThatThrownBy(() -> Phase.EXECUTION.configure(testNG, configurationParameters)) //
+ .hasMessage("Custom listener class must implement org.testng.ITestNGListener: java.lang.Object");
+ }
+
+ static class MyTestListener extends DefaultListener {
+ }
+
+ static class AnotherTestListener extends DefaultListener {
+ }
+}
diff --git a/src/testFixtures/java/example/listeners/SystemPropertyProvidingListener.java b/src/testFixtures/java/example/listeners/SystemPropertyProvidingListener.java
new file mode 100644
index 0000000..972be78
--- /dev/null
+++ b/src/testFixtures/java/example/listeners/SystemPropertyProvidingListener.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package example.listeners;
+
+import org.testng.IClassListener;
+import org.testng.ITestClass;
+
+public class SystemPropertyProvidingListener implements IClassListener {
+
+ public static final String SYSTEM_PROPERTY_KEY = "test.class";
+
+ @Override
+ public void onBeforeClass(ITestClass testClass) {
+ System.setProperty(SYSTEM_PROPERTY_KEY, testClass.getName());
+ }
+
+ @Override
+ public void onAfterClass(ITestClass testClass) {
+ System.clearProperty(SYSTEM_PROPERTY_KEY);
+ }
+}
diff --git a/src/testFixtures/java/example/listeners/SystemPropertyReadingTestCase.java b/src/testFixtures/java/example/listeners/SystemPropertyReadingTestCase.java
new file mode 100644
index 0000000..a5cad97
--- /dev/null
+++ b/src/testFixtures/java/example/listeners/SystemPropertyReadingTestCase.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package example.listeners;
+
+import static example.listeners.SystemPropertyProvidingListener.SYSTEM_PROPERTY_KEY;
+import static org.testng.Assert.assertEquals;
+
+import org.testng.annotations.Test;
+
+public class SystemPropertyReadingTestCase {
+
+ @Test
+ public void test() {
+ assertEquals(System.getProperty(SYSTEM_PROPERTY_KEY), SystemPropertyReadingTestCase.class.getName());
+ }
+}