2

Spring Boot / Jersey can not find a handler method when the application is startet and accessed from a test. If I start the application separately and access http://localhost:8080/demo with a browser everything is fine.

The log says: "Did not find handler method for [/demo]". The relevant logging output:

2018-06-18 17:04:31.071 DEBUG 7628 --- [nio-8080-exec-1] o.s.web.reactive.DispatcherHandler       : Processing GET request for [http://localhost:8080/demo]
2018-06-18 17:04:31.083 DEBUG 7628 --- [nio-8080-exec-1] s.w.r.r.m.a.RequestMappingHandlerMapping : Looking up handler method for path /demo
2018-06-18 17:04:31.085 DEBUG 7628 --- [nio-8080-exec-1] s.w.r.r.m.a.RequestMappingHandlerMapping : Did not find handler method for [/demo]
2018-06-18 17:04:31.087 DEBUG 7628 --- [nio-8080-exec-1] o.s.w.r.handler.SimpleUrlHandlerMapping  : Matching pattern for request [[path='/demo']] is /**

The application consists of the following classes (written in Kotlin):

Resource

import org.springframework.stereotype.Component
import javax.ws.rs.GET
import javax.ws.rs.Path
import javax.ws.rs.core.Response

@Component
@Path("/")
class Resource {

    @GET
    @Path("demo")
    fun test() = Response.ok("Hi!").encoding("UTF-8").build()
}

JerseyConfig

import org.glassfish.jersey.server.ResourceConfig
import org.springframework.stereotype.Component

@Component
class JerseyConfig : ResourceConfig() {

    init {
        register(Resource::class.java)
    }
}

App:

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class App

fun main(args: Array<String>) {
    runApplication<App>(*args)
}

The failing test:

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.test.web.reactive.server.WebTestClient

@ExtendWith(SpringExtension::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
class ResourceTest {

    @Autowired
    lateinit var client: WebTestClient

    @Test
    fun getTest() {
        client.get().uri("demo").exchange().expectStatus().isOk
    }
}

I get the same error, if I use the Jersey client for testing:

@Test
fun testWithJersey() {
    val client = ClientBuilder.newClient()
    val response = client.target("http://localhost:8080/demo").request().get()
    assertThat(response.status).isEqualTo(200)
}

build.gradle:

buildscript {
    ext {
        kotlinVersion = '1.2.50'
        springBootVersion = '2.0.3.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
        classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
    }
}

apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

compileKotlin {
    kotlinOptions {
        freeCompilerArgs = ["-Xjsr305=strict"]
        jvmTarget = "1.8"
    }
}
compileTestKotlin {
    kotlinOptions {
        freeCompilerArgs = ["-Xjsr305=strict"]
        jvmTarget = "1.8"
    }
}

repositories {
    mavenCentral()
}

dependencies {
    compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    compile("org.jetbrains.kotlin:kotlin-reflect")
    compile('org.springframework.boot:spring-boot-starter-jersey')
    testCompile("org.springframework.boot:spring-boot-starter-test") {
        exclude group: "junit", module: "junit"
    }
    testCompile('org.springframework.boot:spring-boot-starter-webflux')
    testCompile("org.junit.jupiter:junit-jupiter-api")
    testRuntime("org.junit.jupiter:junit-jupiter-engine")
}

The test code itself seems to be ok, since when I replace the body of the test method with Thread.sleep(...) and then access the server from a browser, I get the same error (404 due to "Did not find handler method for [/demo]").

Why is the handler method not found in tests? What do I have to change?

deamon
  • 89,107
  • 111
  • 320
  • 448

2 Answers2

1

Are you sure WebTestClient is a universal client (that makes actual network requests) and not only meant to be used with Spring MVC like MockMvc is? The error sounds like it is looking for a Spring MVC handler method. If it was a universal client, the error message wouldn't say anything about handler methods, but instead probable say something about the URL.

I would imagine that you need to make an actual network request with a real client. For instance if you used the Jersey client, you could do something like

@LocalServerPort
private int port;

private Client client = ClientBuilder.newClient();

@Test
public void testCustomerLocationOnPost() {
    URI resourceUri = UriBuilder.fromUri("http://localhost")
            .port(port).path("demo").build();

    Respons response = client.target(resourcrUri).request().get();

    assertThat(response.getStatus()).isEqualTo(200);

}
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • Thanks for your suggestion, but the error remains the same. I think the log message just means that Jersey didn't find a handler method internally like there wouldn't be a handler with a matching pattern. – deamon Jun 19 '18 at 06:27
  • The error means that the framework (which is the Spring web flux test framework) can't find the method. Which would only happen if you are still using the `WebTestClient`, which is part of that framework. – Paul Samsotha Jun 19 '18 at 16:58
1

With the help of the amazing guys from Pivotal I've solved the problem: WebTestClient needs WebFlux as a dependency. But WebFlux brings not only a little test helper but a full blown web framework what gets automagically configured by Spring Boot and is in conflict with Jersey.

The solution is to remove WebFlux from the dependencies

dependencies {
    compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    compile("org.jetbrains.kotlin:kotlin-reflect")
    compile('org.springframework.boot:spring-boot-starter-jersey')
    testCompile("org.springframework.boot:spring-boot-starter-test") {
        exclude group: "junit", module: "junit"
    }
    testCompile("org.junit.jupiter:junit-jupiter-api")
    testRuntime("org.junit.jupiter:junit-jupiter-engine")
}

and use, for example, the Jersey http client:

@ExtendWith(SpringExtension::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
class ResourceTest {

    @Test
    fun testWithJerseyClient() {
        val client = ClientBuilder.newClient()
        val response = client.target("http://localhost:8080/demo").request().get()
        assertThat(response.status).isEqualTo(200)
    }
}

But any other http client will do as long as WebFlux is not a dependency.

Update: This is a bug in Spring Boot that will be fixed in Spring Boot 2.0.4! Then you can use WebFlux for testing along with Jersey on the server side.

deamon
  • 89,107
  • 111
  • 320
  • 448
  • I don't see how your answer is different (besides obvious Java vs Kotlin) from mine other than that I made the client a class member (so you don't repeatedly create an _expensive_ client) and I injected the port (as you can't guarantee it will be 8080, since you are using Spring boots random port). Other than that, your answer is exactly the same. Don't think that would cause the same error you were getting before. – Paul Samsotha Jun 19 '18 at 16:55
  • @PaulSamsotha I've written my answer to explain the actual problem and included the example (that is basically yours) only for completeness. `WebTestClient` is an universal client, BUT it comes with WebFlux and that in combination with Springs magic was the problem. That was all I want to share with future readers. The way you create the test client is better, I've just shortened the code because this detail is not important to solve this specific problem. If I had removed WebFlux after reading your example, the problem would have been solved, but I didn't understand what was going on. – deamon Jun 19 '18 at 19:41
  • By "universal client" I meant an actual client that makes network calls, which I don't think WebTestClient is. It is designed to make call _programmatically_ to the actual handler methods just like MockMvc is. Maybe the wording in my answer is not the same as yours, but I was saying the exact same thing. Maybe you just misunderstood. But its all good. – Paul Samsotha Jun 19 '18 at 19:45
  • Thanks in any case for your effort! Hopefully one of the answers will be helpful for someone facing the same problem. The `WebTestClient` performs real HTTP requests, by the way: https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/test/web/reactive/server/WebTestClient.html – deamon Jun 19 '18 at 19:50