whalebeings.com

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.

Share the page:

Twitter Facebook Reddit LinkIn

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

Recent Post:

Unlocking Your Earning Potential with Canva: 10 Proven Strategies

Discover 10 effective strategies to earn $600 daily using Canva, a versatile tool for entrepreneurs and creatives.

Mastering Optional Chaining and Nullish Coalescing in JavaScript

Uncover how Optional Chaining and Nullish Coalescing can streamline JavaScript coding by preventing errors related to null or undefined values.

# Stalin's Ambitious Project to Create Super Soldiers Through Ape Insemination

Explore Stalin's controversial attempts to engineer super soldiers by inseminating apes with human sperm, delving into the ethics and implications.