Implementing Unit Tests for Movie API with Spring Boot and PostgreSQL
Written on
Unit Testing in Spring Boot: A Comprehensive Guide
In this guide, we'll delve into the process of setting up unit tests for a Spring Boot application, specifically focusing on a Movie API. You can access the full implementation and code in the linked article below. Follow the steps outlined here to kick-start your testing journey.
Unit testing is a critical software testing methodology that emphasizes the isolation and evaluation of individual components within an application. For the Movie API, this means testing each class, method, or component independently to confirm that they function as expected.
Furthermore, we will examine the various annotations and utilities provided by Spring Boot that facilitate unit testing. Let’s get started!
Setting Up the Movie API
Create Necessary Packages
Within the src/test/java directory, create the following packages under the com.example.movieapi root package: controller, mapper, and service.
Disable the MovieApiApplicationTests Class
To avoid confusion, we will disable the MovieApiApplicationTests class generated during project setup through Spring Initializr, as we won't be adding test cases here. Insert the following lines of code:
package com.example.movieapi;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@Disabled
@SpringBootTest
class MovieApiApplicationTests {
@Test
void contextLoads() {
}
}
Create the MovieMapperImplTest Class
Now, in the mapper package, create the MovieMapperImplTest class with the following content:
package com.example.movieapi.mapper;
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 org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(SpringExtension.class)
@Import(MovieMapperImpl.class)
class MovieMapperImplTest {
@Autowired
private MovieMapper movieMapper;
@Test
void testToMovie() {
CreateMovieRequest createMovieRequest = new CreateMovieRequest("123", "title", 2023, "actors");
Movie movie = movieMapper.toMovie(createMovieRequest);
assertThat(movie.getImdbId()).isEqualTo("123");
assertThat(movie.getTitle()).isEqualTo("title");
assertThat(movie.getYear()).isEqualTo(2023);
assertThat(movie.getActors()).isEqualTo("actors");
}
@ParameterizedTest
@MethodSource("provideUpdateMovieRequests")
void testUpdateMovieFromUpdateMovieRequest(UpdateMovieRequest updateMovieRequest, Movie expectedMovie) {
Movie movie = new Movie("123", "title", 2023, "actors");
movieMapper.updateMovieFromUpdateMovieRequest(movie, updateMovieRequest);
assertThat(movie.getImdbId()).isEqualTo(expectedMovie.getImdbId());
assertThat(movie.getTitle()).isEqualTo(expectedMovie.getTitle());
assertThat(movie.getYear()).isEqualTo(expectedMovie.getYear());
assertThat(movie.getActors()).isEqualTo(expectedMovie.getActors());
}
private static Stream<Arguments> provideUpdateMovieRequests() {
return Stream.of(
Arguments.of(new UpdateMovieRequest("newTitle", null, null), new Movie("123", "newTitle", 2023, "actors")),
Arguments.of(new UpdateMovieRequest(null, 2024, null), new Movie("123", "title", 2024, "actors")),
Arguments.of(new UpdateMovieRequest(null, null, "newActors"), new Movie("123", "title", 2023, "newActors")),
Arguments.of(new UpdateMovieRequest("newTitle", 2024, "newActors"), new Movie("123", "newTitle", 2024, "newActors"))
);
}
@Test
void testToMovieResponse() {
Movie movie = new Movie("123", "title", 2023, "actors");
MovieResponse movieResponse = movieMapper.toMovieResponse(movie);
assertThat(movieResponse.imdbId()).isEqualTo("123");
assertThat(movieResponse.title()).isEqualTo("title");
assertThat(movieResponse.year()).isEqualTo(2023);
assertThat(movieResponse.actors()).isEqualTo("actors");
}
}
The MovieMapperImplTest class is designed to verify the functionality of the MovieMapperImpl class, which handles the mapping between various movie-related data transfer objects (DTOs) and the Movie entity. Utilizing JUnit 5 with SpringExtension, it ensures seamless Spring integration for testing.
Create the MovieServiceImplTest Class
Next, in the service package, establish the MovieServiceImplTest class as follows:
package com.example.movieapi.service;
import com.example.movieapi.exception.MovieNotFoundException;
import com.example.movieapi.model.Movie;
import com.example.movieapi.repository.MovieRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
@ExtendWith(SpringExtension.class)
@Import(MovieServiceImpl.class)
class MovieServiceImplTest {
@Autowired
private MovieService movieService;
@MockBean
private MovieRepository movieRepository;
@Test
void testValidateAndGetMovieByIdWhenExisting() {
Movie movie = new Movie("123", "title", 2023, "actors");
given(movieRepository.findById(anyString())).willReturn(Optional.of(movie));
Movie movieFound = movieService.validateAndGetMovieById(movie.getImdbId());
assertThat(movieFound).isEqualTo(movie);}
@Test
void testValidateAndGetMovieByIdWhenNonExisting() {
given(movieRepository.findById(anyString())).willReturn(Optional.empty());
Throwable exception = assertThrows(MovieNotFoundException.class,
() -> movieService.validateAndGetMovieById("123"));assertThat(exception.getMessage()).isEqualTo("Movie with id '123' not found");
}
}
The MovieServiceImplTest class aims to validate the behavior of the MovieServiceImpl class, which manages movie data. It tests scenarios when retrieving movies by their ID, both when the movie is present and absent in the repository.
Additional Test Classes
Next, we will create the DTO test classes in the controller package, including CreateMovieRequestTest, UpdateMovieRequestTest, and MovieResponseTest.
CreateMovieRequestTest Class:
package com.example.movieapi.controller.dto;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.test.json.JsonContent;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
@JsonTest
class CreateMovieRequestTest {
@Autowired
private JacksonTester<CreateMovieRequest> jacksonTester;
@Test
void testSerialize() throws IOException {
CreateMovieRequest createMovieRequest = new CreateMovieRequest("123", "title", 2023, "actors");
JsonContent<CreateMovieRequest> jsonContent = jacksonTester.write(createMovieRequest);
assertThat(jsonContent)
.hasJsonPathStringValue("@.imdbId")
.extractingJsonPathStringValue("@.imdbId").isEqualTo("123");
assertThat(jsonContent)
.hasJsonPathStringValue("@.title")
.extractingJsonPathStringValue("@.title").isEqualTo("title");
assertThat(jsonContent)
.hasJsonPathNumberValue("@.year")
.extractingJsonPathNumberValue("@.year").isEqualTo(2023);
assertThat(jsonContent)
.hasJsonPathStringValue("@.actors")
.extractingJsonPathStringValue("@.actors").isEqualTo("actors");
}
@Test
void testDeserialize() throws IOException {
String content = "{"imdbId":"123","title":"title","year":2023,"actors":"actors"}";
CreateMovieRequest createMovieRequest = jacksonTester.parseObject(content);
assertThat(createMovieRequest.imdbId()).isEqualTo("123");
assertThat(createMovieRequest.title()).isEqualTo("title");
assertThat(createMovieRequest.year()).isEqualTo(2023);
assertThat(createMovieRequest.actors()).isEqualTo("actors");
}
}
UpdateMovieRequestTest Class:
package com.example.movieapi.controller.dto;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.test.json.JsonContent;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
@JsonTest
class UpdateMovieRequestTest {
@Autowired
private JacksonTester<UpdateMovieRequest> jacksonTester;
@Test
void testSerialize() throws IOException {
UpdateMovieRequest updateMovieRequest = new UpdateMovieRequest("title", 2023, "actors");
JsonContent<UpdateMovieRequest> jsonContent = jacksonTester.write(updateMovieRequest);
assertThat(jsonContent)
.doesNotHaveJsonPath("@.imdbId");
assertThat(jsonContent)
.hasJsonPathStringValue("@.title")
.extractingJsonPathStringValue("@.title").isEqualTo("title");
assertThat(jsonContent)
.hasJsonPathNumberValue("@.year")
.extractingJsonPathNumberValue("@.year").isEqualTo(2023);
assertThat(jsonContent)
.hasJsonPathStringValue("@.actors")
.extractingJsonPathStringValue("@.actors").isEqualTo("actors");
}
@Test
void testDeserialize() throws IOException {
String content = "{"title":"title","year":2023,"actors":"actors"}";
UpdateMovieRequest updateMovieRequest = jacksonTester.parseObject(content);
assertThat(updateMovieRequest.title()).isEqualTo("title");
assertThat(updateMovieRequest.year()).isEqualTo(2023);
assertThat(updateMovieRequest.actors()).isEqualTo("actors");
}
}
MovieResponseTest Class:
package com.example.movieapi.controller.dto;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.test.json.JsonContent;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
@JsonTest
class MovieResponseTest {
@Autowired
private JacksonTester<MovieResponse> jacksonTester;
@Test
void testSerialize() throws IOException {
MovieResponse movieResponse = new MovieResponse("123", "title", 2023, "actors");
JsonContent<MovieResponse> jsonContent = jacksonTester.write(movieResponse);
assertThat(jsonContent)
.hasJsonPathStringValue("@.imdbId")
.extractingJsonPathStringValue("@.imdbId").isEqualTo("123");
assertThat(jsonContent)
.hasJsonPathStringValue("@.title")
.extractingJsonPathStringValue("@.title").isEqualTo("title");
assertThat(jsonContent)
.hasJsonPathNumberValue("@.year")
.extractingJsonPathNumberValue("@.year").isEqualTo(2023);
assertThat(jsonContent)
.hasJsonPathStringValue("@.actors")
.extractingJsonPathStringValue("@.actors").isEqualTo("actors");
}
@Test
void testDeserialize() throws IOException {
String content = "{"imdbId":"123","title":"title","year":2023,"actors":"actors"}";
MovieResponse movieResponse = jacksonTester.parseObject(content);
assertThat(movieResponse.imdbId()).isEqualTo("123");
assertThat(movieResponse.title()).isEqualTo("title");
assertThat(movieResponse.year()).isEqualTo(2023);
assertThat(movieResponse.actors()).isEqualTo("actors");
}
}
These DTO test classes are essential for validating the correct serialization and deserialization of the various Data Transfer Object classes using Jackson. The @JsonTest annotation sets up a Spring test environment that includes automatic Jackson configuration, ensuring a smooth testing process.
Create the MovieControllerTest Class
Finally, in the controller package, create the MovieControllerTest class as follows:
package com.example.movieapi.controller;
import com.example.movieapi.controller.dto.CreateMovieRequest;
import com.example.movieapi.controller.dto.UpdateMovieRequest;
import com.example.movieapi.exception.MovieNotFoundException;
import com.example.movieapi.mapper.MovieMapperImpl;
import com.example.movieapi.model.Movie;
import com.example.movieapi.service.MovieService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import java.util.Collections;
import java.util.List;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willDoNothing;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(MovieController.class)
@Import(MovieMapperImpl.class)
class MovieControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private MovieService movieService;
@Test
void testGetMoviesWhenThereIsNone() throws Exception {
given(movieService.getMovies()).willReturn(Collections.emptyList());
ResultActions resultActions = mockMvc.perform(get(API_MOVIES_URL))
.andDo(print());
resultActions.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath(JSON_$, hasSize(0)));
}
@Test
void testGetMoviesWhenThereIsOne() throws Exception {
Movie movie = getDefaultMovie();
List<Movie> movies = Collections.singletonList(movie);
given(movieService.getMovies()).willReturn(movies);
ResultActions resultActions = mockMvc.perform(get(API_MOVIES_URL))
.andDo(print());
resultActions.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath(JSON_$, hasSize(1)))
.andExpect(jsonPath(JSON_$_0_IMDB_ID, is(movie.getImdbId())))
.andExpect(jsonPath(JSON_$_0_TITLE, is(movie.getTitle())))
.andExpect(jsonPath(JSON_$_0_YEAR, is(movie.getYear())))
.andExpect(jsonPath(JSON_$_0_ACTORS, is(movie.getActors())));
}
@Test
void testGetMovieByImdbIdWhenNonExistent() throws Exception {
given(movieService.validateAndGetMovieById(anyString())).willThrow(MovieNotFoundException.class);
ResultActions resultActions = mockMvc.perform(get(API_MOVIES_ID_URL, "123"))
.andDo(print());
resultActions.andExpect(status().isNotFound());
}
@Test
void testGetMovieByImdbIdWhenExistent() throws Exception {
Movie movie = getDefaultMovie();
given(movieService.validateAndGetMovieById(anyString())).willReturn(movie);
ResultActions resultActions = mockMvc.perform(get(API_MOVIES_ID_URL, movie.getImdbId()))
.andDo(print());
resultActions.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath(JSON_$_IMDB_ID, is(movie.getImdbId())))
.andExpect(jsonPath(JSON_$_TITLE, is(movie.getTitle())))
.andExpect(jsonPath(JSON_$_YEAR, is(movie.getYear())))
.andExpect(jsonPath(JSON_$_ACTORS, is(movie.getActors())));
}
@Test
void testCreateMovie() throws Exception {
Movie movie = getDefaultMovie();
given(movieService.saveMovie(any(Movie.class))).willReturn(movie);
CreateMovieRequest createMovieRequest = getDefaultCreateMovieRequest();
ResultActions resultActions = mockMvc.perform(post(API_MOVIES_URL)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(createMovieRequest)))
.andDo(print());
resultActions.andExpect(status().isCreated())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath(JSON_$_IMDB_ID, is(movie.getImdbId())))
.andExpect(jsonPath(JSON_$_TITLE, is(movie.getTitle())))
.andExpect(jsonPath(JSON_$_YEAR, is(movie.getYear())))
.andExpect(jsonPath(JSON_$_ACTORS, is(movie.getActors())));
}
@Test
void testUpdateMovie() throws Exception {
Movie movie = getDefaultMovie();
UpdateMovieRequest updateMovieRequest = getDefaultUpdateMovieRequest();
given(movieService.validateAndGetMovieById(anyString())).willReturn(movie);
given(movieService.saveMovie(any(Movie.class))).willReturn(movie);
ResultActions resultActions = mockMvc.perform(patch(API_MOVIES_ID_URL, movie.getImdbId())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updateMovieRequest)))
.andDo(print());
resultActions.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath(JSON_$_IMDB_ID, is(movie.getImdbId())))
.andExpect(jsonPath(JSON_$_TITLE, is(updateMovieRequest.title())))
.andExpect(jsonPath(JSON_$_YEAR, is(updateMovieRequest.year())))
.andExpect(jsonPath(JSON_$_ACTORS, is(updateMovieRequest.actors())));
}
@Test
void testDeleteMovieWhenExistent() throws Exception {
Movie movie = getDefaultMovie();
given(movieService.validateAndGetMovieById(anyString())).willReturn(movie);
willDoNothing().given(movieService).deleteMovie(any(Movie.class));
ResultActions resultActions = mockMvc.perform(delete(API_MOVIES_ID_URL, movie.getImdbId()))
.andDo(print());
resultActions.andExpect(status().isOk());
}
@Test
void testDeleteMovieWhenNonExistent() throws Exception {
given(movieService.validateAndGetMovieById(anyString())).willThrow(MovieNotFoundException.class);
ResultActions resultActions = mockMvc.perform(delete(API_MOVIES_ID_URL, "123"))
.andDo(print());
resultActions.andExpect(status().isNotFound());
}
private Movie getDefaultMovie() {
return new Movie("123", "title", 2023, "actors");}
private CreateMovieRequest getDefaultCreateMovieRequest() {
return new CreateMovieRequest("123", "title", 2023, "actors");}
private UpdateMovieRequest getDefaultUpdateMovieRequest() {
return new UpdateMovieRequest("newTitle", 2024, "newActors");}
private static final String API_MOVIES_URL = "/api/movies";
private static final String API_MOVIES_ID_URL = "/api/movies/{imdbId}";
private static final String JSON_$ = "$";
private static final String JSON_$_IMDB_ID = "$.imdbId";
private static final String JSON_$_TITLE = "$.title";
private static final String JSON_$_YEAR = "$.year";
private static final String JSON_$_ACTORS = "$.actors";
private static final String JSON_$_0_IMDB_ID = "$[0].imdbId";
private static final String JSON_$_0_TITLE = "$[0].title";
private static final String JSON_$_0_YEAR = "$[0].year";
private static final String JSON_$_0_ACTORS = "$[0].actors";
}
The MovieControllerTest class is intended to validate the functionality of the MovieController, which manages HTTP requests related to movie operations such as creating, retrieving, updating, and deleting movie data. It utilizes MockMvc for simulating requests and verifying responses.
Running Unit Tests
To execute the tests, navigate to the root folder of your Movie API project in a terminal and run the following command:
./mvnw clean test
Upon completion, all tests should pass successfully.
Conclusion
In this guide, we have explored the implementation of unit tests for a Spring Boot application known as the Movie API. We covered the testing of the mapper, service, controller, and DTOs. After running the test cases, we confirmed that all tests passed as expected.
Additional Resources
For further engagement and support, consider the following actions:
- Engage with this article by clapping, highlighting, or asking 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.
This video provides a step-by-step tutorial on creating a RESTful API using Spring Boot, PostgreSQL, and Java, which complements the content discussed in this article.
This video offers insights into using Testcontainers for integration testing in Spring Boot, simplifying the testing process and enhancing the quality of your applications.