hantsy
06/29/2025, 1:49 AMAnnotationSpec
working well.
@Import(TestcontainersConfiguration::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class DemoApplicationAnnotationSpec : AnnotationSpec() {
@LocalServerPort
private var port: Int = 8080
lateinit var client: WebClient
@BeforeEach
fun setUp() {
client = WebClient.create("<http://localhost>:$port")
}
@Test
fun `get all posts`() {
client.get()
.uri("/posts")
.accept(MediaType.APPLICATION_JSON)
.exchangeToFlux {
assertThat(it.statusCode()).isEqualTo(HttpStatus.OK)
it.bodyToFlux(Post::class.java)
}
.test()
.expectNextCount(2)
.verifyComplete()
}
}
How to convert this to use StringSpec
FuncSpec
,etc., especially I do not know where to use the Spring special annotation to inject beans, such as LocalServerPort
, MockkBean
, etc.
I have tried to use LocalServerPort in the test class body, but it can not be accessed in the test block.
xxxStringSpec:StringSpec({
//1. @LocalServerPort is NOT allowed here
//2. The port injected in the class body(below) cannot be accessed here.
}){
@LocalServerPort
private var port: Int = 8080
}
Emil Kantis
06/29/2025, 7:42 AM@MockkBean
, you need to specify the annotation target as well, for instance:
class FooTest(
@field:MockkBean val myMockService: Service
): StringSpec({
// ...
})
Emil Kantis
06/29/2025, 7:42 AM@LocalServerPort
, but try adding it in the same place, and try around with different annotation targets until you figure out what's needed 🙂hantsy
06/30/2025, 4:59 AMSpringAutowireConstructorExtension
, but here I have tried use @field:LocalServerPort var port: Int = 8080
in the top level class constructor, it does not work, it always return 8080 in the testing codes, which will fail the test running.Emil Kantis
06/30/2025, 5:34 AMhantsy
06/30/2025, 5:44 AMEmil Kantis
06/30/2025, 6:25 AM@LocalServerPort
field during runtime. You need to define it as a field rather than constructor parameter. Example:
@Import(TestcontainersConfiguration::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class DemoApplicationFuncSpec : FunSpec() {
@LocalServerPort
var port: Int = 8080
lateinit var client: WebClient
init {
beforeEach {
client = WebClient.create("<http://localhost>:$port")
}
test("get all posts") {
client.get()
.uri("/posts")
.accept(MediaType.APPLICATION_JSON)
.exchangeToFlux {
assertThat(it.statusCode()).isEqualTo(HttpStatus.OK)
it.bodyToFlux(Post::class.java)
}
.test()
.expectNextCount(2)
.verifyComplete()
}
}
}
Emil Kantis
06/30/2025, 6:30 AMFunSpec
constructor.Emil Kantis
06/30/2025, 6:30 AM@Import(TestcontainersConfiguration::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class DemoApplicationFuncSpec(
@field:LocalServerPort var port: Int = 8080,
) : FunSpec() {
init {
lateinit var client: WebClient
beforeEach {
client = WebClient.create("<http://localhost>:$port")
}
test("get all posts") {
client.get()
.uri("/posts")
.accept(MediaType.APPLICATION_JSON)
.exchangeToFlux {
assertThat(it.statusCode()).isEqualTo(HttpStatus.OK)
it.bodyToFlux(Post::class.java)
}
.test()
.expectNextCount(2)
.verifyComplete()
}
}
}
Emil Kantis
06/30/2025, 6:31 AM@Import(TestcontainersConfiguration::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class DemoApplicationFuncSpec : FunSpec(
{
lateinit var client: WebClient
beforeEach {
client = WebClient.create("<http://localhost>:$port")
}
test("get all posts") {
client.get()
.uri("/posts")
.accept(MediaType.APPLICATION_JSON)
.exchangeToFlux {
assertThat(it.statusCode()).isEqualTo(HttpStatus.OK)
it.bodyToFlux(Post::class.java)
}
.test()
.expectNextCount(2)
.verifyComplete()
}
}
) {
@LocalServerPort
var port: Int = 8080
}
hantsy
06/30/2025, 6:54 AMclass DemoApplicationFuncSpec(
@field:LocalServerPort var port: Int = 8080
) : FunSpec(){
init {}}
hantsy
06/30/2025, 6:54 AMport
can not be accessed in testing codes.hantsy
06/30/2025, 7:04 AMIt seems Spring mutates theWe use this annoation to get the runtime randam port number instead of the fixed initial number.field during runtime. You need to define it as a field rather than constructor parameter@LocalServerPort
Emil Kantis
06/30/2025, 7:07 AMhantsy
06/30/2025, 7:17 AMCopy code@field:LocalServerPort var port: Int?
And restore the test
FunTest({...})
and got the error:
tensions.ExtensionException$BeforeEachException: org.springframework.web.util.InvalidUrlException: Bad authority
at io.kotest.engine.test.TestExtensions$beforeTestBeforeAnyBeforeContainer$errors$2$2.invoke(TestExtensions.kt:81)
at io.kotest.engine.test.TestExtensions$beforeTestBeforeAnyBeforeContainer$errors$2$2.invoke(TestExtensions.kt:81)
at io.kotest.common.ResultsKt.mapError(results.kt:17)
at io.kotest.engine.test.TestExtensions.beforeTestBeforeAnyBeforeContainer-gIAlu-s(TestExtensions.kt:81)
at io.kotest.engine.test.interceptors.LifecycleInterceptor.intercept(LifecycleInterceptor.kt:47)
at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:100)
at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
at io.kotest.engine.test.interceptors.TestCaseExtensionInterceptor$intercept$2.invokeSuspend(TestCaseExtensionInterceptor.kt:24)
at io.kotest.engine.test.interceptors.TestCaseExtensionInterceptor$intercept$2.invoke(TestCaseExtensionInterceptor.kt)
at io.kotest.engine.test.interceptors.TestCaseExtensionInterceptor$intercept$2.invoke(TestCaseExtensionInterceptor.kt)
at io.kotest.engine.test.TestExtensions$intercept$execute$1$1$1.invokeSuspend(TestExtensions.kt:143)
at io.kotest.engine.test.TestExtensions$intercept$execute$1$1$1.invoke(TestExtensions.kt)
at io.kotest.engine.test.TestExtensions$intercept$execute$1$1$1.invoke(TestExtensions.kt)
at io.kotest.extensions.spring.SpringTestExtension.intercept(SpringTestExtension.kt:73)
at io.kotest.engine.test.TestExtensions$intercept$execute$1$1.invokeSuspend(TestExtensions.kt:140)
at io.kotest.engine.test.TestExtensions$intercept$execute$1$1.invoke(TestExtensions.kt)
at io.kotest.engine.test.TestExtensions$intercept$execute$1$1.invoke(TestExtensions.kt)
at io.kotest.engine.test.TestExtensions$intercept$execute$1$1$1.invokeSuspend(TestExtensions.kt:143)
at io.kotest.engine.test.TestExtensions$intercept$execute$1$1$1.invoke(TestExtensions.kt)
at io.kotest.engine.test.TestExtensions$intercept$execute$1$1$1.invoke(TestExtensions.kt)
at io.kotest.extensions.spring.SpringTestExtension.intercept(SpringTestExtension.kt:73)
at io.kotest.engine.test.TestExtensions$intercept$execute$1$1.invokeSuspend(TestExtensions.kt:140)
at io.kotest.engine.test.TestExtensions$intercept$execute$1$1.invoke(TestExtensions.kt)
at io.kotest.engine.test.TestExtensions$intercept$execute$1$1.invoke(TestExtensions.kt)
at io.kotest.engine.test.TestExtensions.intercept(TestExtensions.kt:148)
at io.kotest.engine.test.interceptors.TestCaseExtensionInterceptor.intercept(TestCaseExtensionInterceptor.kt:24)
at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:100)
at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
at io.kotest.engine.test.interceptors.BeforeSpecListenerInterceptor$intercept$runTest$1$4$1.invokeSuspend(BeforeSpecListenerInterceptor.kt:50)
at io.kotest.engine.test.interceptors.BeforeSpecListenerInterceptor$intercept$runTest$1$4$1.invoke(BeforeSpecListenerInterceptor.kt)
at io.kotest.engine.test.interceptors.BeforeSpecListenerInterceptor$intercept$runTest$1$4$1.invoke(BeforeSpecListenerInterceptor.kt)
at io.kotest.engine.test.interceptors.BeforeSpecListenerInterceptor.intercept(BeforeSpecListenerInterceptor.kt:60)
at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:100)
at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
at io.kotest.engine.test.interceptors.TestEnabledCheckInterceptor.intercept(TestEnabledCheckInterceptor.kt:31)
at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:100)
at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
at io.kotest.engine.test.interceptors.CoroutineErrorCollectorInterceptor$intercept$3.invokeSuspend(CoroutineErrorCollectorInterceptor.kt:34)
at io.kotest.engine.test.interceptors.CoroutineErrorCollectorInterceptor$intercept$3.invoke(CoroutineErrorCollectorInterceptor.kt)
at io.kotest.engine.test.interceptors.CoroutineErrorCollectorInterceptor$intercept$3.invoke(CoroutineErrorCollectorInterceptor.kt)
at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:61)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:163)
at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
at io.kotest.engine.test.interceptors.CoroutineErrorCollectorInterceptor.intercept(CoroutineErrorCollectorInterceptor.kt:33)
at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:100)
at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
at io.kotest.engine.test.interceptors.CoroutineDispatcherFactoryInterceptor$intercept$4.invokeSuspend(coroutineDispatcherFactoryInterceptor.kt:57)
at io.kotest.engine.test.interceptors.CoroutineDispatcherFactoryInterceptor$intercept$4.invoke(coroutineDispatcherFactoryInterceptor.kt)
at io.kotest.engine.test.interceptors.CoroutineDispatcherFactoryInterceptor$intercept$4.invoke(coroutineDispatcherFactoryInterceptor.kt)
at io.kotest.engine.concurrency.FixedThreadCoroutineDispatcherFactory$withDispatcher$4.invokeSuspend(FixedThreadCoroutineDispatcherFactory.kt:59)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith$$$capture(ContinuationImpl.kt:33)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: org.springframework.web.util.InvalidUrlException: Bad authority
Emil Kantis
06/30/2025, 7:19 AMbeforeEach
, what is actually used now?hantsy
06/30/2025, 7:27 AM@Import(TestcontainersConfiguration::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class DemoApplicationFuncSpec(
@field:LocalServerPort var port: Int?
) : FunSpec({
lateinit var client: WebClient
beforeEach {
println("beforeEach:::local server port:$port")
client = WebClient.create("<http://localhost>:$port")
}
test("get all posts") {
client.get()
.uri("/posts")
.accept(MediaType.APPLICATION_JSON)
.exchangeToFlux {
assertThat(it.statusCode()).isEqualTo(HttpStatus.OK)
it.bodyToFlux(Post::class.java)
}
.test()
.expectNextCount(2)
.verifyComplete()
}
})
Print null and failed.
@Import(TestcontainersConfiguration::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class DemoApplicationFuncSpec(
@field:LocalServerPort var port: Int?
) : FunSpec() {
init {
lateinit var client: WebClient
beforeEach {
println("beforeEach:::local server port:$port")
client = WebClient.create("<http://localhost>:$port")
}
test("get all posts") {
client.get()
.uri("/posts")
.accept(MediaType.APPLICATION_JSON)
.exchangeToFlux {
assertThat(it.statusCode()).isEqualTo(HttpStatus.OK)
it.bodyToFlux(Post::class.java)
}
.test()
.expectNextCount(2)
.verifyComplete()
}
}
}
Print beforeEach:::local server port:53518
and worked well. Here we use
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
Need spring to assign a random port at test runtime.