livesdmo.com

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.

Share the page:

Twitter Facebook Reddit LinkIn

-----------------------

Recent Post:

Mastering the Art of Happiness: A Guide to Self-Love and Growth

Discover three effective strategies to cultivate happiness and self-love, even in challenging times. Begin your journey toward a fulfilling life today.

Exploring the Magnification of Distant Galaxies in an Expanding Universe

Discover how distant galaxies may appear magnified due to the expanding universe, challenging our understanding of cosmic distances.

Sonic Branding: Unlocking New Potential for Sonic Drive In

Exploring how Sonic Drive In can leverage sonic branding to enhance customer experience and brand identity.

Mastering the Essential Skill of Curve Sketching

Explore the importance of curve sketching in mathematics and its relevance in today's technology-driven world.

Understanding Procrastination: Unraveling Its Effects on Life

Explore the nature of procrastination, its psychological roots, and how to combat it effectively.

Lessons from Steve Jobs: Insights for Innovation and Growth

Discover valuable lessons from Steve Jobs on innovation, simplicity, and personal growth that can inspire your journey.

Embarking on a Transformative 8-Week Journey on Medium

Reflecting on my 8-week writing journey on Medium, sharing valuable lessons learned and insights for aspiring creators.

Transforming Policing: The Role of Facial Recognition Glasses

Exploring the potential and challenges of facial recognition glasses in law enforcement and their impact on civil liberties.