Issue
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));
}
Answered By - KellyM