Testing Spring Boot Applications

Integration Testing with TestRestTemplate

18 min Lesson 8 of 13

Integration Testing with TestRestTemplate

Unit tests and slice tests are fast and focused, but they leave a critical question unanswered: does the whole stack work together? Integration tests close that gap. Spring Boot's TestRestTemplate lets you fire real HTTP requests against a fully started application server, traverse every layer — security filters, controllers, services, and the database — and assert on the actual HTTP responses a client would receive.

How Integration Tests Differ from Slice Tests

Slice tests like @WebMvcTest and @DataJpaTest load only a thin slice of the Spring context. They are fast precisely because they skip most of the application. Integration tests take the opposite approach: they boot the complete ApplicationContext, start an embedded Tomcat server on a real port, and let you send HTTP requests as any external client would. The trade-off is clear — setup takes several seconds, so integration tests should cover complete user journeys, not every low-level branch.

@SpringBootTest with a Real Port

To start a real HTTP server in tests, set webEnvironment to RANDOM_PORT (or DEFINED_PORT). Spring Boot then binds to an available port and injects the value into your test class via @LocalServerPort.

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.beans.factory.annotation.Autowired; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class OrderApiIntegrationTest { @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @Test void getOrder_returnsOk() { var response = restTemplate.getForEntity( "http://localhost:" + port + "/api/orders/1", OrderResponse.class ); assertThat(response.getStatusCode().value()).isEqualTo(200); assertThat(response.getBody()).isNotNull(); } }
Why RANDOM_PORT? Using a random port prevents port conflicts when tests run in parallel on CI. @LocalServerPort injects whatever port Spring chose, so you always build URLs against the correct value.

TestRestTemplate vs RestTemplate

TestRestTemplate wraps Spring's standard RestTemplate and adds several test-friendly behaviours. Most importantly, it does not throw exceptions on 4xx/5xx responses — it returns a ResponseEntity with the error status so you can assert on error scenarios cleanly. It also disables redirect following by default, which is what you want when testing redirect logic explicitly.

You get the bean for free from Spring Boot's test autoconfiguration when using RANDOM_PORT or DEFINED_PORT — no manual setup needed.

Testing Create, Read, Update, and Delete

A realistic integration test suite exercises the full CRUD surface of a resource:

import org.springframework.http.*; import java.util.List; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class ProductApiIntegrationTest { @Autowired private TestRestTemplate restTemplate; @LocalServerPort private int port; private String base() { return "http://localhost:" + port + "/api/products"; } @Test void createAndRetrieveProduct() { // CREATE var request = new ProductRequest("Widget", 9.99); ResponseEntity<ProductResponse> created = restTemplate.postForEntity(base(), request, ProductResponse.class); assertThat(created.getStatusCode()).isEqualTo(HttpStatus.CREATED); Long id = created.getBody().id(); // READ ResponseEntity<ProductResponse> fetched = restTemplate.getForEntity(base() + "/" + id, ProductResponse.class); assertThat(fetched.getBody().name()).isEqualTo("Widget"); // UPDATE var updateRequest = new HttpEntity<>(new ProductRequest("Gadget", 19.99)); restTemplate.exchange(base() + "/" + id, HttpMethod.PUT, updateRequest, Void.class); // DELETE restTemplate.delete(base() + "/" + id); // VERIFY GONE ResponseEntity<String> gone = restTemplate.getForEntity(base() + "/" + id, String.class); assertThat(gone.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); } }

Sending Headers and Testing Security

Real applications require authentication headers. Use TestRestTemplate.withBasicAuth() for HTTP Basic, or build a custom HttpEntity with the required headers for Bearer token auth:

// HTTP Basic — convenience wrapper ResponseEntity<OrderResponse> response = restTemplate.withBasicAuth("admin", "password") .getForEntity(base() + "/1", OrderResponse.class); // Bearer token — custom HttpEntity HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(jwtToken); HttpEntity<Void> requestEntity = new HttpEntity<>(headers); ResponseEntity<OrderResponse> secured = restTemplate.exchange(base() + "/1", HttpMethod.GET, requestEntity, OrderResponse.class); // Verify unauthenticated access is denied ResponseEntity<String> denied = restTemplate.getForEntity(base() + "/1", String.class); assertThat(denied.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
Keep a separate test user. Seed a known user (e.g. with a @Sql script or a @BeforeEach that calls the repo directly) rather than relying on production data. This keeps tests deterministic and independent of whatever is in the database at runtime.

Database State in Integration Tests

Because the full application context runs, all data changes are real. Each test that mutates the database can pollute the next one unless you reset state. There are three common strategies:

  • @Transactional on the test class — rolls back after each test. Works well for non-HTTP tests, but has no effect here because TestRestTemplate sends requests over a real HTTP socket; the server runs in a different transaction scope.
  • @Sql to clean up — annotate the test method or class with @Sql(executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, scripts = "cleanup.sql"). Explicit and reliable.
  • Separate test database with a known state — configure application-test.properties to use an H2 in-memory database and set spring.jpa.hibernate.ddl-auto=create-drop. The schema is recreated fresh for every test run.
# src/test/resources/application-test.properties spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL spring.datasource.driver-class-name=org.h2.Driver spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true

Activate this profile in the test class by adding @ActiveProfiles("test") to the annotation list.

Asserting on Response Bodies

Prefer asserting on strongly typed response objects rather than raw JSON strings. When you pass a class to getForEntity, Spring uses Jackson to deserialise the response body automatically. For collections, use ParameterizedTypeReference:

import org.springframework.core.ParameterizedTypeReference; import java.util.List; ResponseEntity<List<ProductResponse>> listResponse = restTemplate.exchange( base(), HttpMethod.GET, null, new ParameterizedTypeReference<List<ProductResponse>>() {} ); assertThat(listResponse.getBody()).hasSize(3); assertThat(listResponse.getBody()) .extracting(ProductResponse::name) .containsExactlyInAnyOrder("Widget", "Gadget", "Doohickey");
Avoid asserting on raw JSON strings. String comparisons break when field order changes or formatting changes. Deserialise into a DTO and assert on fields — your tests will survive JSON serialiser upgrades.

Performance Considerations

Integration tests are inherently slower than unit tests. Follow these practices to keep your test suite manageable:

  • Reuse the application context. By default Spring Boot caches the context between test classes with the same configuration. Avoid adding @MockBean to integration tests — each unique set of mocked beans creates a separate context, multiplying startup costs.
  • Keep integration tests in a dedicated source set or package. Run them separately from unit tests so developers get fast feedback during development and full coverage in CI.
  • Use Testcontainers for production-like databases. In the next lesson you will see how Testcontainers replaces H2 with a real MySQL or PostgreSQL container, eliminating H2 compatibility surprises without sacrificing isolation.

Summary

TestRestTemplate with @SpringBootTest(webEnvironment = RANDOM_PORT) gives you true end-to-end tests: a full Spring context, a real HTTP server, and the ability to exercise every layer of your application with actual HTTP calls. They are the most faithful simulation of production behaviour available without deploying. Use them for critical user journeys, secure the database state per test, lean on ResponseEntity for error-scenario assertions, and keep the suite lean to preserve build speed.