Search code examples
javamongodbspring-bootjunitmockmvc

Getting NestedServletException: Request processing failed; nested exception is java.lang.IllegalStateException: Ambiguous handler methods mapped


I want to conduct a JUnit 5 integration test for my small application.

The problem occurs in the test which is sending a GET request with @PathVariable with MockMvc usage.

My controller looks like:

@RestController
@RequestMapping("/device")
public class DeviceController {

  private DeviceService deviceService;

  public DeviceController(DeviceService deviceService) {
    this.deviceService = deviceService;
  }

  @GetMapping
  public ResponseEntity<Set<DeviceDto>> findAllDevices() {
    return ResponseEntity.ok(deviceService.findAllDevices());
  }

  @RequestMapping(method = RequestMethod.GET, value = "/{id}")
  public ResponseEntity<Optional<DeviceDto>> findDeviceById(
      @PathVariable(value = "id") String id) {
    return ResponseEntity.ok(deviceService.findDeviceById(id));
  }

  @GetMapping("/{vendor}")
  public ResponseEntity<Set<DeviceDto>> findDevicesByVendor(@PathVariable String vendor) {
    return ResponseEntity.ok(deviceService.findAllDevicesByVendor(vendor));
  }

  @GetMapping("/{model}")
  public ResponseEntity<Set<DeviceDto>> findDevicesByModel(@PathVariable String model) {
    return ResponseEntity.ok(deviceService.findAllDevicesByModel(model));
  }

  @GetMapping("/preview")
  public ResponseEntity<Set<DeviceDtoPreview>> findDevicesWithIpAndSubnetMask() {
    return ResponseEntity.ok(deviceService.findAllDevicesForPreview());
  }
}

MongoDB entity looks like:

@Document
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class Device {

  @Id
  @GeneratedValue(generator = "uuid")
  public String id;

  @Field("manufacturer")
  public String vendor;
  @Field("model")
  public String model;
  @Field("serialNumber")
  public String serNum;
  @Field("macAddress")
  private String mac;
  @Field("ip")
  private String ip;
  @Field("subnetMask")
  private String netMask;
}

Repository:

public interface DeviceRepository extends MongoRepository<Device, String> {

  Set<Device> findAllByVendor(String vendor);

  Set<Device> findAllByModel(String model);

}

Service methods:


public Optional<DeviceDto> findDeviceById(final String id) {
    return deviceRepository
        .findById(id)
        .map(DeviceMapper.MAPPER::deviceToDeviceDto);
  }

  public Set<DeviceDto> findAllDevicesByVendor(final String vendor) {
    return deviceRepository.findAllByVendor(vendor)
        .stream()
        .map(DeviceMapper.MAPPER::deviceToDeviceDto)
        .collect(Collectors.toSet());
  }

  public Set<DeviceDto> findAllDevicesByModel(final String model) {
    return deviceRepository.findAllByModel(model)
        .stream()
        .map(DeviceMapper.MAPPER::deviceToDeviceDto)
        .collect(Collectors.toSet());
  }

My ControllerTest looks:

package com.interview.exercise.controller;

import static org.mockito.BDDMockito.given;

import org.hamcrest.Matchers;
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.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = DeviceController.class)
class DeviceControllerTest {

  @Autowired
  private MockMvc mockMvc;

  @MockBean
  private DeviceRepository deviceRepository;

  @MockBean
  private DeviceService deviceService;

  @Test
  void addDevice() {
  }

  @Test
  void findAllDevicesOnGetRequest() throws Exception {
    var device1 = DeviceDto.builder().id(UUID.randomUUID().toString()).ip("192.168.0.101 ")
        .netMask("255.255.0.1 ").model("model1").vendor("vendor1").build();

    var device2 = DeviceDto.builder().id(UUID.randomUUID().toString()).ip("192.168.0.102 ")
        .netMask("255.255.0.2").model("model2").vendor("vendor2").build();

    var device3 = DeviceDto.builder().id(UUID.randomUUID().toString()).ip("192.168.0.103 ")
        .netMask("255.255.0.3 ").model("model3").vendor("vendor3").build();
    var devices = Set.of(device1, device2, device3);
    given(deviceService.findAllDevices()).willReturn(devices);

    ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.get("/device")
        .contentType(
            MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.jsonPath("$",
            Matchers.hasSize(3)));

  }

  @Test
  void findDeviceByIdOnGetRequest() throws Exception {
    var device = DeviceDto.builder().id("1").ip("192.168.0.101")
        .netMask("255.255.0.1 ").model("model1").vendor("vendor1").build();

    given(deviceService.findDeviceById("1")).willReturn(Optional.of(device));
    ResultActions resultMatchers = mockMvc.perform(MockMvcRequestBuilders
        .get("/device/{id}", "1")
        .contentType(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.jsonPath("$.model", Matchers.equalTo(device.getModel())));

  }

  @Test
  void findDevicesByVendorOnGetRequest() throws Exception {

    var device1 = DeviceDto.builder().id(UUID.randomUUID().toString()).ip("192.168.0.101 ")
        .netMask("255.255.0.1 ").model("model1").vendor("vendor1").build();

    var device2 = DeviceDto.builder().id(UUID.randomUUID().toString()).ip("192.168.0.102 ")
        .netMask("255.255.0.2").model("model2").vendor("vendor2").build();

    var device3 = DeviceDto.builder().id(UUID.randomUUID().toString()).ip("192.168.0.103 ")
        .netMask("255.255.0.3 ").model("model3").vendor("vendor3").build();
    var devices = Set.of(device1, device2, device3);
    given(deviceService.findAllDevicesByVendor("vendor3")).willReturn(devices);

    ResultActions resultActions = mockMvc
        .perform(MockMvcRequestBuilders
            .get("/device/{vendor}", "vendor3")
            .contentType(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.jsonPath("$",
            Matchers.hasSize(1)));

  }

  @Test
  void findDevicesWithIpAndSubnetMaskOnGetRequest() throws Exception {
    var device1 = DeviceDtoPreview.builder().id(UUID.randomUUID().toString()).ip("192.168.0.101 ")
        .netMask("255.255.0.1 ").build();

    var device2 = DeviceDtoPreview.builder().id(UUID.randomUUID().toString()).ip("192.168.0.102 ")
        .netMask("255.255.0.2").build();

    var device3 = DeviceDtoPreview.builder().id(UUID.randomUUID().toString()).ip("192.168.0.103 ")
        .netMask("255.255.0.3 ").build();

    var device4 = DeviceDtoPreview.builder().id(UUID.randomUUID().toString()).ip("192.168.0.103 ")
        .netMask("255.255.0.3 ").build();
    var devices = Set.of(device1, device2, device3, device4);
    given(deviceService.findAllDevicesForPreview()).willReturn(devices);

    ResultActions resultActions = mockMvc
        .perform(MockMvcRequestBuilders.get("/device/preview").contentType(
            MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.jsonPath("$",
            Matchers.hasSize(4)));
  }

  @Test
  void findDevicesByModelOnGetRequest() throws Exception {
    var device1 = DeviceDto.builder().id(UUID.randomUUID().toString()).ip("192.168.0.101 ")
        .netMask("255.255.0.1 ").model("model1").vendor("vendor1").build();

    var device2 = DeviceDto.builder().id(UUID.randomUUID().toString()).ip("192.168.0.102 ")
        .netMask("255.255.0.2").model("model2").vendor("vendor2").build();

    var device3 = DeviceDto.builder().id(UUID.randomUUID().toString()).ip("192.168.0.103 ")
        .netMask("255.255.0.3 ").model("model3").vendor("vendor3").build();

    var device4 = DeviceDto.builder().id(UUID.randomUUID().toString()).ip("192.168.0.103 ")
        .netMask("255.255.0.3 ").model("model2").vendor("vendor4").build();
    var devices = Set.of(device1, device2, device3, device4);
    given(deviceService.findAllDevicesByModel("model2")).willReturn(devices);

    ResultActions resultActions = mockMvc
        .perform(MockMvcRequestBuilders.get("/device/{model}", "model2").contentType(
            MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.jsonPath("$",
            Matchers.hasSize(4)));

  }

}

Tests without @PathVariable are passing but problem occurs with a test which contains @PathVariable. I was trying to use @RequestMapping in a different ways but it still producing the same error.

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.IllegalStateException: Ambiguous handler methods mapped for '/device/model2': {public org.springframework.http.ResponseEntity com.interview.exercise.controller.DeviceController.findDeviceById(java.lang.String), public org.springframework.http.ResponseEntity com.interview.exercise.controller.DeviceController.findDevicesByModel(java.lang.String)}

I am pretty convinced that in the past I was creating tests which were similar and everything was ok but now I can not reach a goal with GET request in my integration test. I will be grateful for advice how to fix my mockMvc.perform() method to achieve desirable effect.


Solution

  • I do not believe Spring is capable of distinguishing between @GetMapping("/{vendor}") and @GetMapping("/{model}"). Both have the the @PathVariable type (String) and base path, /device.

    To help Spring correctly map requests to a controller method, try something like this in the controller:

     @GetMapping("vendor/{vendorID}")
      public ResponseEntity<Set<DeviceDto>> findDevicesByVendor(@PathVariable String vendorID) {
        return ResponseEntity.ok(deviceService.findAllDevicesByVendor(vendorID));
      }
    
      @GetMapping("model/{modelID}")
      public ResponseEntity<Set<DeviceDto>> findDevicesByModel(@PathVariable String modelID) {
        return ResponseEntity.ok(deviceService.findAllDevicesByModel(modelID));
      }