Hello! Welcome to the next lesson in our journey through backend development with Java and Spring.
In our previous lessons, we explored "slice testing," where we tested individual layers of our application—the web layer with @WebMvcTest and the persistence layer with @DataJpaTest—in isolation. This is a powerful technique for ensuring each component works correctly on its own.
Today, we will put all the pieces together. The goal of this lesson is to learn how to write end-to-end tests for the application using @SpringBootTest and TestRestTemplate. An end-to-end (E2E) test verifies the entire application flow, from an incoming HTTP request through the service and repository layers to the database, and back again. This is the ultimate way to ensure all your application's layers are integrating correctly.
1. From Slice Tests to Full Integration Tests
Slice tests are fast and focused, but they don't catch integration bugs. For example, a controller test with @WebMvcTest uses a mocked service, so it can't tell you if the real service behaves as the controller expects. An end-to-end test closes this gap by loading and testing the entire system.
The primary tool for this is the @SpringBootTest annotation.
Spring Boot REST controller Integration Testing
To understand the setup for an E2E test, let's read a short article that explains how @SpringBootTest works. This will clarify how it differs from the slice test annotations we've used before.
Please read the section titled 'Creating the integration test'. Focus on the description of the webEnvironment parameter, especially RANDOM_PORT. This is the key setting that tells Spring Boot to start a real web server for our test.
As you read, the key takeaway is that @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) does two things:
- It loads your entire application context, including all your controllers, services, repositories, and other components.
- It starts an embedded web server (like Tomcat) on a randomly available port, allowing your test to make real HTTP requests to the application.
2. Making HTTP Requests with TestRestTemplate
Since our test will run a real server, we need a client to make HTTP requests to it. Spring Boot provides TestRestTemplate for this purpose. It's a convenient wrapper around RestTemplate specifically designed for integration tests.
Spring Boot REST controller Integration Testing
The same article provides a good, concise introduction to TestRestTemplate.
Please read the section titled 'The TestRestTemplate'. This will give you a clear definition of what it is and what it's used for. Note the mention of getForObject() and getForEntity(), which are common methods you'll be using.
TestRestTemplate allows us to perform any HTTP action (GET, POST, PUT, DELETE) and then inspect the response, including the status code, headers, and body.
3. Writing Your First End-to-End Test
Let's combine these concepts. An E2E test class typically looks like this:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
// 1. Tell Spring Boot to load the entire application and start a server on a random port
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class YourControllerIT { // "IT" suffix often denotes Integration Test
// 2. Inject the test client
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldGetAllEntities() {
// 3. Make a GET request to an endpoint
// The URL is relative because TestRestTemplate is automatically configured
// with the base URL of the running test server.
ResponseEntity<String> response = restTemplate.getForEntity("/api/your-entities", String.class);
// 4. Assert the response
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
// You can also use JSONAssert for more detailed JSON body assertions
}
}
This structure forms the basis of all E2E tests. However, there's one critical piece missing: the database.
4. Reliable Database Testing with Testcontainers
When you run a full @SpringBootTest, it will try to connect to the database configured in your application.properties. This could be a local developer database, which might contain existing data and isn't ideal for clean, repeatable tests.
While @DataJpaTest defaults to an in-memory H2 database, H2 is not a perfect replica of production databases like PostgreSQL or MySQL. This can hide subtle bugs.
The modern solution is Testcontainers. It's a library that lets you programmatically start and stop disposable Docker containers for your tests. This means you can run your E2E tests against a real, clean PostgreSQL (or MySQL) database every single time.
The following video provides an excellent walkthrough of integrating Testcontainers into a Spring Boot application for E2E testing.
Spring Boot Testcontainers - Integration Testing made easy!
The video 'Spring Boot Testcontainers - Integration Testing made easy!' by Dan Vega is a fantastic practical guide. We'll watch the most relevant part where he builds a full end-to-end test.
Please watch from 00:23:50 to 00:30:10. Pay close attention to these steps: Test Class Setup: How @SpringBootTest, @Testcontainers, and the @Container annotation are used together. Service Connection: The use of @ServiceConnection (a feature in newer Spring Boot versions) which automatically configures the application to connect to the test container database, removing a lot of old boilerplate code. Using TestRestTemplate: How he uses restTemplate to make a GET request to /api/posts. Assertions: How he asserts both the HTTP status code and the content of the response body.
Let's break down the key code from the video:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers // Enables Testcontainers support in this test class
class PostControllerTest {
@Autowired
TestRestTemplate restTemplate;
// Defines a PostgreSQL container that will be started before tests
@Container
@ServiceConnection // Magic annotation: connects Spring to this container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@Test
void shouldFindAllPosts() {
// Act: Make a real HTTP call
Post[] posts = restTemplate.getForObject("/api/posts", Post[].class);
// Assert: Check the results from the live application
assertThat(posts.length).isEqualTo(100);
}
@Test
void shouldFindPostWhenValidPostID() {
// Using .exchange() gives more control to assert status codes
ResponseEntity<Post> response = restTemplate.exchange("/api/posts/1", HttpMethod.GET, null, Post.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
}
}
This setup gives you the highest level of confidence. Your test is exercising the exact same code, connected to the exact same type of database, that you will run in production.
Test your understanding!
You want to write an E2E test for a DELETE /api/posts/99 endpoint. The endpoint should return a 204 No Content status on successful deletion. How would you structure this test using TestRestTemplate?
Show answer
Here's a good approach:
-
Arrange: The test database is already populated with posts (as seen in the video). No extra setup is needed.
-
Act:
- Send a
DELETErequest. Thedeletemethod doesn't return a body, so we just pass the URL.
restTemplate.delete("/api/posts/99"); - To verify, try to
GETthe post that was just deleted.
ResponseEntity<Void> response = restTemplate.getForEntity("/api/posts/99", Void.class);
- Send a
-
Assert:
- Assert that the GET request after the delete results in a
404 Not Found.
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
(Alternatively, you could use
restTemplate.exchange("/api/posts/99", HttpMethod.DELETE, null, Void.class)and directly assert that its response status isHttpStatus.NO_CONTENT.) - Assert that the GET request after the delete results in a
5. Testing POST Requests
Testing POST requests is slightly different because you need to provide a request body and often check for a Location header in the response, which is a REST API best practice indicating the URL of the newly created resource.
Writing Integration Tests for Rest Services with Spring Boot
The article 'Writing Integration Tests for Rest Services with Spring Boot' shows a clear example of testing a POST request and asserting the Location header.
Read the section 'Writing Integration Test for the Http POST Operation'. Focus on the addCourse test method. Notice how an HttpEntity is created to wrap the request body, and how response.getHeaders().get(HttpHeaders.LOCATION) is used to check for the URI of the new resource.
The pattern looks like this:
@Test
public void addCourse() {
// Arrange: Create the object to be sent in the request body
Course course = new Course("New Course", "Description", ...);
HttpEntity<Course> requestEntity = new HttpEntity<>(course);
// Act: Send the POST request
ResponseEntity<String> response = restTemplate.exchange(
"/students/Student1/courses",
HttpMethod.POST,
requestEntity,
String.class
);
// Assert: Check for the Location header
String locationHeader = response.getHeaders().getFirst(HttpHeaders.LOCATION);
assertThat(locationHeader).contains("/students/Student1/courses/");
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
}
Conclusion
You have now learned the most comprehensive testing strategy in Spring Boot. By writing E2E tests, you gain confidence that all the individual parts of your application not only work on their own but also work together seamlessly.
Key Takeaways:
- End-to-end tests verify the integration of all application layers, from the web endpoint to the database.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)is the annotation used to load the full application context and start a real web server.TestRestTemplateis the client used to make live HTTP requests to the test server.- Testcontainers is the modern standard for providing a realistic, disposable database (like PostgreSQL) for your tests, ensuring they are reliable and production-like.
What's Next?
So far, we have written unit tests, slice tests, and now end-to-end tests. But how do we know if our tests are actually covering the important parts of our code? In the next lesson, we will learn how to measure test coverage to identify untested code paths, giving us a metric to evaluate the quality and completeness of our test suite.