You’ve written Controller tests with MockMvc, but there are times you want to verify things “through actual HTTP.” When you need to check filter misconfigurations, real serialization results, or how authentication headers are handled, MockMvc alone can leave you uneasy.
This article introduces how to write tests that actually start the application with @SpringBootTest(webEnvironment = RANDOM_PORT) and send real HTTP requests using TestRestTemplate. We’ll assume you already have the basics of JUnit and Mockito down.
What MockMvc Alone Can’t Verify
MockMvc works by directly invoking DispatcherServlet internally, so it doesn’t go through an actual servlet container or network stack. While this makes it fast and convenient, it falls short in cases like:
- Whether Spring Security’s filter chain is actually working
- The Content-Type and headers of the actually serialized JSON
- Confirming behavior over the same path as an external access
TestRestTemplate is well-suited for this kind of verification that actually goes through HTTP.
Actually Starting the App with @SpringBootTest(RANDOM_PORT)
Specifying RANDOM_PORT for webEnvironment actually starts the application on an available port.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserApiE2ETest {
@Autowired
private TestRestTemplate restTemplate;
@LocalServerPort
private int port;
}
Spring Boot automatically registers TestRestTemplate as a Bean, so you can use it just by adding @Autowired. Since the port changes for each test run, there are no port conflicts even when running in parallel.
Building the Base URL with @LocalServerPort
Since the port number changes each time the test runs, receive it with @LocalServerPort and build the URL from it.
private String url(String path) {
return "http://localhost:" + port + path;
}
Doing this keeps the request-sending part clean.
Sending a GET Request and Verifying It
Let’s start with a basic GET.
@Test
void getUser_returns200AndBody() {
ResponseEntity<UserDto> response =
restTemplate.getForEntity(url("/api/users/1"), UserDto.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().getName()).isEqualTo("Taro");
}
If you only want the body, you can use getForObject, but if you want to check the status and headers as well, getForEntity is the standard choice.
Sending POST/PUT/DELETE
For update operations, use postForEntity or exchange.
@Test
void createUser_returns201() {
CreateUserRequest req = new CreateUserRequest("Hanako");
ResponseEntity<UserDto> response =
restTemplate.postForEntity(url("/api/users"), req, UserDto.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getHeaders().getLocation().getPath()).contains("/api/users/");
}
@Test
void deleteUser_returns204() {
restTemplate.exchange(url("/api/users/1"), HttpMethod.DELETE, null, Void.class);
}
For cases like PUT where you want to specify both the body and headers in detail, create an HttpEntity and pass it to exchange.
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<UpdateUserRequest> entity = new HttpEntity<>(new UpdateUserRequest("Jiro"), headers);
ResponseEntity<UserDto> response =
restTemplate.exchange(url("/api/users/1"), HttpMethod.PUT, entity, UserDto.class);
Verifying Headers and Body Contents in Detail
Since ResponseEntity gives you access to headers and body in addition to status, you can assert on the fields you need directly.
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
assertThat(response.getBody().getEmail()).isNotBlank();
Hitting Authenticated Endpoints
withBasicAuth is convenient for APIs that require Basic authentication.
@Test
void adminEndpoint_requiresAuth() {
ResponseEntity<String> unauthorized =
restTemplate.getForEntity(url("/api/admin"), String.class);
assertThat(unauthorized.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
ResponseEntity<String> ok = restTemplate
.withBasicAuth("admin", "password")
.getForEntity(url("/api/admin"), String.class);
assertThat(ok.getStatusCode()).isEqualTo(HttpStatus.OK);
}
If you want to build the Authorization header yourself, such as for a Bearer token, set it on HttpHeaders and send it via exchange.
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
restTemplate.exchange(url("/api/admin"), HttpMethod.GET, new HttpEntity<>(headers), String.class);
Verifying 4xx/5xx Error Responses
This is a point that’s surprisingly easy to misunderstand: TestRestTemplate does not throw an exception even when a 4xx/5xx is returned. You can simply receive the ResponseEntity as-is.
@Test
void getUser_notFound_returns404WithErrorBody() {
ResponseEntity<ErrorResponse> response =
restTemplate.getForEntity(url("/api/users/999"), ErrorResponse.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
assertThat(response.getBody().getMessage()).contains("見つかりません");
}
If you use plain RestTemplate, it throws an exception on 4xx/5xx, but TestRestTemplate behaves this way because error handling is disabled by default for testing purposes.
MockMvc or TestRestTemplate: Which Should You Use?
Honestly, in terms of execution speed and ease of writing, MockMvc has the advantage in many cases. The criterion is simple: “do you need to go through the servlet container and filter chain?”
- Want to quickly check just the Controller logic → MockMvc
- Want to verify Spring Security filters and authentication over a production-equivalent path → TestRestTemplate
- Want to check actual JSON serialization results or CORS configuration → TestRestTemplate
Many projects use both together: MockMvc for testing Controllers individually, and TestRestTemplate only for the important scenarios involving authentication and filters. If you need integration tests that also cover the database, integration testing with Testcontainers is also worth checking out.
Summary
Combining @SpringBootTest(webEnvironment = RANDOM_PORT) with TestRestTemplate lets you write tests that send real HTTP requests with the application actually running. It’s a powerful option when you want to verify things including the filter chain and authentication.
Try replacing one of your existing MockMvc tests that involves authentication or headers with TestRestTemplate — you’ll likely feel the difference right away. If you’d like to organize the overall picture of annotations, check out the test annotations cheat sheet as well.