Testing Spring Boot CRUD REST APIs with MockMvc - Practical Example

Testing is a crucial part of any application development process, especially when building RESTful APIs with Spring Boot. In this blog post, we’ll walk you through a practical example of testing CRUD (Create, Read, Update, Delete) REST APIs for a Person entity using Spring Data JPA with H2 as the database. We’ll create a Maven project from scratch using Spring Initializr, write JUnit 5 tests, and demonstrate how to test the PersonController using MockMvc. Let’s get started!

Prerequisites

If you don’t already have Maven installed, you can download it from the official Maven website https://maven.apache.org/download.cgi or through SDKMAN https://sdkman.io/sdks#maven

You can clone the https://github.com/dmakariev/examples repository.

git clone https://github.com/dmakariev/examples.git
cd examples/spring-boot/crud-mockmvc

Creating a Maven Project

First, let’s create a new Spring Boot project with Spring Initializr using the terminal.

  1. Open your terminal and navigate to the directory where you want to create your project.
  2. Run the following command to generate a new Maven project:
    curl https://start.spring.io/starter.tgz -d packaging=jar \
    -d dependencies=data-jpa,web,h2,lombok \
    -d baseDir=crud-mockmvc -d artifactId=crud-mockmvc \
    -d name=crud-mockmvc -d type=maven-project \
    -d groupId=com.makariev.examples.spring | tar -xzvf -
    
  3. This command will download the Spring Initializr project and extract it into a directory named crud-mockmvc.
  4. Next, open the project in your favorite IDE (such as NetBeans, IntelliJ IDEA, VSCode or Eclipse) to proceed.

Creating the Entity: Person

We’ll create a simple Person entity with firstName, lastName, and birthYear fields.

package com.makariev.examples.spring.crudmockmvc.person;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data // generates getters, setters, equals and hashCode and constructor
@NoArgsConstructor //generates default constructor
@Entity
public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String firstName;
    private String lastName;
    private int birthYear;

    public Person(String firstName, String lastName, int birthYear) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.birthYear = birthYear;
    }

}

Creating the Repository: PersonRepository

package com.makariev.examples.spring.crudmockmvc.person;

import org.springframework.data.jpa.repository.JpaRepository;

public interface PersonRepository extends JpaRepository<Person, Long> {

}

Implementing the CRUD REST API

Now, let’s create a RESTful CRUD API to manage Person entities using the PersonController. The controller will handle HTTP requests to perform operations like creating, retrieving, updating, and deleting Person objects.

package com.makariev.examples.spring.crudmockmvc.person;

import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor // generates constructor for all 'final' fields 
@RestController
@RequestMapping("/api/persons")
public class PersonController {

    private final PersonRepository personRepository;

    @GetMapping
    public Page<Person> findAll(Pageable pageable) {
        return personRepository.findAll(pageable);
    }

    @GetMapping("{id}")
    public Optional<Person> findById(@PathVariable("id") Long id) {
        return personRepository.findById(id);
    }

    @PostMapping
    @ResponseStatus( HttpStatus.CREATED )
    public Person create(@RequestBody Person person) {
        return personRepository.save(person);
    }

    @PutMapping("{id}")
    public Person updateById(@PathVariable("id") Long id, @RequestBody Person person) {
        var loaded = personRepository.findById(id).orElseThrow();
        loaded.setFirstName(person.getFirstName());
        loaded.setLastName(person.getLastName());
        loaded.setBirthYear(person.getBirthYear());
        return personRepository.save(loaded);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus( HttpStatus.NO_CONTENT )
    public void deleteById(@PathVariable("id") Long id) {
        personRepository.deleteById(id);
    }
}

Run the Application

Return to your terminal. Navigate to the directory containing your project. Execute

./mvnw spring-boot:run

Access the Application from Terminal/CLI

Open your web browser and navigate to http://localhost:8080/hi. You should see the “Hello, World!” message displayed in your browser. Or if you prefer more personalized message, then navigate to http://localhost:8080/hi?name=Joe. You should see the “Hello, Joe!” message displayed in your browser.

Using the Rest Api with Curl

To create a new person, use the POST method with the person data as a JSON body:

curl -X POST -H "Content-Type: application/json" \
-d '{"firstName":"Katherine", "lastName":"Johnson", "birthYear":1919}' \
http://localhost:8080/api/persons

To get a list of persons, use the GET method:

curl -X GET http://localhost:8080/api/persons

To get a list of persons, from page 1, when the page size is 1

curl -X GET http://localhost:8080/api/persons -G -d page=0 -d size=1

To get a list of persons, sorted by firstName:

curl -X GET http://localhost:8080/api/persons -G -d sort=firstName,asc
curl -X GET http://localhost:8080/api/persons -G -d sort=firstName,desc
curl -X GET http://localhost:8080/api/persons -G -d sort=firstName

To get a list of persons, from page 3, when the page size is 1, sorted by firstName

curl -X GET http://localhost:8080/api/persons -G -d page=2 -d size=1 -d sort=firstName,asc

To get a list of persons, sorted by firstName and lastName:

curl -X GET http://localhost:8080/api/persons -G -d sort=firstName,asc -d sort=lastName,desc
curl -X GET http://localhost:8080/api/persons -G -d sort=firstName,desc -d sort=lastName,asc
curl -X GET http://localhost:8080/api/persons -G -d sort=firstName -d sort=lastName

To get a specific person by id, use the GET method with the id as a path variable:

curl -X GET http://localhost:8080/api/persons/1

To update an existing person by id, use the PUT method with the person data as a JSON body:

curl -X PUT -H "Content-Type: application/json" \
-d '{"firstName":"Katherine", "lastName":"Johnson", "birthYear":1918}' \
http://localhost:8080/api/persons/1

Using the Rest Api with HTTPIE

you could download alternative Terminal/CLI client from here https://httpie.io/cli

To create a new person, use the POST method with the person data as a JSON body:

http POST http://localhost:8080/api/persons firstName=Alice lastName=Smith birthYear=1996

To get a list of persons, use the GET method:

http GET http://localhost:8080/api/persons

To get a list of persons, from page 1, when the page size is 1

http GET http://localhost:8080/api/persons page==0 size==1

To get a list of persons, sorted by firstName:

http GET http://localhost:8080/api/persons sort==firstName,asc
http GET http://localhost:8080/api/persons sort==firstName,desc
http GET http://localhost:8080/api/persons sort==firstName

To get a list of persons, from page 3, when the page size is 1, sorted by firstName

http GET http://localhost:8080/api/persons page==2 size==1 sort==firstName,asc

To get a list of persons, sorted by firstName and lastName:

http GET http://localhost:8080/api/persons sort==firstName,asc sort==lastName,desc
http GET http://localhost:8080/api/persons sort==firstName,desc sort==lastName,asc
http GET http://localhost:8080/api/persons sort==firstName sort==lastName

To get a specific person by id, use the GET method with the id as a path variable:

http GET http://localhost:8080/api/persons/1

To update an existing person by id, use the PUT method with the person data as a JSON body:

http PUT http://localhost:8080/api/persons/1 firstName=Bob lastName=Jones birthYear=1990

To delete an existing person by id, use the DELETE method with the id as a path variable:

http DELETE http://localhost:8080/api/persons/1

Writing JUnit Tests with MockMvc

We’ll create JUnit 5 tests for the PersonController using MockMvc, which allows us to simulate HTTP requests and validate the responses.

Let’s create a test class PersonControllerTest in the src/test/java/com/makariev/examples/spring/crudmockmvc/person directory:

package com.makariev.examples.spring.crudmockmvc.person;

import java.util.List;

import com.fasterxml.jackson.databind.ObjectMapper;
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.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.hamcrest.CoreMatchers.is;
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.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class PersonControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private PersonRepository personRepository;

    // Test method for creating a new Person
    @Test
    void shouldCreateNewPerson() throws Exception {
        // Create a new Person instance
        final Person person = new Person();
        person.setFirstName("John");
        person.setLastName("Doe");
        person.setBirthYear(1980);

        // Perform a POST request to create the Person
        mockMvc.perform(post("/api/persons")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(person)))
                .andDo(print())
                .andExpect(status().isCreated());

        // Verify that the Person was created in the database
        assertThat(personRepository.count()).isEqualTo(1);

        //clean the database
        personRepository.deleteAll();
    }

    // Test method for retrieving a Person by ID
    @Test
    void shouldRetrievePersonById() throws Exception {
        // Create a new Person and save it in the database
        final Person savedPerson = personRepository.save(new Person("Alice", "Smith", 1990));

        // Perform a GET request to retrieve the Person by ID
        mockMvc.perform(get("/api/persons/{id}", savedPerson.getId()))
                .andExpect(status().isOk())
                .andDo(print())
                .andExpect(jsonPath("$.firstName", is(savedPerson.getFirstName())))
                .andExpect(jsonPath("$.lastName", is(savedPerson.getLastName())))
                .andExpect(jsonPath("$.birthYear", is(savedPerson.getBirthYear())));

        //clean the database
        personRepository.delete(savedPerson);
    }

    // Test method for updating a Person
    @Test
    void shouldUpdatePerson() throws Exception {
        // Create a new Person and save it in the database
        final Person savedPerson = personRepository.save(new Person("Bob", "Johnson", 1985));

        // Update the Person's information
        savedPerson.setFirstName("UpdatedFirstName");
        savedPerson.setLastName("UpdatedLastName");

        // Perform a PUT request to update the Person
        mockMvc.perform(put("/api/persons/{id}", savedPerson.getId())
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(savedPerson)))
                .andDo(print())
                .andExpect(status().isOk());

        // Verify that the Person's information was updated in the database
        final Person updatedPerson = personRepository.findById(savedPerson.getId()).orElse(null);
        assertThat(updatedPerson).isNotNull();
        assertThat(updatedPerson.getFirstName()).isEqualTo("UpdatedFirstName");
        assertThat(updatedPerson.getLastName()).isEqualTo("UpdatedLastName");

        //clean the database
        personRepository.delete(savedPerson);
    }

    // Test method for deleting a Person
    @Test
    void shouldDeletePerson() throws Exception {
        // Create a new Person and save it in the database
        final Person savedPerson = personRepository.save(new Person("Eve", "Williams", 2000));

        // Perform a DELETE request to delete the Person by ID
        mockMvc.perform(delete("/api/persons/{id}", savedPerson.getId()))
                .andDo(print())
                .andExpect(status().isNoContent());

        // Verify that the Person was deleted from the database
        assertThat(personRepository.existsById(savedPerson.getId())).isFalse();
    }

    // Test method for retrieving a list of persons
    @Test
    void shouldRetrievePersonList() throws Exception {
        // Create a new persons and save them in the database
        List.of(
                new Person("Alice", "Smith", 1990),
                new Person("Ada", "Lovelace", 1815),
                new Person("Niklaus", "Wirth", 1934),
                new Person("Donald", "Knuth", 1938),
                new Person("Edsger", "Dijkstra", 1930),
                new Person("Grace", "Hopper", 1906),
                new Person("John", "Backus", 1924)
        ).forEach(personRepository::save);

        // Perform a GET request to retrieve list of persons
        mockMvc.perform(
                get("/api/persons")
                        .param("page", "0")
                        .param("size", "1")
        ).andExpect(status().isOk())
                .andDo(print())
                .andExpect(jsonPath("$.totalElements", is(7)))
                .andExpect(jsonPath("$.numberOfElements", is(1)));

        // Perform a GET request to retrieve list of persons, 
        // from page 3, when the page size is 1, sorted by `firstName`
        mockMvc.perform(
                get("/api/persons")
                        .param("page", "2")
                        .param("size", "1")
                        .param("sort", "firstName,asc")
        ).andExpect(status().isOk())
                .andDo(print())
                .andExpect(jsonPath("$.totalElements", is(7)))
                .andExpect(jsonPath("$.numberOfElements", is(1)))
                .andExpect(jsonPath("$.content[0].firstName", is("Donald")));

        //clean the database
        personRepository.deleteAll();
    }
}

Running the Test

To run the test, execute the following command in the project’s root directory:

./mvnw test

JUnit 5 and AssertJ will execute the test, and you should see output indicating whether the test passed or failed.

Conclusion

In this blog post, we’ve demonstrated how to create a Spring Boot project for testing CRUD REST APIs using MockMvc. We’ve set up a Person entity, implemented the CRUD operations in the PersonController, and written JUnit tests in PersonControllerTest.java to validate the API endpoints. Proper testing ensures the reliability and correctness of your RESTful services.


Coffee Time!

Happy coding!

Share: X (Twitter) LinkedIn