Skip to content

Instantly share code, notes, and snippets.

@joeyslalom
Last active February 14, 2020 18:45
Show Gist options
  • Save joeyslalom/e93f3e2fbea929fe59248f8a24e145ee to your computer and use it in GitHub Desktop.
Save joeyslalom/e93f3e2fbea929fe59248f8a24e145ee to your computer and use it in GitHub Desktop.
Running JUnit5 as an application
@SpringBootApplication
public class IntegrationTestApp {
public static void main(String[] args) {
ConfigurableApplicationContext appContext = new SpringApplicationBuilder()
.initializers((ApplicationContextInitializer<GenericApplicationContext>) context ->
context.registerBean(Junit5Runner.class,
() -> new Junit5Runner(ExplicitlyDeclaredTest.class))
)
.sources(IntegrationTestApp.class)
.run(args);
System.exit(SpringApplication.exit(appContext));
}
private static class Junit5Runner implements ApplicationRunner, ExitCodeGenerator {
private static final Logger LOG = LoggerFactory.getLogger(Junit5Runner.class);
private final List<Class<?>> testClasses;
private int exitCode = 0;
private Junit5Runner(Class<?>... testClasses) {
this.testClasses = Arrays.asList(testClasses);
}
@Override
public void run(ApplicationArguments args) {
DiscoverySelector[] selectors = testClasses.stream()
.map(DiscoverySelectors::selectClass)
.collect(Collectors.toList())
.toArray(new DiscoverySelector[testClasses.size()]);
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(selectors)
.build();
Launcher launcher = LauncherFactory.create();
logTests(launcher, request);
SummaryGeneratingListener listener = new SummaryGeneratingListener();
LoggingListener logListener = LoggingListener.forBiConsumer((t, ss) -> {
String msg = ss.get();
if (t == null) {
LOG.info("test message={}", msg);
} else {
LOG.error("test error message={}", msg, t);
}
});
launcher.registerTestExecutionListeners(listener, logListener);
launcher.execute(request);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(stream, StandardCharsets.UTF_8));
if (listener.getSummary().getTotalFailureCount() > 0) {
listener.getSummary().printFailuresTo(writer);
LOG.error("JUnit Failures={}", stream);
exitCode = 2;
}
listener.getSummary().printTo(writer);
LOG.info("JUnit Summary={}", stream);
}
// log tests found for the request, optional
private static void logTests(Launcher launcher, LauncherDiscoveryRequest request) {
TestPlan testPlan = launcher.discover(request);
testPlan.getRoots().stream()
.flatMap(testId -> testPlan.getDescendants(testId).stream())
.filter(testId -> testId.getType() == TestDescriptor.Type.TEST)
.forEach(testId ->
LOG.info("uniqueId={} displayName={}", testId.getUniqueId(), testId.getDisplayName()));
}
@Override
public int getExitCode() {
return exitCode;
}
}
}
@SpringBootApplication
class IntegrationTestApp
fun main(args: Array<String>) {
val context = SpringApplicationBuilder().initializers(
beans {
bean {
Junit5Runner(listOf(IntegrationTests::class.java))
}
}
).sources(IntegrationTestApp::class.java).run(*args);
exitProcess(SpringApplication.exit(context))
}
class Junit5Runner(private val testClasses: List<Class<*>>) : ApplicationRunner, ExitCodeGenerator {
private val log = LoggerFactory.getLogger(Junit5Runner::class.java)
private var exitCode = 0
override fun getExitCode(): Int = exitCode
override fun run(args: ApplicationArguments?) {
val selectors = testClasses.map { DiscoverySelectors.selectClass(it) }.toTypedArray()
val request = LauncherDiscoveryRequestBuilder.request()
.selectors(*selectors)
.build()
val launcher = LauncherFactory.create()
logTests(launcher, request)
val listener = SummaryGeneratingListener()
val logListener = LoggingListener.forBiConsumer { t, u ->
val msg = u.get()
if (t == null) {
log.info("test message=$msg")
} else {
log.error("test error message=$msg", t)
}
}
launcher.registerTestExecutionListeners(listener, logListener)
launcher.execute(request)
val stream = ByteArrayOutputStream()
val writer = PrintWriter(stream)
if (listener.summary.totalFailureCount > 0) {
listener.summary.printFailuresTo(writer)
log.error("Junit Failures:$stream")
exitCode = 2
}
listener.summary.printTo(writer)
log.info("Junit Summary:$stream")
}
// logs tests found for the request, optional
private fun logTests(launcher: Launcher, request: LauncherDiscoveryRequest) {
val testPlan = launcher.discover(request)
testPlan.roots
.flatMap { testPlan.getDescendants(it) }
.filter { it.type == TestDescriptor.Type.TEST }
.forEach {
log.info("uniqueId=${it.uniqueId} displayName=${it.displayName}")
}
}
}
@joeyslalom
Copy link
Author

When creating integration tests, JUnit5 works great; developers are familiar with its constructs (e.g., @Before) and its included assertions. Currently @SpringBootTest run well in the context of Maven or Gradle, so test classes can leverage Spring @Beans.

My goal is to continue using these tests as another application - outside of a build tool, and to package as a Spring uber jar. This simple Spring Boot app is my solution, which basically replicates JUnit5's ConsoleLauncher.

Implementation notes:

  • Junit5Runner is not a @Component because this will cause Spring to recurse
  • Tests classes must be put in src/main, NOT src/test. Otherwise, the Spring Boot plugin will not include them in the classpath of the bootJar (BOOT-INF/classes). Also:
  • Must manually specify the test classes to Junit5Runner. Hence, they also cannot be in src/test. By explicitly naming the classes, they are loaded into the classpath and allows the JUnit5 Launcher to discover them.
    • I think this is also due to the packaging with the uber jar; e.g., I was able to get DiscoverySelectors.selectPackage working when run by ./gradlew bootRun, but no classes are found when run via java -jar
  • To include the junit-engine, the dependency must be included as implementation rather than testImplementation

Resources:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment