I tried to use Kotest with Spring, but I only foun...
# kotest
h
I tried to use Kotest with Spring, but I only found the
AnnotationSpec
working well.
Copy code
@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.
Copy code
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
}
e
Normally you would inject it in the constructor. With
@MockkBean
, you need to specify the annotation target as well, for instance:
Copy code
class FooTest(
  @field:MockkBean val myMockService: Service
): StringSpec({
   // ...
})
I haven't used
@LocalServerPort
, but try adding it in the same place, and try around with different annotation targets until you figure out what's needed 🙂
h
@Emil Kantis I know the constructor injection should work when registering
SpringAutowireConstructorExtension
, 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.
e
Can you share a repo w/ reproducer?
e
It seems Spring mutates the
@LocalServerPort
field during runtime. You need to define it as a field rather than constructor parameter. Example:
Copy code
@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()
        }
    }
}
You can have it a constructor param as well, but when you use the short-form of defining specs, what ends up happening is that you capture the default value of the param for the entire spec, rather than looking up the field. If you try adding any other field to an actual body, you can see that you can't actually access those fields from the lambda passed to the
FunSpec
constructor.
I.e this also works:
Copy code
@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()
        }
    }
}
But if you try to do this, you'll see that port is unresolved in the lambda:
Copy code
@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
}
h
This style works
Copy code
class DemoApplicationFuncSpec(
    @field:LocalServerPort var port: Int = 8080
) : FunSpec(){
    init {}}
The last one I have tried before, not working. The
port
can not be accessed in testing codes.
It seems Spring mutates the
@LocalServerPort
field during runtime. You need to define it as a field rather than constructor parameter
We use this annoation to get the runtime randam port number instead of the fixed initial number.
e
Yes, but it seems to not be intialized when we construct the spec, but probably made available later just before running the first test.. if you remove the default value you'll see there's no value available for construction
h
Copy code
@field:LocalServerPort var port: Int?
And restore the test
FunTest({...})
and got the error:
Copy code
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
e
If you print what port is used in
beforeEach
, what is actually used now?
h
Copy code
@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.
Copy code
@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
Copy code
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
Need spring to assign a random port at test runtime.