A Spring Boot integration test starts the real application context with @SpringBootTest and checks that the controller, the service, the repository and the database work together. A unit test checks one class with its dependencies replaced by mocks, whereas an integration test sends a request through all the layers and checks what comes back.
We write integration tests for the bugs that unit tests cannot see, such as a JSON field with the wrong name, a validation annotation that never runs, a broken SQL query or a missing transaction. For example, a table booking REST API can pass every unit test and still return HTTP 500 in production because the service calls an SMS provider that nobody configured.
The following example starts a Spring Boot 4.1.1 app on a random port with an H2 in-memory database, books a table through a real HTTP call with RestTestClient, and checks the response, the database row and the call to the SMS client.
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
@Sql(statements = "DELETE FROM reservation")
class ReservationApiIT {
@Autowired
RestTestClient client;
@Autowired
ReservationRepository repository;
@MockitoBean
SmsClient smsClient;
@Test
void booksATable() {
ReservationRequest request = new ReservationRequest("Lokesh", 4, LocalDate.of(2030, 5, 20));
client.post().uri("/reservations")
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.exchange()
.expectStatus().isCreated() // 201
.expectHeader().valueMatches("Location", "/reservations/\\d+")
.expectBody()
.jsonPath("$.guestName").isEqualTo("Lokesh")
.jsonPath("$.partySize").isEqualTo(4);
assertThat(repository.count()).isEqualTo(1); // row in H2
verify(smsClient).send("Lokesh", "Table for 4 on 2030-05-20"); // mock called once
}
}
[INFO] Running com.howtodoinjava.reservations.ReservationApiIT
... o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 41387 (http) with context path '/'
... c.h.reservations.ReservationApiIT : Started ReservationApiIT in 1.558 seconds (process running for 17.418)
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.322 s -- in com.howtodoinjava.reservations.ReservationApiIT
[INFO] BUILD SUCCESS
Notice that only the SmsClient is a mock. The HTTP request, the JSON conversion, the bean validation, the transaction and the SQL all run as they do in production, and the Tomcat port changes on every run.
Next, we compare integration tests with unit and slice tests, set up the Maven dependencies for Spring Boot 4, and write tests for errors and test data. After that, we look at the MockMvc variant without a server, TestRestTemplate, running the tests with Failsafe and keeping the build fast with context caching.
1. Integration Tests vs Unit Tests and Slice Tests
Spring Boot offers several test types, and they differ in how much of the application they load. A unit test loads nothing from Spring. A slice test such as @WebMvcTest or @DataJpaTest loads one layer, and @SpringBootTest loads the whole application context, that is, every bean of the app.
The webEnvironment attribute of @SpringBootTest decides whether a real server starts.
- MOCK (the default) creates the web context without a server, and tests send requests through MockMvc.
- RANDOM_PORT starts the embedded Tomcat on a free port, and tests send real HTTP requests.
- DEFINED_PORT starts Tomcat on server.port, 8080 by default, which fails when another process uses the port.
- NONE creates a context without any web environment, for batch jobs and message listeners.

Each kind of test catches a different class of bugs, so a typical project has many unit tests, some slice tests and a few integration tests for the main flows.
| Test type | Loads | Typical speed | Finds bugs in |
|---|---|---|---|
| Unit test with Mockito | one class | milliseconds | business logic |
| @WebMvcTest | controllers, JSON, validation | about a second | request mapping, status codes |
| @DataJpaTest | repositories, JPA, database | about a second | queries, mappings |
| @SpringBootTest (MOCK) | whole context, no server | seconds | wiring, transactions, layers together |
| @SpringBootTest (RANDOM_PORT) | whole context and Tomcat | seconds | the full HTTP path, filters, error responses |
For the unit-test side of the same controller, see testing the controller, service and DAO layers.
2. Setting Up a Spring Boot 4 Integration Test
The example app takes table reservations for a restaurant. ReservationController exposes three endpoints, ReservationService saves the booking with ReservationRepository and sends a text message through SmsClient, and an H2 database holds the reservation table. The project uses Java 25 and Spring Boot 4.1.1.
2.1. Maven Dependencies
Spring Boot 4 splits the test support into modules, and each technology has its own test starter. The starter spring-boot-starter-webmvc-test brings JUnit, AssertJ, Mockito, MockMvc and RestTestClient. For TestRestTemplate, we also add spring-boot-starter-restclient-test.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-restclient-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Without spring-boot-starter-restclient-test, a test with @AutoConfigureTestRestTemplate fails to start the context with NoClassDefFoundError: org/springframework/boot/restclient/RestTemplateBuilder. Projects that use only RestTestClient or MockMvc don’t need that starter.
2.2. The Controller Under Test
The controller validates the request body, returns 201 Created with a Location header for a new booking, and returns 404 Not Found for an unknown id. With spring.mvc.problemdetails.enabled=true in application.properties, Spring MVC returns validation errors as ProblemDetail JSON.
@RestController
@RequestMapping("/reservations")
public class ReservationController {
private final ReservationService service;
public ReservationController(ReservationService service) {
this.service = service;
}
@PostMapping
public ResponseEntity<Reservation> book(@Valid @RequestBody ReservationRequest request) {
Reservation saved = service.book(request);
return ResponseEntity.created(URI.create("/reservations/" + saved.getId())).body(saved);
}
@GetMapping("/{id}")
public ResponseEntity<Reservation> find(@PathVariable long id) {
return ResponseEntity.of(service.find(id));
}
@GetMapping
public List<Reservation> forDate(@RequestParam LocalDate date) {
return service.forDate(date);
}
}
The request is a record with Jakarta Bean Validation constraints, so a party of 20 or a date in the past never reaches the service.
public record ReservationRequest(
@NotBlank String guestName,
@Min(1) @Max(12) int partySize,
@NotNull @Future LocalDate date) {
}
3. Testing the API on a Random Port
With RANDOM_PORT, the test and the app run in the same JVM, but every request goes over a real HTTP connection to Tomcat. The annotation @AutoConfigureRestTestClient registers a RestTestClient bean that already knows the random port, so we write /reservations instead of a full URL. If a test needs the port number itself, e.g. for a WebSocket client, we inject it with @LocalServerPort.

3.1. Checking Error Responses
Integration tests are most useful on error paths, because the response depends on validation, exception handling and the JSON settings together. The next two tests send a party of 20 and ask for a reservation that does not exist.
@Test
void rejectsPartyLargerThanTwelve() {
ReservationRequest request = new ReservationRequest("Lokesh", 20, LocalDate.of(2030, 5, 20));
client.post().uri("/reservations")
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.exchange()
.expectStatus().isBadRequest() // 400
.expectHeader().contentType(MediaType.APPLICATION_PROBLEM_JSON)
.expectBody()
.jsonPath("$.detail").isEqualTo("Invalid request content.");
assertThat(repository.count()).isZero(); // nothing saved
}
@Test
void returns404ForUnknownReservation() {
client.get().uri("/reservations/999")
.exchange()
.expectStatus().isNotFound(); // 404
}
With curl, the same request to the running app returns this response.
HTTP/1.1 400
Content-Type: application/problem+json
{"detail":"Invalid request content.","instance":"/reservations","status":400,"title":"Bad Request"}
3.2. Replacing Only External Systems With @MockitoBean
An integration test replaces only the parts that leave our process, such as an SMS gateway, a payment provider or another team’s REST API. Everything we own stays real, otherwise the test checks the mocks instead of the app.
The @MockitoBean annotation replaces the SmsClient bean in the application context with a Mockito mock, and Spring injects the mock into ReservationService. In Spring Boot 4, @MockitoBean from Spring Framework replaces the removed @MockBean. Without the mock, the real SmsClient throws an IllegalStateException because no provider is configured, and the same booking request returns 500 Internal Server Error.
@MockitoBean
SmsClient smsClient;
// in the test, after the POST request
verify(smsClient).send("Lokesh", "Table for 4 on 2030-05-20"); // the service sent the text
4. Test Data With @Sql
Integration tests share one database, so the data from one test can break another test. A typical case is a test that counts the bookings for one evening and fails only when it runs after a test that added a booking for the same evening.
The @Sql annotation runs SQL statements or scripts before each test method. On the class, DELETE FROM reservation empties the table before every test. On a method, @Sql(“/reservations.sql”) loads the rows that the test needs.
INSERT INTO reservation (id, guest_name, party_size, date) VALUES (101, 'Priya', 2, DATE '2030-05-20');
INSERT INTO reservation (id, guest_name, party_size, date) VALUES (102, 'Anna', 6, DATE '2030-05-20');
INSERT INTO reservation (id, guest_name, party_size, date) VALUES (103, 'Tom', 3, DATE '2030-05-21');
@Test
@Sql("/reservations.sql")
void listsReservationsForOneDate() {
client.get().uri("/reservations?date=2030-05-20")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.length()").isEqualTo(2) // Anna and Priya, not Tom
.jsonPath("$[0].guestName").isEqualTo("Anna")
.jsonPath("$[1].guestName").isEqualTo("Priya");
}
By default, a method-level @Sql replaces the class-level @Sql, so the DELETE does not run before this test. When booksATable() runs first, its booking for 2030-05-20 is still in the table, and the test fails.
[ERROR] com.howtodoinjava.reservations.ReservationApiIT.listsReservationsForOneDate -- Time elapsed: 0.242 s <<< FAILURE!
java.lang.AssertionError: JSON path "$.length()" expected:<2> but was:<3>
The annotation @SqlMergeMode(MergeMode.MERGE) on the class tells Spring to run the class-level statements first and the method-level script after them, so every test starts from an empty table.
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
@Sql(statements = "DELETE FROM reservation")
@SqlMergeMode(MergeMode.MERGE)
class ReservationApiIT {
// fields and tests as before
}
We can’t use @Transactional for cleanup in a RANDOM_PORT test. The HTTP request runs in a Tomcat thread with its own transaction, and that transaction commits before the response comes back, so the rollback at the end of the test does not undo it.
5. Integration Tests Without a Server Using MockMvc
With the default MOCK environment, Spring Boot loads the full context but starts no server. The annotation @AutoConfigureMockMvc sets up MockMvc, which calls the DispatcherServlet in the test thread. MockMvcTester wraps MockMvc with AssertJ assertions.
Because the request runs in the test thread, @Transactional on the test class works here. Spring rolls back every test at the end, so the booking from the first test is gone when the third test checks the table.
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class ReservationMockMvcIT {
@Autowired
MockMvcTester mvc;
@Autowired
ReservationRepository repository;
@MockitoBean
SmsClient smsClient;
@Test
@Order(1)
void booksATableWithoutServer() {
assertThat(mvc.post().uri("/reservations")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"guestName": "Lokesh", "partySize": 4, "date": "2030-05-20"}
"""))
.hasStatus(HttpStatus.CREATED)
.bodyJson().extractingPath("$.guestName").isEqualTo("Lokesh");
assertThat(repository.count()).isEqualTo(1); // rolled back after the test
}
@Test
@Order(3)
void bookingFromFirstTestWasRolledBack() {
assertThat(repository.count()).isZero(); // 0
}
}
A MOCK test is faster and gives us rollback, but it skips the servlet container. A RANDOM_PORT test also covers the things that only exist in a real server, such as servlet filters registered with the container, the error page handling of Tomcat and the HTTP connection itself.
6. Using TestRestTemplate
Older tests use TestRestTemplate, which is based on RestTemplate. In Spring Boot 4, it moved to the package org.springframework.boot.resttestclient, and the test class needs @AutoConfigureTestRestTemplate plus the spring-boot-starter-restclient-test dependency from section 2.1.
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureTestRestTemplate
class ReservationTestRestTemplateIT {
@Autowired
TestRestTemplate restTemplate;
@Test
void returns404ForUnknownReservation() {
ResponseEntity<String> response = restTemplate.getForEntity("/reservations/999", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); // 404
}
}
For new tests, we prefer RestTestClient, which the Spring Boot reference documentation uses for servers on a random port. Its chained expectStatus() and jsonPath() assertions replace the manual ResponseEntity checks. For more TestRestTemplate calls, see TestRestTemplate POST examples.
7. Running Integration Tests With Maven Failsafe
Integration tests take seconds, not milliseconds, so many teams keep them out of mvn test. The Maven Failsafe plugin runs the classes whose names end with IT in the integration-test phase, after the package phase. The Spring Boot parent already configures the Failsafe goals, so we only declare the plugin.
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
</plugin>
</plugins>
</build>
The command mvn test runs only the unit tests, and mvn verify also runs the three IT classes.
[INFO] --- failsafe:3.5.6:integration-test (default) @ spring-boot-integration-test ---
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 9.331 s -- in com.howtodoinjava.reservations.ReservationMockMvcIT
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.882 s -- in com.howtodoinjava.reservations.ReservationTestRestTemplateIT
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.375 s -- in com.howtodoinjava.reservations.ReservationApiIT
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
To run JUnit tests with Maven in general, see the JUnit 5 Maven dependency setup.
8. Keeping Integration Tests Fast With Context Caching
Starting the application context is the slow part of an integration test. The Spring TestContext framework keeps every context it creates in a static cache and reuses it for every test class with the same configuration in the same JVM. The cache holds up to 32 contexts by default.
The cache key includes the annotations, the active profiles, the properties and the @MockitoBean fields. For example, when a copy of ReservationApiIT with the same configuration runs first, the copy starts the context in 8.4 seconds. ReservationApiIT reuses that context, so its log has no “Started” line and its four tests take 0.146 seconds.
[INFO] Running com.howtodoinjava.reservations.ReservationApiCopyIT
... Started ReservationApiCopyIT in 8.415 seconds (process running for 10.339)
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 10.80 s -- in com.howtodoinjava.reservations.ReservationApiCopyIT
...
[INFO] Running com.howtodoinjava.reservations.ReservationApiIT
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.146 s -- in com.howtodoinjava.reservations.ReservationApiIT
So we keep the test configuration of the integration test classes the same where we can. Every different set of @MockitoBean fields, a different @ActiveProfiles value and every @DirtiesContext creates another context, and each one costs another startup.
9. Spring Boot Integration Testing FAQs
9.1. Should Integration Tests Use H2 or a Real Database?
H2 is fine when the app uses plain JPA and standard SQL, because the tests run without any setup. When the app uses database-specific features such as PostgreSQL JSON columns or MySQL-specific SQL, we test against the same database as production. Testcontainers starts that database in a Docker container for the test run, and Spring Boot’s @ServiceConnection connects the app to it. Testcontainers needs Docker on the developer machine and on the CI server.
9.2. What Is the Difference Between @SpringBootTest and @WebMvcTest?
The annotation @SpringBootTest loads every bean of the application, whereas @WebMvcTest loads only the web layer, such as controllers, JSON conversion and validation. In a @WebMvcTest, we replace the services with @MockitoBean, so it checks the request mapping but not the service logic or the database.
9.3. How Do I Use Different Properties in Integration Tests?
We put the test values in src/test/resources/application.properties, set them for one class with @SpringBootTest(properties = “…”), or activate a profile with @ActiveProfiles. Test-only beans go into a class with @TestConfiguration.
10. Conclusion
A Spring Boot integration test loads the real application context with @SpringBootTest and checks a request through all the layers. With RANDOM_PORT and RestTestClient, the test also covers the HTTP connection and the servlet container, and with the default MOCK environment and MockMvcTester, the test runs faster and can roll back its data with @Transactional.
Good integration tests replace only external systems with @MockitoBean, load their own data with @Sql, and clean up before every test. In Spring Boot 4, the test support comes from spring-boot-starter-webmvc-test, and TestRestTemplate needs the extra spring-boot-starter-restclient-test dependency. Failsafe keeps the slower tests in mvn verify, and a shared test configuration lets the context cache start the app only once.
11. References
- Spring Boot Reference: Testing Spring Boot Applications
- Spring Framework Reference: RestTestClient
- Spring Framework Reference: Executing SQL Scripts
- Spring Framework Reference: Context Caching
- Spring Framework Reference: @MockitoBean
- Maven Failsafe Plugin
Happy Learning !!