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()); + } +}