Implementing Integration Tests for a Spring Boot Movie API
Written on
Introduction to Integration Testing
In this article, we will discuss the implementation of integration tests within a Spring Boot application, specifically a Movie API. This application leverages Spring Data JPA to interact with a PostgreSQL database. You can access the complete code and implementation details in the article linked below. Follow the outlined steps to get started.
Integration testing is a crucial type of software testing that assesses how various components of an application work together. In our scenario, this involves testing the Movie API's interaction with other elements, such as the database, to ensure seamless integration. We will utilize Testcontainers to create a PostgreSQL Docker container for our testing needs.
Updating the Movie API
To begin, we need to modify the pom.xml file by including the necessary Testcontainers and HttpClient5 dependencies. Hereβs how you can do it:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
<scope>test</scope>
</dependency>
Creating the MyContainers Interface
Next, we will create the MyContainers interface in the src/test/java directory, specifically within the com.example.movieapi package. This interface will define the PostgreSQL container used in the tests:
package com.example.movieapi;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
public interface MyContainers {
@Container
@ServiceConnection
PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer<>("postgres:15.4");
}
In this interface, we declare a container using the @Container annotation from Testcontainers. The PostgreSQLContainer instance is tailored for running PostgreSQL during tests. The @ServiceConnection annotation allows Spring Boot's autoconfiguration to dynamically register the necessary properties.
Modifying the MovieApiApplicationTests Class
Now, navigate to the MovieApiApplicationTests class generated during the Spring Initializr project setup. Replace its content with the following:
package com.example.movieapi;
import com.example.movieapi.controller.dto.CreateMovieRequest;
import com.example.movieapi.controller.dto.MovieResponse;
import com.example.movieapi.controller.dto.UpdateMovieRequest;
import com.example.movieapi.model.Movie;
import com.example.movieapi.repository.MovieRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
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.testcontainers.context.ImportTestcontainers;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ImportTestcontainers(MyContainers.class)
class MovieApiApplicationTests {
@Autowired
private TestRestTemplate testRestTemplate;
@Autowired
private MovieRepository movieRepository;
@BeforeEach
void setUp() {
movieRepository.deleteAll();}
@Test
void testGetMoviesWhenThereIsNone() {
ResponseEntity responseEntity = testRestTemplate.getForEntity(API_MOVIES_URL, MovieResponse[].class);
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isEmpty();
}
@Test
void testGetMoviesWhenThereIsOne() {
Movie movie = getDefaultMovie();
movieRepository.save(movie);
ResponseEntity responseEntity = testRestTemplate.getForEntity(API_MOVIES_URL, MovieResponse[].class);
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isNotNull();
assertThat(responseEntity.getBody()).hasSize(1);
assertThat(responseEntity.getBody()[0].imdbId()).isEqualTo(movie.getImdbId());
assertThat(responseEntity.getBody()[0].title()).isEqualTo(movie.getTitle());
assertThat(responseEntity.getBody()[0].year()).isEqualTo(movie.getYear());
assertThat(responseEntity.getBody()[0].actors()).isEqualTo(movie.getActors());
}
@Test
void testGetMovieWhenNonExistent() {
String url = API_MOVIES_IMDB_URL.formatted("123");
ResponseEntity responseEntity = testRestTemplate.getForEntity(url, String.class);
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
void testGetMovieWhenExistent() {
Movie movie = getDefaultMovie();
movieRepository.save(movie);
String url = API_MOVIES_IMDB_URL.formatted("123");
ResponseEntity responseEntity = testRestTemplate.getForEntity(url, MovieResponse.class);
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isNotNull();
assertThat(responseEntity.getBody().imdbId()).isEqualTo(movie.getImdbId());
assertThat(responseEntity.getBody().title()).isEqualTo(movie.getTitle());
assertThat(responseEntity.getBody().year()).isEqualTo(movie.getYear());
assertThat(responseEntity.getBody().actors()).isEqualTo(movie.getActors());
}
@Test
void testCreateMovie() {
CreateMovieRequest createMovieRequest = new CreateMovieRequest("123", "title", 2023, "actors");
ResponseEntity responseEntity = testRestTemplate.postForEntity(API_MOVIES_URL, createMovieRequest, MovieResponse.class);
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(responseEntity.getBody()).isNotNull();
assertThat(responseEntity.getBody().imdbId()).isEqualTo(createMovieRequest.imdbId());
assertThat(responseEntity.getBody().title()).isEqualTo(createMovieRequest.title());
assertThat(responseEntity.getBody().year()).isEqualTo(createMovieRequest.year());
assertThat(responseEntity.getBody().actors()).isEqualTo(createMovieRequest.actors());
Optional movieOptional = movieRepository.findById(responseEntity.getBody().imdbId());
assertThat(movieOptional.isPresent()).isTrue();
movieOptional.ifPresent(movieCreated -> {
assertThat(movieCreated.getImdbId()).isEqualTo(createMovieRequest.imdbId());
assertThat(movieCreated.getTitle()).isEqualTo(createMovieRequest.title());
assertThat(movieCreated.getYear()).isEqualTo(createMovieRequest.year());
assertThat(movieCreated.getActors()).isEqualTo(createMovieRequest.actors());
});
}
@Test
void testUpdateMovie() {
Movie movie = getDefaultMovie();
movieRepository.save(movie);
UpdateMovieRequest updateMovieRequest = new UpdateMovieRequest("newTitle", 2024, "newActors");
HttpEntity requestUpdate = new HttpEntity<>(updateMovieRequest);
String url = API_MOVIES_IMDB_URL.formatted(movie.getImdbId());
ResponseEntity responseEntity = testRestTemplate.exchange(url, HttpMethod.PATCH, requestUpdate, MovieResponse.class);
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isNotNull();
assertThat(responseEntity.getBody().imdbId()).isEqualTo(movie.getImdbId());
assertThat(responseEntity.getBody().title()).isEqualTo(updateMovieRequest.title());
assertThat(responseEntity.getBody().year()).isEqualTo(updateMovieRequest.year());
assertThat(responseEntity.getBody().actors()).isEqualTo(updateMovieRequest.actors());
Optional movieOptional = movieRepository.findById(responseEntity.getBody().imdbId());
assertThat(movieOptional.isPresent()).isTrue();
movieOptional.ifPresent(movieUpdated -> {
assertThat(movieUpdated.getImdbId()).isEqualTo(movie.getImdbId());
assertThat(movieUpdated.getTitle()).isEqualTo(updateMovieRequest.title());
assertThat(movieUpdated.getYear()).isEqualTo(updateMovieRequest.year());
assertThat(movieUpdated.getActors()).isEqualTo(updateMovieRequest.actors());
});
}
@Test
void testDeleteMovieWhenNonExistent() {
String imdbId = "123";
String url = API_MOVIES_IMDB_URL.formatted(imdbId);
ResponseEntity responseEntity = testRestTemplate.exchange(url, HttpMethod.DELETE, null, String.class);
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
void testDeleteMovieWhenExistent() {
Movie movie = getDefaultMovie();
movieRepository.save(movie);
String url = API_MOVIES_IMDB_URL.formatted(movie.getImdbId());
ResponseEntity responseEntity = testRestTemplate.exchange(url, HttpMethod.DELETE, null, MovieResponse.class);
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
Optional movieOptional = movieRepository.findById(movie.getImdbId());
assertThat(movieOptional).isNotPresent();
}
private Movie getDefaultMovie() {
return new Movie("123", "title", 2023, "actors");}
private static final String API_MOVIES_URL = "/api/movies";
private static final String API_MOVIES_IMDB_URL = "/api/movies/%s";
}
This test class is responsible for validating various functionalities of the API through integration tests. Here's a brief overview:
Annotations
- @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT): Configures Spring Boot to start the application on a random port for testing.
- @ImportTestcontainers(MyContainers.class): Integrates Testcontainers configuration for running tests with external dependencies, such as the PostgreSQL container.
Dependencies
- TestRestTemplate: Automatically injected for performing HTTP requests and validating responses during testing.
- MovieRepository: Also auto-injected to facilitate interactions with the PostgreSQL database for operations related to movies.
Test Methods
- setUp(): Clears the PostgreSQL movies collection before each test.
- testGetMoviesWhenThereIsNone: Verifies the API's response when no movies are present.
- testGetMoviesWhenThereIsOne: Checks the API's response with one movie in the database.
- testGetMovieWhenNonExistent: Tests the API's behavior when trying to retrieve a non-existent movie.
- testGetMovieWhenExistent: Tests retrieving an existing movie.
- testCreateMovie: Tests the functionality of creating a new movie.
- testUpdateMovie: Verifies the update operation for an existing movie.
- testDeleteMovieWhenNonExistent: Tests deletion attempts for a non-existent movie.
- testDeleteMovieWhenExistent: Validates the deletion of an existing movie.
Running Integration Tests
To execute the tests, open a terminal in the movie-api root directory and run:
./mvnw clean test
Testcontainers will launch a PostgreSQL Docker container before executing the integration tests, and all tests should pass successfully.
Conclusion
This article outlined the process of implementing integration tests for a Spring Boot application, specifically the Movie API. We utilized Testcontainers to set up a PostgreSQL Docker container for testing. Finally, we confirmed that all test cases were executed successfully.
Support and Engagement
If you found this article helpful and wish to support me, consider taking the following actions:
- π Engage by clapping, highlighting, and replying to my story; I'm happy to answer any of your questions.
- π Share this article on social media.
- π Follow me on Medium, LinkedIn, and Twitter.
- βοΈ Subscribe to my newsletter to stay updated on my latest posts.