Summary

  • A simple @SpringBootTest passes when you test an application that uses a controller, but the same test fails when you test an application that uses a JAX-RS/Jersey endpoint instead.
  • (If you run the application manually, you can confirm that the application does work if it uses a JAX-RS/Jersey endpoint.)

Ideally, there are two ways to fix this issue:

  • Fix @SpringBootTest so that it just works with JAX-RS/Jersey.
  • In the alternative, update the Testing the Web Layer guide to include a working example for JAX-RS/Jersey. (Or, make some other similar update to the documentation.)

I've attached spring-jersey-test.zip, which can reproduce this issue; all you need to do is unzip it and run ./gradlew build.


This code is similar to the example in the Testing the Web Layer guide, except that it uses a JAX-RS/Jersey instead of a controller:

package org.example;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.springframework.stereotype.Component;

@Component
@Path("/")
public final class GreetingEndpoint {

    @GET
    public String helloWorld() {
        return "Hello, world!";
    }
}
package org.example;

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

@Component
public final class JerseyConfig extends ResourceConfig {

    public JerseyConfig() {
        register(GreetingEndpoint.class);
    }
}
package org.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GreetingApplication {

    public static void main(String[] args) {
        SpringApplication.run(GreetingApplication.class, args);
    }
}

Here is the test for the application:

package org.example;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest
@AutoConfigureMockMvc
public final class GreetingApplicationTest {

    @Autowired
    private MockMvc mvc;

    @Test
    public void helloWorld() throws Exception {
        mvc.perform(get("/")).andExpect(status().isOk()).andExpect(content().string("Hello, world!"));
    }
}

Here is how the code behaves:

  • If you run the application (e.g. ./gradlew bootRun) and manually test it, it works.
  • If you run the tests (e.g., ./gradlew build), the tests fail.
  • (I've also confirmed that the tests pass if you use a controller instead of a JAX-RS/Jersey endpoint.)

In the interest of documenting a workaround in the interim, I've also created a Stack Overflow question: https://stackoverflow.com/questions/79240962/how-do-i-test-a-spring-boot-application-that-uses-jax-rs-jersey.

Comment From: mikewacker

Just found the issue here; MockMvc is closely coupled with Spring's MVC architecture; it doesn't work with a JAX-RS/Jersey endpoint. However, the TestRestTemplate example in the Testing the Web Layer guide would work here; I've confirmed this.

(Might be worth adding a note to the documentation, though.)

Comment From: bclozel

We already have a section for testing with mock environments. Feel free to suggest where we should have an additional note. Which part of the docs where you looking at?

Comment From: mikewacker

The context here is that I'm trying to adapt the Testing the Web Layer guide to JAX-RS/Jersey—though I would note that the testing documentation doesn't have a single mention of JAX-RS or Jersey.

  • For an application-level test (i.e., this issue), the answer is that the TestRestTemplate example in the guide will work with JAX-RS/Jersey, but the MockMvc example will not.
  • For an endpoint-level test, I'm stilling trying to figure out how to isolate a single endpoint. The idea here is similar to testing a single controller with @WebMvcTest—though it's fine to use a real web environment.
  • (A mock environment similar to MockMvc is nice to have, but a real web environment is fine.)

Some more context:

  • I'm trying to use the same testing stack (JUnit Jupiter, AssertJ, Spring Boot's test library) regardless of whether a controller or JAX-RS/Jersey endpoint is used.
  • Notwithstanding the above point about a consistent testing stack, the other issue with using JerseyTest here is that it doesn't support JUnit Jupiter.

Comment From: mikewacker

For the sake of updating the documentation, this code works w.r.t. writing an endpoint-level test:

package org.example;

import static org.assertj.core.api.Assertions.assertThat;

import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;

@SpringBootTest(classes = GreetingEndpointTest.TestApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT)
public final class GreetingEndpointTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void helloWorld() {
        assertThat(restTemplate.getForObject(String.format("http://localhost:%d", port), String.class))
                .isEqualTo("Hello, world!");
    }

    @Component
    public static final class JerseyTestConfig extends ResourceConfig {

        public JerseyTestConfig() {
            register(GreetingEndpoint.class);
        }
    }

    @SpringBootConfiguration
    @EnableAutoConfiguration
    @Import(JerseyTestConfig.class)
    public static class TestApplication {

        public static void main(String[] args) {
            SpringApplication.run(TestApplication.class, args);
        }
    }
}

(In a real-world context, the production JerseyConfig class may register multiple endpoints, hence the need to set up a real web environment that tests a single endpoint.)

Comment From: bclozel

I don't think we want to highlight this solution in our reference documentation. This promotes the idea that applications should test with a live server single web endpoints, somehow manually slicing their application context to setup a single jaxrs resource and the relevant pieces, plus a live server.

Our test slicing approach works well for lightweight application contexts that can be started quickly and stored efficiently in the test context cache. Here, starting up a live server with many slightly different configurations is likely to quickly fill the test context cache and make the test suite slower compared to using a regular @SpringBootTest setup where the context can be reused across tests.

Comment From: mikewacker

Is there a better option that you would recommend then? The goal is to document how to write an application-level and endpoint-level test for Spring Boot w/ JAX-RS/Jersey, similar to how the Testing the Web Layer guide documents how to write an application-level and controller-level test.

Comment From: bclozel

A test framework like https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest/test-framework.html looks interesting.

I have never explored this area and I don't think Jersey is popular enough in our community to invest a lot of our time in this at the moment.