Working with integration tests in Spring Boot is often smooth but can get tricky when you’re testing internal API calls. For instance, using MockMvc usually provides an easy way to test controllers. However, running into a failing internal API call during a MockMvc integration test with a ResourceAccessException can involve some head-scratching and debugging sessions. Let’s see why this happens and how you can resolve it effectively.
Spring Boot Application Overview
Imagine building a calculation library, where the CalculationService depends on an internal REST API call. Let’s first take a look at the components involved.
CalculationService
The CalculationService class might look something like this:
@Service
public class CalculationService {
private final RestTemplate restTemplate;
@Autowired
public CalculationService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public Integer performCalculation(Integer input) {
String url = "http://localhost:8080/api/mock/calculate?value=" + input;
ResponseEntity response = restTemplate.getForEntity(url, Integer.class);
return response.getBody();
}
}
This setup simply delegates calculation logic to another controller using a RestTemplate. It’s straightforward and usually works fine in a running application context.
The MockApi Controller
We have a MockApi controller that CalculationService calls internally during tests or demonstration purposes:
@RestController
@RequestMapping("/api/mock")
public class MockApi {
@GetMapping("/calculate")
public ResponseEntity calculate(@RequestParam Integer value) {
return ResponseEntity.ok(value * 2);
}
}
The MockApi endpoint returns a simple example calculation—it doubles the provided value.
Introducing TestController
The TestController usually serves as the main API endpoint we’re testing:
@RestController
@RequestMapping("/api/test")
public class TestController {
private final CalculationService calculationService;
@Autowired
public TestController(CalculationService calculationService) {
this.calculationService = calculationService;
}
@GetMapping("/perform")
public ResponseEntity performTest(@RequestParam Integer input) {
Integer result = calculationService.performCalculation(input);
return ResponseEntity.ok(result);
}
}
This controller serves as the entry point for the integration test scenario we’re tackling.
RestClient Configuration
For our RestTemplate Bean, you probably have something like CalculationConfiguration:
@Configuration
public class CalculationConfiguration {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
This ensures we have a RestTemplate bean available throughout the application context.
Integration Test Setup
When performing a Spring Boot integration test, annotations are essential. They instruct Spring how to set up testing contexts, load components, and perform mock API calls.
The test class probably includes annotations like:
- @AutoConfigureMockMvc: This sets up the MockMvc environment automatically, enabling HTTP mocking.
- @SpringBootTest: Ensures the test loads the entire Spring context.
- @ExtendWith(SpringExtension.class): Provides JUnit 5 integration with Spring context.
Initializing MockMvc and ObjectMapper occurs like this:
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
And the integration test method resembles something like this:
@Test
void testPerformCalculation() throws Exception {
mockMvc.perform(get("/api/test/perform")
.param("input", "10"))
.andExpect(status().isOk())
.andExpect(content().string("20"));
}
The goal is to assert that sending a GET request with input 10 returns the calculated response of 20.
However, there’s a catch—the test isn’t passing. Why?
Error Analysis: Understanding the Failure
Here’s where things get challenging. When running the test, you might have encountered a ResourceAccessException. This error typically indicates something like:
org.springframework.web.client.ResourceAccessException: I/O error on GET request...; nested exception is java.net.ConnectException: Connection refused
This exception happens because, even though MockMvc correctly mocks the endpoint you’re testing, the actual RestTemplate isn’t aware of this setup. Your RestTemplate makes a real HTTP call to “localhost:8080,” but during the test, no actual server is listening, causing the connection refused error.
Why Does This Happen?
MockMvc doesn’t start an embedded web server. Instead, it provides mocked servlet environments to test controller endpoints directly. Thus, RestTemplate attempts to communicate externally on a real port (8080 by default), while MockMvc endpoints don’t exist on an actual listening server during tests.
Potential Solutions
Now that we’ve pinpointed the root cause, let’s explore some practical approaches to solving this issue.
Check & Modify Test Configuration
First, ensure your integration test context fully loads necessary beans. If the test context doesn’t initialize certain components correctly, this issue arises. Verify your test annotations carefully.
Another solid solution is to use Spring Boot’s @RestClientTest or mock RestTemplate itself using @MockBean rather than relying on an actual RestTemplate bean. Here’s how you’d do it using mock RestTemplate calls:
@SpringBootTest
@AutoConfigureMockMvc
public class IntegrationTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private RestTemplate restTemplate;
@BeforeEach
public void setupMocks() {
when(restTemplate.getForEntity(anyString(), eq(Integer.class)))
.thenReturn(ResponseEntity.ok(20));
}
@Test
void testPerformCalculation() throws Exception {
mockMvc.perform(get("/api/test/perform")
.param("input", "10"))
.andExpect(status().isOk())
.andExpect(content().string("20"));
}
}
By mocking RestTemplate responses in integration tests, you can effectively test your controller logic without external HTTP calls. This approach is beneficial for concise tests that do not rely on additional services or external resources.
Or, if you prefer a test that checks internal calls realistically, you can leverage TestRestTemplate instead of MockMvc. TestRestTemplate integrates with a running server context in actual integration tests by spinning up an embedded server.
Summary and Best Practices
While Spring Boot’s MockMvc provides a simple and effective approach for controller testing, an internal API call using RestTemplate can be tricky in integration tests. The ResourceAccessException arises because no actual HTTP service is listening at runtime.
You have two solid options:
- Mock your RestTemplate responses using Mockito and @MockBean for quick and efficient unit-level integration tests.
- Use TestRestTemplate instead of MockMvc for comprehensive and realistic integration testing across components.
Both approaches are robust and help maintain clean tests. Whichever strategy suits your workflow, ensure your configurations clearly reflect your testing goals.
Experiencing similar issues? Be sure to check popular discussions on Stack Overflow, and always carefully configure your test context annotations.
Have you encountered other similar challenges in integration testing your Spring apps? Feel free to share your approach or ask questions below!
0 Comments