Skip to content

Instantly share code, notes, and snippets.

@joeyslalom
Last active February 12, 2020 17:30
Show Gist options
  • Save joeyslalom/f7ecee85c28cd7001057880f899e9660 to your computer and use it in GitHub Desktop.
Save joeyslalom/f7ecee85c28cd7001057880f899e9660 to your computer and use it in GitHub Desktop.
spring boot junit test runner
@SpringBootApplication
class BitsApp
fun main(args: Array<String>) {
val context = SpringApplicationBuilder().initializers(
beans {
bean {
Junit5Runner(listOf(
FirstTests::class.java,
VaultTests::class.java
))
}
}
).sources(BitsApp::class.java).run(*args);
System.exit(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 run(args: ApplicationArguments?) {
val selectors = testClasses.map { DiscoverySelectors.selectClass(it) }.toTypedArray()
val request = LauncherDiscoveryRequestBuilder.request()
.selectors(*selectors)
.build()
val launcher = LauncherFactory.create()
val testPlan = launcher.discover(request)
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")
}
override fun getExitCode(): Int = exitCode
// 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

joeyslalom commented Feb 10, 2020

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