I have the following project, still in development: https://github.com/TarekSaid/blotit, using Kotlin and Spring Webflux.
I was writing my unit tests with Spock (Groovy), but after some issues with testing Kotlin Coroutines and being unable to use syntactic sugar with data classes (even with #JvmOverloads), I've decided to switch to Kotest + Mockk.
My only issue now is with the handler unit tests' performance, as I have to use mockkstatic on ServerRequestExtensionsKt to mock request.awaitBodyOrNull. While the Spock specification runs in 0.072s, the equivalent Kotest test runs in 0.440s. While negligible, it could add up as I add more tests.
I was wondering if there was a better way to unit test the handler with Kotest (please note that I already use WebTestClient to run integration tests). I'll eventually add some verifications to check for service calls, etc, which will be mocked. That's why I'm testing the handler directly.
The handler itself is still very simple:
#Component
class RatingHandler {
suspend fun rate(request: ServerRequest): ServerResponse {
// DataSheet is a data class used as the request body
return request.awaitBodyOrNull(DataSheet::class)?.let {
ServerResponse.ok().buildAndAwait()
} ?: ServerResponse.badRequest().buildAndAwait()
}
}
Here's my test class:
class RatingHandlerTest : StringSpec({
val handler = RatingHandler()
val request: ServerRequest = mockk()
val sheet: DataSheet = mockk()
// tried to use beforeSpec to see if it'd make a difference
beforeSpec {
mockkStatic("org.springframework.web.reactive.function.server.ServerRequestExtensionsKt")
}
"rate should return status ok when the body is present" {
coEvery { request.awaitBodyOrNull(DataSheet::class) } returns sheet
handler.rate(request).statusCode() shouldBe HttpStatus.OK
}
"rate should return invalid request for missing sheet" {
coEvery { request.awaitBodyOrNull(DataSheet::class) } returns null
handler.rate(request).statusCode() shouldBe HttpStatus.BAD_REQUEST
}
})
Is that how I'm supposed to unit test the handler, or is there a better way?
Related
Problem:
I am using the vertx-web-graphql dependency to run a graphql-server and now I want to test that said server.
But the vertx-web-client testing always times out, even after the assertion that is just before the testContext.completeNow() does not fail.
Versions:
Kotlin (1.7.20)
vertx(core, graphql, postgres, web-client, vertx-junit5) (4.3.4)
Problem-Code:
#ExtendWith(VertxExtension::class)
class GraphQlVerticleTest {
#BeforeEach
fun `deploy GraphQL Verticle`(vertx: Vertx, testContext: VertxTestContext) {
vertx.deployVerticle(GraphQlVerticle(setupPostgreSqlClient(vertx), setupJWTAuthenticationProvider(vertx)),
testContext.succeedingThenComplete())
}
#Test
fun `Wrong Login Credentials cause Login Failed message`(vertx: Vertx, testContext: VertxTestContext) {
val webClient: WebClient = WebClient.create(vertx)
testContext.assertComplete(webClient.post(graphQLPort, graphQLHostName, graphQLURI)
.putHeader("Content-Type", "application/json")
.sendJsonObject(createLoginAttemptQueryAsJsonObject(wrongEmail, wrongPassword)))
.onComplete { asyncResult ->
assertThat(asyncResult.result().bodyAsString()).isEqualTo(loginFailedResponse)
println("hello I executed")
testContext.completeNow()
}
}
#AfterEach
fun `check that the Verticle is still there`(vertx: Vertx, testContext: VertxTestContext) {
assertThat(vertx.deploymentIDs()).isNotEmpty.hasSize(1)
}
}
The Verticle and some of the functions don't really matter, as the Assert Statement does not fail, but for some reason the testContext never gets completed and then the Test times out.
I used the testContext.assertComplete(...).onComplete(...) in all my other Vertx-Tests, but with the Web-Client it just does not work.
As far as I can tell, in this scenario the test-context has to be completed in different way, but I just can't figure out how.
Any help would be appreciated 🙂.
And please tell me if there is anything to make the question clearer, as I have never asked anything on StackOverflow before.
What I tried:
I tried placing the the assertion and testContext completion statement into a testContext.verify {} block, but that didn't work either. I read through the vertx-junit5 & vertx-web-client documentation, but there wasn't anything there concerning web-client testing. And my search didn't had any fruitful results either.
What I expected:
That the test would complete after the assertion statement succeeded, like it did in all my other vertx-tests.
Found the problem: you must complete the testContext in the method annotated with #AfterEach:
#AfterEach
fun `check that Verticle is still there`(vertx: Vertx, testContext: VertxTestContext) {
assertThat(vertx.deploymentIDs()).isNotEmpty.hasSize(1)
testContext.completeNow()
}
Or simply not inject it:
#AfterEach
fun `check that Verticle is still there`(vertx: Vertx) {
assertThat(vertx.deploymentIDs()).isNotEmpty.hasSize(1)
}
In both cases, the test passes.
Originally posted by #tsegismont in https://github.com/eclipse-vertx/vertx-junit5/issues/122#issuecomment-1315619893
I'm new to Kotlin Coroutines and Flows and unit testing them. I have a pretty simple test:
#Test
fun debounce(): Unit = runBlocking {
val state = MutableStateFlow("hello")
val debouncedState = state.debounce(500).stateIn(this, SharingStarted.Eagerly, "bla")
assertThat(debouncedState.value).isEqualTo("bla")
state.value = "good bye"
// not yet...
assertThat(debouncedState.value).isEqualTo("bla")
delay(600)
// now!
assertThat(debouncedState.value).isEqualTo("good bye")
// cannot close the state flows :(
cancel("DONE")
}
It works just fine (except that I cannot stop it, but that's a different issue).
Next, I want to test my ViewModel which includes the exact same state flows. It's basically the same code above, but I thought it should run in the same scope as viewModel.someMutableStateFlow so I tried to run it on viewModelScope:
#Test
fun debounce(): Unit = runBlocking {
val viewModel = MyViewModel()
// in view model the state and debouncedState are defined the same way as above
val state = viewModel.someMutableStateFlow
val debouncedState = state.debounce(500).stateIn(viewModel.viewModelScope, SharingStarted.Eagerly, "bla")
///////////////////////////////////////////////////
// Below is the same code as in previous example //
///////////////////////////////////////////////////
assertThat(debouncedState.value).isEqualTo("bla")
state.value = "good bye"
// not yet...
assertThat(debouncedState.value).isEqualTo("bla")
delay(600)
// now!
assertThat(debouncedState.value).isEqualTo("good bye")
// cannot close the state flows :(
cancel("DONE")
}
But this time the debouncedState.value is never changed, it stays bla all the time! Nothing is emitted from those states.
Does this have something to do with the fact that I am using viewModelScope and maybe it is not running?
Some explanation about what's going on here would be great.
From this documentation about setting the Main dispatcher:
However, some APIs such as viewModelScope use a hardcoded Main dispatcher under the hood.
It then describes how to replace the Main dispatcher with a TestDispatcher:
class HomeViewModelTest {
#Test
fun settingMainDispatcher() = runTest {
val testDispatcher = UnconfinedTestDispatcher(testScheduler)
Dispatchers.setMain(testDispatcher)
try {
val viewModel = HomeViewModel()
viewModel.loadMessage() // Uses testDispatcher, runs its coroutine eagerly
assertEquals("Greetings!", viewModel.message.value)
} finally {
Dispatchers.resetMain()
}
}
}
To quote the article:
If the Main dispatcher has been replaced with a TestDispatcher, any newly-created TestDispatchers will automatically use the scheduler from the Main dispatcher, including the StandardTestDispatcher created by runTest if no other dispatcher is passed to it.
The documentation also describes how to make a test rule so you don't have to do this in each test.
I am testing an event driven architecture in KTOR. My Core logic is held in a class that reacts to different Event types being emitted by a StateFlow. EventGenerators push Events into the StateFlow which are picked up by the Core.
However, when the Core attempts to respond to an ApplicationCall embedded in one of my Events I receive an ResponseAlreadySentException and I'm not sure why this would be the case. This does not happen if I bypass the StateFlow and call the Core class directly from the EventGenerator. I am not responding to ApplicationCalls anywhere else in my code, and have checked with breakpoints that the only .respond line is not being hit multiple times.
MyStateFlow class:
class MyStateFlow {
val state: StateFlow<CoreEvent>
get() = _state
private val _state = MutableStateFlow<CoreEvent>(CoreEvent.NothingEvent)
suspend fun update(event: CoreEvent) {
_state.value = event
}
}
My Core class:
class Core(
myStateFlow: MyStateFlow,
coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.IO
) {
init {
CoroutineScope(coroutineContext).launch {
myStateFlow.state.collect {
onEvent(it)
}
}
}
suspend fun onEvent(event: CoreEvent) {
when(event) {
is FooEvent {
event.call.respond(HttpStatusCode.OK, "bar")
}
...
}
}
}
One of my EventGenerators is a Route in my KTOR Application class:
get("/foo") {
myStateFlow.update(CoreEvent.FooEvent(call))
}
However, hitting /f00 in my browser returns either an ResponseAlreadySentException or an java.lang.UnsupportedOperationException with message: "Headers can no longer be set because response was already completed". The error response can flip between the two while I'm tinkering with different attempted solutions, but they seem to be saying the same thing: The call has already been responded to before I attempt to call call.respond(...).
If I change my Route instead to call the Core.onEvent() directly, hitting /foo returns "bar" in my browser as is the intended behaviour:
get("/foo") {
core.onEvent(CoreEvent.FooEvent(call))
}
For completeness, my dependency versions are:
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.10"
implementation "io.ktor:ktor-server-netty:1.4.1"
Thank you in advanced for any insight you can offer.
I've implemented a TestListener as follows:
object IntegrationTest: TestListener {
override fun beforeProject() {
println("integration tests - beforeProject")
}
override fun beforeSpec(description: Description, spec: Spec) {
println("integration tests - beforeSpec")
}
}
And used it in a test:
class SimpleTest: StringSpec() {
override fun listeners() = listOf(IntegrationTest)
init {
"it - 1" {
println("it - 1")
}
"it - 2" {
println("it - 2")
}
}
}
The problem is that integration tests - beforeProject is never printed in the output.
The result is:
integration tests - beforeSpec
it - 1
it - 2
I tried it in intellij and using gradle CLI. Am I missing something?
beforeProject has to run before any tests are discovered, otherwise it's not really before the project but would kind of be "before any tests have executed" (The difference might not be important in your use class, but KotlinTest mantains the distinction).
Therefore overriding that method in a listener that's added to a test class doesn't do anything (as you have seen).
So instead you need to add your listener to ProjectConfig which is project wide configuration. You do this by subclassing AbstractProjectConfig and putting it in a special package name, like this:
package io.kotlintest.provided
object ProjectConfig : AbstractProjectConfig() {
// add listeners here
}
See full docs here:
https://github.com/kotlintest/kotlintest/blob/master/doc/reference.md#project-config
I have been using Spring's WebFlux framework with Kotlin for about a month now, and have been loving it. As I got ready to make the dive into writing production code with WebFlux and Kotlin I found myself struggling to unit test my routers in a simple, lightweight way.
Spring Test is an excellent framework, however it is heavier weight than what I was wanting, and I was looking for a test framework that was more expressive than traditional JUnit. Something in the vein of JavaScript's Mocha. Kotlin's Spek fit the bill perfectly.
What follows below is an example of how I was able to unit test a simple router using Spek.
WebFlux defines an excellent DSL using Kotlin's Type-Safe Builders for building routers. While the syntax is very succinct and readable it is not readily apparent how to assert that the router function bean it returns is configured properly as its properties are mostly inaccessible to client code.
Say we have the following router:
#Configuration
class PingRouter(private val pingHandler: PingHandler) {
#Bean
fun pingRoute() = router {
accept(MediaType.APPLICATION_JSON).nest {
GET("/ping", pingHandler::handlePing)
}
}
}
We want to assert that when a request comes in that matches the /ping route with an application/json content header the request is passed off to our handler function.
object PingRouterTest: Spek({
describe("PingRouter") {
lateinit var pingHandler: PingHandler
lateinit var pingRouter: PingRouter
beforeGroup {
pingHandler = mock()
pingRouter = PingRouter(pingHandler)
}
on("Ping route") {
/*
We need to setup a dummy ServerRequest who's path will match the path of our router,
and who's headers will match the headers expected by our router.
*/
val request: ServerRequest = mock()
val headers: ServerRequest.Headers = mock()
When calling request.pathContainer() itReturns PathContainer.parsePath("/ping")
When calling request.method() itReturns HttpMethod.GET
When calling request.headers() itReturns headers
When calling headers.accept() itReturns listOf(MediaType.APPLICATION_JSON)
/*
We call pingRouter.pingRoute() which will return a RouterFunction. We then call route()
on the RouterFunction to actually send our dummy request to the router. WebFlux returns
a Mono that wraps the reference to our PingHandler class's handler function in a
HandlerFunction instance if the request matches our router, if it does not, WebFlux will
return an empty Mono. Finally we invoke handle() on the HandlerFunction to actually call
our handler function in our PingHandler class.
*/
pingRouter.pingRoute().route(request).subscribe({ it.handle(request) })
/*
If our pingHandler.handlePing() was invoked by the HandlerFunction, we know we properly
configured our route for the request.
*/
it("Should call the handler with request") {
verify(pingHandler, times(1)).handlePing(request)
}
}
}
})