我们最近将应用程序升级到 Spring 6、java JDK21,现在正在使用
jakarta.validation-api
。此外,我们还有一个扩展 ResponseEntityExceptionHandler
的自定义类。当我们启动应用程序以测试错误请求时,我们确实会调用 handleMethodArgumentNotValid()
方法,因此测试类之外不会出现问题。我们正在测试类中使用独立的mockMvc 实现来测试此方法重写。当我们对控制器执行请求时,应该调用 @Valid
,因为意识到 @RequestBody
缺少参数,但控制器仍然返回 200。
spring 5 和 spring 6 之间发生了什么变化,我们无法利用
MockMvc
独立在测试类中调用 @Valid
?有人遇到过这种情况吗
我们尝试在调用
MethodArgumentNotValidException
中的服务类时抛出 FakeController
,但这会引发 500 错误。这是有道理的。 spring 5 和 6 之间肯定发生了一些变化。我们确实看到 HandlerMethod 中有一个名为 validateArguments
的布尔值,但我们无法在测试类中将其设置为 true。
下面是测试类和相关依赖版本
package com.mock.controller;
import static org.junit.Assert.assertEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import org.apache.commons.lang.StringEscapeUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
public class ExceptionHandlerTest {
@Mock private MockService service;
private ObjectMapper mapper;
private MockMvc mvc;
private MockController controller;
private ExceptionHandlerExceptionResolver createExceptionResolver(boolean is500Disabled) {
ExceptionHandlerExceptionResolver exceptionResolver = new ExceptionHandlerExceptionResolver() {
@Override
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
Method method = new ExceptionHandlerMethodResolver(CustomGlobalExceptionHandler.class).resolveMethod(exception);
CustomGlobalExceptionHandler handler = new CustomGlobalExceptionHandler();
handler.setDisable500(is500Disabled);
return new ServletInvocableHandlerMethod(handler, method);
}
};
exceptionResolver.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
exceptionResolver.afterPropertiesSet();
return exceptionResolver;
}
@Before
public void setup() {
MockitoAnnotations.openMocks(this);
controller = new MockController(service);
mvc = MockMvcBuilders.standaloneSetup(controller)
.setHandlerExceptionResolvers(createExceptionResolver(false))
.build();
mapper = new ObjectMapper();
}
@Test
public void thatMethodArgumentNotValidExceptionHappens() throws Exception {
Request req = new Request(null, "brand", "model", "color", "qty");
MvcResult result = mvc.perform(post("/api/some/post").contentType(MediaType.APPLICATION_JSON_VALUE).content(mapper.writeValueAsString(req)))
.andExpect(status().isBadRequest())
.andReturn();
assertEquals(HttpStatus.BAD_REQUEST.value(), result.getResponse().getStatus());
JSONObject obj = new JSONObject(UnescapeResponseJSON(result));
assertEquals("Error Processing Input", obj.get("detail"));
assertEquals("id cannot be empty or blank", ((JSONArray)obj.get("errors")).get(0));
assertEquals("Error Processing Input "+((JSONArray)obj.get("errors")).get(0), obj.get("message"));
}
private String UnescapeResponseJSON(MvcResult result) throws UnsupportedEncodingException {
String unescapedJSON = StringEscapeUtils.unescapeJava(result.getResponse().getContentAsString());
return unescapedJSON.replaceAll("^\"|\"$", "");
}
@RestController
public class MockController {
private MockService service;
public MockController(MockService service) {this.service = service;}
@PostMapping(value = "/api/some/post", consumes=MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Object> post(@Valid @RequestBody Request req, BindingResult bindingResult) {
return new ResponseEntity<>(service.get(10), HttpStatus.OK);
}
}
public class MockService {
public Object get(int i) {
return new Object();
}
}
public class Request {
@NotNull(message = "id cannot be empty or blank")
private String id;
@NotNull(message = "brand cannot be empty or blank")
private String brand;
@NotNull(message = "model cannot be empty or blank")
private String model;
@NotNull(message = "color cannot be empty or blank")
private String color;
@NotNull(message = "qty cannot be empty or blank")
private String qty;
public Request() {}
public Request(String id, String brand, String model, String color, String qty) {
this.id = id;
this.brand = brand;
this.model = model;
this.color = color;
this.qty = qty;
}
//omitted getters and setters
}
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.1.0-M2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.1.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.1.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.1.5</version>
</dependency>
我在执行 Spring 5 -> 6 升级时遇到了同样的问题。
修复方法是使用
LocalValidatorFactoryBean
设置验证器,并确保表达式工厂以及 hibernate 本身具有相关的运行时依赖项。
MockMvcBuilders.standaloneSetup(new MyController())
.setValidator(new LocalValidatorFactoryBean())
.build()
testRuntimeOnly("org.glassfish:jakarta.el:4.0.2")