Summary
- A simple
@SpringBootTestpasses 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
@SpringBootTestso 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
TestRestTemplateexample in the guide will work with JAX-RS/Jersey, but theMockMvcexample 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
MockMvcis 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
JerseyTesthere 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.