Mockito 和 JUnit 控制器在任何请求主体上测试 returns 400 json

Mockito and JUnit Controller test returns 400 on any request body json

我有一个到补丁数据的映射:

@Data
@NoArgsConstructor
public class CertificateUpdateDTO {
    @InjectString
    private String name;
    @InjectString
    private String description;
    @Min(value = 0, message = "Price cannot be negative")
    private double price;
    @Min(value = 1, message = "Duration cannot be less than one day")
    private int duration;
}
 

@ApiOperation("Update certificate by id")
    @ApiResponses({
            @ApiResponse(code = 200, message = "If updated successfully or certificate doesn't exist"),
            @ApiResponse(code = 400, message = "If JSON object in request body is invalid"),
            @ApiResponse(code = 404, message = "Certificate with given id doesn't exist")
    })
    @PatchMapping(value = "/{id}", consumes = {MediaType.APPLICATION_JSON_VALUE})
    public ResponseEntity update(@PathVariable int id, @Valid @DefaultDto CertificateUpdateDTO certificateUpdateDTO){
        if(certificateService.updateCertificate(id, certificateUpdateDTO)){
            return ResponseEntity.ok().build();
        }

        return ResponseEntity.notFound().build();
    }

@DefaultDto@InjectString 是我的注解,它们只是做与@RequestBody 相同的事情,并将默认值注入由 @InjectString 注释的字符串字段,它工作正常。

我需要检查是否请求此映射的 ID 1 returns 404,它从 curl:

开始工作
curl -X PATCH -H "Content-Type: application/json" -d '{"duration": "10", "price": "10"}' localhost:8080/v1/certificate/1

curl的响应码是404。 但是当我尝试 运行 测试它时 returns 400:

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = ControllerTestConfiguration.class, loader = AnnotationConfigContextLoader.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class CertificateControllerTest {

    @Autowired
    private CertificateService certificateService;
    @Autowired
    private CertificateController certificateController;

    private ObjectMapper objectMapper = new ObjectMapper();
    private MockMvc mockMvc;

    @BeforeAll
    public void init(){
        mockMvc = MockMvcBuilders.standaloneSetup(certificateController).build();
    }

    @AfterEach
    public void postEach(){
        reset(certificateService);
    }

    ...    

    @Test
    @SneakyThrows
    public void updateFailTest(){
        CertificateUpdateDTO certificateUpdateDTO = new CertificateUpdateDTO();
        certificateUpdateDTO.setName("c1");
        certificateUpdateDTO.setDescription("desk");
        certificateUpdateDTO.setDuration(10);
        certificateUpdateDTO.setPrice(10);

        when(certificateService.updateCertificate(1, certificateUpdateDTO)).thenReturn(false);

        mockMvc.perform(patch("/v1/certificate/1")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(certificateUpdateDTO))
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().is(404));

        verify(certificateService, times(1)).updateCertificate(1, certificateUpdateDTO);
    }
}

当前测试class的配置是:

@Configuration
public class ControllerTestConfiguration {

    ...

    @Bean
    public CertificateService certificateService(){
        return mock(CertificateService.class);
    }

    @Bean
    public CertificateController certificateController(){
        return new CertificateController(certificateService());
    }

    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

}

错误消息说持续时间无效:duration = 0

UPD:

我试图删除@Valid,发现我的注释在测试环境中不起作用,我是如何处理它们的:

public class DefaultValueHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return methodParameter.getParameterAnnotation(DefaultDto.class) != null;
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        HttpServletRequest servletRequest = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
        BufferedReader reader = servletRequest.getReader();
        String body = reader.lines().collect(Collectors.joining());
        Class<?> clazz = methodParameter.getParameterType();
        Object dto = new ObjectMapper().readValue(body, clazz);

        for(Field field : clazz.getDeclaredFields()){
            InjectString annotation = field.getAnnotation(InjectString.class);
            if(annotation != null){
                field.setAccessible(true);
                if(ReflectionUtils.getField(field, dto) == null) {
                    ReflectionUtils.setField(field, dto, annotation.value());
                }
            }
        }

        return dto;
    }
}

以及我如何注册 MethodResolver:

public class AppConfig implements WebMvcConfigurer {

    private final Environment env;

    @Autowired
    public AppConfig(Environment env) {
        this.env = env;
    }

    ...

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new DefaultValueHandlerMethodArgumentResolver());
    }
    
    ...

}

经过一些研究和我的 leed fiend 的帮助,我找到了解决方案:)

要添加自定义 ArgumentResolver,您应该在 MockMvcBuilder 中注册它。

改变这个:

mockMvc = MockMvcBuilders.standaloneSetup(certificateController).build();

为此:

mockMvc = MockMvcBuilders.standaloneSetup(certificateController).setCustomArgumentResolvers(new DefaultValueHandlerMethodArgumentResolver()).build();