javaspring-boottesting

Spring Boot Integration Test with @MockMvc Returning Empty Body


I’m having an issue with my Spring Boot integration test using @MockMvc. When I run the test, the jsonPath assertions fail because there is nothing in the response body ($).

package env.starwars.web;

import static org.mockito.Mockito.when;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import com.fasterxml.jackson.databind.ObjectMapper;

import env.starwars.domain.Planet;
import env.starwars.domain.PlanetService;
import env.starwars.factory.PlanetFactory;

@WebMvcTest()
@AutoConfigureMockMvc
public class PlanetControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @MockitoBean
    private PlanetService planetService;

    @Test
    public void createPlanet_WithValidData_ReturnCreated() throws Exception {
        Planet planet = PlanetFactory.create();
        when(planetService.create(planet)).thenReturn(planet);

        mockMvc.perform(MockMvcRequestBuilders.post("/planets")
                .content(objectMapper.writeValueAsString(planet)).contentType(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isCreated())
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.jsonPath("$.name").value(planet.getName()))
                .andExpect(MockMvcResultMatchers.jsonPath("$.terrain").value(planet.getTerrain()))
                .andExpect(MockMvcResultMatchers.jsonPath("$.climate").value(planet.getClimate()));
    }
}

My PlanetFactory:

package env.starwars.factory;

import env.starwars.domain.Planet;
import env.starwars.web.dto.PlanetDto;

public class PlanetFactory {

    public static Planet create() {
        return new Planet("name", "climate", "terrain");
    }

    public static Planet createWithInvalidData() {
        return new Planet("", "", "");
    }

    public static PlanetDto createDto() {
        return new PlanetDto(1L, "name", "climate", "terrain");
    }

    public static PlanetDto createAalothDto() {
        return new PlanetDto(1L, "Aaloth", "Unknown", "Terrestrial");
    }

    public static Planet createAaloth() {
        return new Planet(1L, "Aaloth", "Unknown", "Terrestrial");
    }
}

Here's a sample of the debug console output:

19:04:00.130 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [env.starwars.web.PlanetControllerTest]: PlanetControllerTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
19:04:00.247 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration env.starwars.StarWarsApplication for test class env.starwars.web.PlanetControllerTest
19:04:00.300 [main] INFO org.springframework.boot.devtools.restart.RestartApplicationListener -- Restart disabled due to context in which it is running

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.4.1)

2025-01-12T19:04:00.557-03:00  INFO 177734 --- [           main] env.starwars.web.PlanetControllerTest    : Starting PlanetControllerTest using Java 17.0.13 with PID 177734 (started by fynko in /home/fynko/codes/star-wars)
2025-01-12T19:04:00.558-03:00  INFO 177734 --- [           main] env.starwars.web.PlanetControllerTest    : No active profile set, falling back to 1 default profile: "default"
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2025-01-12T19:04:02.060-03:00  INFO 177734 --- [           main] o.s.t.web.servlet.TestDispatcherServlet  : Initializing Servlet ''
2025-01-12T19:04:02.063-03:00  INFO 177734 --- [           main] o.s.t.web.servlet.TestDispatcherServlet  : Completed initialization in 1 ms
2025-01-12T19:04:02.089-03:00  INFO 177734 --- [           main] env.starwars.web.PlanetControllerTest    : Started PlanetControllerTest in 1.792 seconds (process running for 2.431)

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /planets
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"65"]
             Body = {"id":null,"name":"name","climate":"climate","terrain":"terrain"}
    Session Attrs = {}

Handler:
             Type = env.starwars.web.PlanetController
           Method = env.starwars.web.PlanetController#create(Planet)
    Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 201
    Error message = null
    Headers = []
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /planets
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"65"]
             Body = {"id":null,"name":"name","climate":"climate","terrain":"terrain"}
    Session Attrs = {}

Handler:
             Type = env.starwars.web.PlanetController
           Method = env.starwars.web.PlanetController#create(Planet)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 201
    Error message = null
          Headers = []
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

My Controller:

package env.starwars.web;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import env.starwars.domain.Planet;
import env.starwars.domain.PlanetService;
import env.starwars.web.dto.PlanetDto;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@RestController
@RequestMapping("/planets")
public class PlanetController {

    @Autowired
    private PlanetService planetService;

    @PostMapping()
    public ResponseEntity<Planet> create(@RequestBody Planet planet) {
        Planet planetCreated = planetService.create(planet);

        return ResponseEntity.status(HttpStatus.CREATED).body(planetCreated);
    }

    @GetMapping("/{id}")
    public ResponseEntity<PlanetDto> getPlanetById(@PathVariable(name = "id") Long id) {
        return planetService.getById(id).map(planet -> ResponseEntity.ok(planet))
                .orElseGet(() -> ResponseEntity.notFound().build());
    }

    @GetMapping("/name/{name}")
    public ResponseEntity<PlanetDto> getPlanetByName(@PathVariable(name = "name") String name) {
        return planetService.getByName(name).map(planet -> ResponseEntity.ok(planet))
                .orElseGet(() -> ResponseEntity.notFound().build());
    }

    @GetMapping
    public ResponseEntity<List<PlanetDto>> listPlanets(@RequestParam(required = false) String terrain,
            @RequestParam(required = false) String climate) {
        List<PlanetDto> planets = planetService.listPlanets(terrain, climate);
        return ResponseEntity.ok().body(planets);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<PlanetDto> deletePlanetById(@PathVariable(name = "id") Long id) {
        planetService.deleteById(id);
        return ResponseEntity.noContent().build();
    }
}

My service:

package env.starwars.domain;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.springframework.data.domain.Example;
import org.springframework.stereotype.Service;

import env.starwars.utils.MapperPlanet;
import env.starwars.web.dto.PlanetDto;

@Service
public class PlanetService {

    private final PlanetRepository planetRepository;
    private final MapperPlanet mapperPlanet;

    public PlanetService(PlanetRepository planetRepository, MapperPlanet mapperPlanet) {
        this.planetRepository = planetRepository;
        this.mapperPlanet = mapperPlanet;
    }

    public Planet create(Planet planet) {
        return planetRepository.save(planet);
    }

    public Optional<PlanetDto> getById(Long id) {
        Optional<Planet> planet = planetRepository.findById(id);

        if (planet.isPresent()) {
            PlanetDto planetDto = mapperPlanet.toPlanetDto(planet.get());
            return Optional.of(planetDto);
        }

        return Optional.empty();
    }

    public Optional<PlanetDto> getByName(String name) {
        Optional<Planet> planet = planetRepository.findByName(name);

        if (planet.isPresent()) {
            PlanetDto planetDto = mapperPlanet.toPlanetDto(planet.get());
            return Optional.of(planetDto);
        }

        return Optional.empty();
    }

    public List<PlanetDto> listPlanets(String terrain, String climate) {
        Example<Planet> query = QueryBuilder.makeQuery(new Planet(climate, terrain));

        return planetRepository.findAll(query).stream().map(mapperPlanet::toPlanetDto)
                .collect(Collectors.toList());
    }

    public void deleteById(Long id) {
        planetRepository.deleteById(id);
    }
}

Planet:

package env.starwars.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotEmpty;

@Entity
@Table(name = "planets")
public class Planet {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true, nullable = false)
    @NotEmpty()
    private String name;
    @Column(nullable = false)
    @NotEmpty()
    private String climate;
    @Column(nullable = false)
    @NotEmpty()
    private String terrain;

    public Planet(String climate, String terrain) {
        this.climate = climate;
        this.terrain = terrain;
    }

    public Planet(String name, String climate, String terrain) {
        this.name = name;
        this.climate = climate;
        this.terrain = terrain;
    }

    public Planet(Long id, String name, String climate, String terrain) {
        this.id = id;
        this.name = name;
        this.climate = climate;
        this.terrain = terrain;
    }

    public Planet() {

    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getClimate() {
        return climate;
    }

    public void setClimate(String climate) {
        this.climate = climate;
    }

    public String getTerrain() {
        return terrain;
    }

    public void setTerrain(String terrain) {
        this.terrain = terrain;
    }

    @Override
    public String toString() {
        return "Planet [id=" + id + ", name=" + name + ", climate=" + climate + ", terrain=" + terrain + "]";
    }

}

Here is a screenshot

Anyone can help me? I would really appreciate any help in understanding why the response body is empty in my integration test. Thank you in advance!


Solution

  • You don't have an equals() method on your Planet class, so this stubbing:

    when(planetService.create(planet)).thenReturn(planet);
    

    doesn't match, and planetService.create() is returning null.

    Using your Planet class I was able to replicate your issue, but by adding an equals() method to Planet, the test worked.