diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java index c4bf0a54dc4b..3bde1c73875f 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java @@ -44,6 +44,7 @@ import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.aot.AotApplicationContextInitializer; +import org.springframework.core.KotlinDetector; import org.springframework.core.Ordered; import org.springframework.core.PriorityOrdered; import org.springframework.core.SpringVersion; @@ -65,6 +66,7 @@ import org.springframework.test.context.support.TestPropertySourceUtils; import org.springframework.test.context.web.WebMergedContextConfiguration; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -153,6 +155,15 @@ private Method getMainMethod(MergedContextConfiguration mergedConfig, UseMainMet "Cannot use main method as no @SpringBootConfiguration-annotated class is available"); Method mainMethod = (springBootConfiguration != null) ? ReflectionUtils.findMethod(springBootConfiguration, "main", String[].class) : null; + if (mainMethod == null && KotlinDetector.isKotlinPresent()) { + try { + Class kotlinClass = ClassUtils.forName(springBootConfiguration.getName() + "Kt", + springBootConfiguration.getClassLoader()); + mainMethod = ReflectionUtils.findMethod(kotlinClass, "main", String[].class); + } + catch (ClassNotFoundException ex) { + } + } Assert.state(mainMethod != null || useMainMethod == UseMainMethod.WHEN_AVAILABLE, () -> "Main method not found on '%s'".formatted(springBootConfiguration.getName())); return mainMethod; diff --git a/spring-boot-project/spring-boot-test/src/test/kotlin/org/springframework/boot/test/context/KotlinApplicationWithMainThrowingException.kt b/spring-boot-project/spring-boot-test/src/test/kotlin/org/springframework/boot/test/context/KotlinApplicationWithMainThrowingException.kt new file mode 100644 index 000000000000..b1a2adbc673f --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/kotlin/org/springframework/boot/test/context/KotlinApplicationWithMainThrowingException.kt @@ -0,0 +1,15 @@ +package org.springframework.boot.test.context + +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.runApplication +import org.springframework.context.annotation.Configuration + +@Configuration(proxyBeanMethods = false) +@SpringBootConfiguration +open class KotlinApplicationWithMainThrowingException { +} + +fun main(args: Array) { + runApplication(*args) + throw IllegalStateException("ThrownFromMain") +} diff --git a/spring-boot-project/spring-boot-test/src/test/kotlin/org/springframework/boot/test/context/SpringBootContextLoaderKotlinTests.kt b/spring-boot-project/spring-boot-test/src/test/kotlin/org/springframework/boot/test/context/SpringBootContextLoaderKotlinTests.kt new file mode 100644 index 000000000000..30557548e44c --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/kotlin/org/springframework/boot/test/context/SpringBootContextLoaderKotlinTests.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.context + +import org.assertj.core.api.Assertions.assertThatIllegalStateException +import org.junit.jupiter.api.Test +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.test.context.SpringBootTest.UseMainMethod +import org.springframework.context.annotation.Configuration +import org.springframework.test.context.TestContext +import org.springframework.test.context.TestContextManager + +/** + * Kotlin tests for [SpringBootContextLoader]. + */ +class SpringBootContextLoaderKotlinTests { + + @Test + fun `when UseMainMethod ALWAYS and main method throws exception`() { + val testContext = ExposedTestContextManager( + UseMainMethodAlwaysAndKotlinMainMethodThrowsException::class.java + ).exposedTestContext + assertThatIllegalStateException().isThrownBy { testContext.applicationContext } + .havingCause() + .withMessageContaining("ThrownFromMain") + } + + /** + * [TestContextManager] which exposes the [TestContext]. + */ + internal class ExposedTestContextManager(testClass: Class<*>) : TestContextManager(testClass) { + val exposedTestContext: TestContext + get() = super.getTestContext() + } + + @SpringBootTest(classes = [KotlinApplicationWithMainThrowingException::class], useMainMethod = UseMainMethod.ALWAYS) + internal class UseMainMethodAlwaysAndKotlinMainMethodThrowsException + +} \ No newline at end of file