Spring-boot controller integration tests

Benaya Trabelsi
4 min readJul 6, 2023

In the previous article, I talked about integration tests for the caching mechanism in spring boot. In this article, I’ll go back to the basics, and talk about good old controller integration tests.

The focus here will be on the key features of integration tests, and the question — what exactly are we testing, and what are we mocking? Considering the testing pyramid, where is the line drawn between unit, integration, and E2E tests?

Motivation — what is the problem?

I got confused when I first approached the term “integration tests”, especially with spring boot. looking at the testing pyramid, the integration tests part was silently standing between the simple-to-understand unit tests and the full-blown end-to-end tests. With unit tests in spring boot, you just mock the hell out of anything that isn’t the actually tested object. With the E2E test on the other hand, no mocks are involved, you simply spin up an entire environment and run the tests, using Postman, rest template, or any other HTTP client.

In this article, I’ll talk about integration tests, and more specifically — explain the moving parts — what components do we need available when designing integration tests for the controller?

Drawing the line — defining the mocks and tested objects

For our article, I built a simple controller with only two GET methods and one service. here is the code:

@RestController
@RequiredArgsConstructor
@Slf4j
public class LinksController {
private final LinksService linksService;

@GetMapping("/validateLink-url")
public UrlSafety validateLink(@RequestBody String link, @RequestParam("directSearch") boolean isDirectSearch) {
UrlUtils.validateUrl(link);
if (isDirectSearch) {
return linksService.isSafeUrlByDirectSearch(link) ? UrlSafety.VALID : UrlSafety.INVALID;
}else{
return linksService.isSafeUrlByDomainSearchThenUrl(link) ? UrlSafety.VALID : UrlSafety.INVALID;
}
}
@GetMapping("/validateLink-domain")
public UrlSafety validateLinkByDomain(@RequestBody String link) {
UrlUtils.validateUrl(link);
return linksService.isSafeDomainByUrl(link) ? UrlSafety.VALID : UrlSafety.INVALID;
}
}

At a glance, we can see the LinksService, the GET endpoints, the Request body, and the Request params (more widely known as query param) used as parameters. We return an Enum value as a response, and we have an error-handling mechanism with Controller advice and an error handler.

The error handling class:

@RestControllerAdvice
public class ControllerExceptionHandler {

@ExceptionHandler(MalformedURLException.class)
public ProblemDetail handleIllegalArgumentException(BadUrlException e) {
return ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY, e.getMessage());
}
}

It only has one exception handler, which returns a 422 status code, and the error message as the detail, using the spring 6 ErrorDetail object.

To test the expected behavior of the controller, we don’t need the service, so we can throw the LinksService to the mocking trash can. What we do need, is something that can get a request, understand it with all its parts(URL, params, body, headers, etc.) create, and return a response.

For this purpose, we can take one of two approaches:

  • Using WebMvcTest and Mockmvc. This way we send requests to the dispatcher servlet, while mocking the actual server, testing the controller behavior. no servlet container is actually running.
  • spinning up a web server. using SpringBootTest annotation with TestRestTemplate we can actually create the entire application context, and send HTTP requests to the test server. This approach is more suitable for E2E tests, and we don’t do that here.

Since our purpose here is integration testing, I’ll go with the first option, with WebMvcTest and Mockmvc. The context provided by WebMvcTest is minimal and only includes the beans needed for MVC testing, so we add the service bean with mockBean. This way, when the controller will run a service method, it will automatically go to our mock bean, and won’t try to access the real service class which doesn’t exist in our minimal application context.

Let’s see some of the code:

@WebMvcTest
public class LinksControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;

@MockBean
private LinksService linksService;

@Test
public void testValidateLinkDirectSearchTrue_WhenUrlExists_ShouldReturnValid() throws Exception {
String url = "http://safe-url.com";
when(linksService.isSafeUrlByDirectSearch(url)).thenReturn(true);

mockMvc.perform(get("/validateLink-url")
.param("directSearch", "true")
.content(url)
.contentType(MediaType.TEXT_PLAIN)
.accept(MediaType.APPLICATION_PROBLEM_JSON))
.andExpect(status().isOk())
.andExpect(content().string("\"" + UrlSafety.VALID.getValue() + "\""));

verify(linksService, times(1)).isSafeUrlByDirectSearch(url);
}

We use mokitos when method with the service mock bean to define the service behavior, and then build and perform the request with mockmvc. Please note that we don’t use a controller object here. we only send the request to the relevant endpoint with the desired params, and test the expected response. in our case, we set the query param “direct search” to true, expecting the controller to call the isSafeUrlByDirectSearch method, and not the isSafeUrlByDomainSearchThenUrl method, which we verify at the end using the mockito verify method.

To see the rest of the test, including other test cases, please see the code in my repo. it’s more of the same, so I didn’t go over all of it here.

Conclusion

Understanding the concept of `integration tests` is challenging, and before designing the tests, we must first understand what we want to test, the testing environment we need, and what we don’t need. In our case, I wrote integration tests for the controller, so my moving parts are:

  • what I wanted to test — REST API behavior.
  • what I didn’t want to test — was the LinkService, which I set as a mock bean, and the controller object itself.
  • The test environment — I needed only the parts of the application context needed for testing the controller behavior. that’s why I used WebMvcTest annotation to create the environment, and mockmvc to perform the requests.

Please click the 👏🏽 button(up to 50 times, more the better :) ) if you find my article helpful, or comment below to talk about it.

The repo used for this project: link-validator-API.
You’re welcome to reach out on
LinkedIn.

--

--