Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
custom listeners that should be registered when executing tests (default: none)
custom listeners that should be registered when executing tests (default: "")

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in #9.

+
`testng.verbose` (integer)::
TestNG's level of verbosity (default: 0)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
TestNG's level of verbosity (default: 0)
TestNG's level of verbosity (default: `0`)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in #9.


Expand Down Expand Up @@ -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+"]
----
<project>
<!-- ... -->
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>{surefire-version}</version>
<configuration>
<properties>
<configurationParameters>
testng.listeners = com.acme.MyCustomListener1, com.acme.MyCustomListener2
</configurationParameters>
</properties>
</configuration>
</plugin>
</plugins>
</build>
<!-- ... -->
</project>
----
====


== Limitations

Expand Down
6 changes: 5 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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<Test>("testFixturesJUnitPlatform_${versionSuffix}") {
javaLauncher.set(java8Launcher)
Expand All @@ -153,6 +156,7 @@ tasks {
useJUnitPlatform {
includeEngines("testng")
}
systemProperty("testng.listeners", "example.listeners.SystemPropertyProvidingListener")
testLogging {
events = EnumSet.allOf(TestLogEvent::class.java)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -108,6 +111,8 @@ public TestDescriptor discover(EngineDiscoveryRequest request, UniqueId uniqueId
* <dd>the output directory for reports (default: "test-output")</dd>
* <dt>{@code testng.useDefaultListeners} (boolean)</dt>
* <dd>whether TestNG's default report generating listeners should be used (default: {@code false})</dd>
* <dt>{@code testng.listeners} (comma-separated list of fully-qualified class names)</dt>
* <dd>custom listeners that should be registered when executing tests (default: none)</dd>
* <dt>{@code testng.verbose} (integer)</dt>
* <dd>TestNG's level of verbosity (default: 0)</dd>
* </dl>
Expand Down Expand Up @@ -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) //
Copy link

@l0rinc l0rinc Jul 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #9, I've delegating the parsing of the parameter to TestNG so I don't think we have to test these cases anymore as it now behaves consistently with how TestNG handles -listener on the command line.

.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));
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()));
}
}
Original file line number Diff line number Diff line change
@@ -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 {
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}