典型的Spring应用会分为三层,分别是DAO、Service、Controller三层。Controller主要负责请求接入,具体的逻辑交给Service完成。要测试Controller,往往需要Mock Service,通过MockMVC+PowerMockito这个组合很适合做这个测试。
引入依赖
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.0-RC.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.0-RC.3</version>
<scope>test</scope>
</dependency>
测试准备
使用@InjectMocks
注入要测试的Controller。如果Controller依赖Service,那么通过@Mock
注入这些Service。
@InjectMocks
private TaskController taskController;
@Mock
private TaskService taskService;
Controller往往会注入全局的异常处理。在测试环境下,需要手动设置这个异常处理类。
final StaticApplicationContext applicationContext = new StaticApplicationContext();
applicationContext.registerSingleton("exceptionHandler", GlobalControllerExceptionHandler.class);
final WebMvcConfigurationSupport webMvcConfigurationSupport = new WebMvcConfigurationSupport();
webMvcConfigurationSupport.setApplicationContext(applicationContext);
基本使用
首先设置好mock方法的返回结果。
PowerMockito.when(taskService.createTask(task)).thenReturn(task);
接着调用这个mock方法。
RequestBuilder request = post("/task")
.contentType(APPLICATION_JSON_UTF8)
.content(requestJson);
MvcResult result = mvc.perform(request).andReturn();
一些问题
使用Java8时间
如果对象使用Java 8的时间类型,测试过程会遇到很多问题。比如Task对象有两个属性是OffsetDateTime类型。
public class Task {
OffsetDateTime datetimeStartTask;
OffsetDateTime datetimeEndTask;
}
目前我的解决办法是,序列化使用Gson,对Java8的时间类型支持很好。这样生成的requestJson可以被反序列化转换成对象。
String requestJson = gson.toJson(task, Task.class);
RequestBuilder request = post("/task")
.contentType(APPLICATION_JSON_UTF8)
.content(requestJson);
MvcResult result = mvc.perform(request).andReturn();
assert (result.getResponse().getStatus() == HttpStatus.CREATED.value());
反序列的时候,要提前设置WRITE_DATES_AS_TIMESTAMPS=false
,要不然SpringMVC使用Jackson将OffsetDateTime序列化成Timestamp,反序列的时候会报错。设置的正确方法如下所示。MockMvcBuilders
要同时设置全局异常和消息转换类。
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new
MappingJackson2HttpMessageConverter();
mappingJackson2HttpMessageConverter.setObjectMapper(builder.build());
mvc = MockMvcBuilders.standaloneSetup(taskController)
.setHandlerExceptionResolvers(webMvcConfigurationSupport.handlerExceptionResolver())
.setMessageConverters(mappingJackson2HttpMessageConverter)
.build();
Objects.equals居然会认为2018-12-12T12:19:13.603Z
和2018-12-12T20:19:13.603+0800
这两个时间不相等,使用toEpochSecond方法转成Timestamp比较就好了。
jshell> OffsetDateTime t1 = OffsetDateTime.parse("2018-12-12T12:19:13.603Z")
t1 ==> 2018-12-12T12:19:13.603Z
jshell> OffsetDateTime t2= OffsetDateTime.parse("2018-12-12T20:19:13.603+08:00")
t2 ==> 2018-12-12T20:19:13.603+08:00
jshell> t1.toEpochSecond()
$18 ==> 1544617153
jshell> t2.toEpochSecond()
$19 ==> 1544617153
jshell> Objects.equals(t1, t2)
$17 ==> false
jshell> ZonedDateTime t1 = ZonedDateTime.parse("2018-12-12T12:19:13.603Z")
t1 ==> 2018-12-12T12:19:13.603Z
jshell> ZonedDateTime t2 = ZonedDateTime.parse("2018-12-12T20:19:13.603+08:00")
t2 ==> 2018-12-12T20:19:13.603+08:00
jshell> Objects.equals(t1, t2)
$22 ==> false
使用对象参数
当when里面的方法使用对象作为参数时,传入的对象与反序列化得到的对象并不相等,这会导致无法触发when的条件,所以需要实现对象的hashcode和equalTo方法。
PowerMockito.when(taskService.createTask(task)).thenReturn(task);
objectMapper.configure(SerializationFeature.WRAP_ROOT_VALUE, false);
ObjectWriter ow = objectMapper.writer().withDefaultPrettyPrinter();
String requestJson = ow.writeValueAsString(task);
RequestBuilder request = post("/task")
.contentType(APPLICATION_JSON_UTF8)
.content(requestJson);
MvcResult result = mvc.perform(request).andReturn();
Path参数校验
模型的校验可以测试到。比如传入空的name,会触发MethodArgumentNotValidException
异常。
public class Task {
@Size(min = 1, max = 128)
String name;
}
PathVariable和RequestParam的校验测试不到,很奇怪。
//id传入-2也行
@GetMapping(value="/task/{id}")
public ResponseEntity<Task> getTask(@PathVariable @Range(min=1) Long id) throws ApiException {
Task task = taskService.getTaskById(id);
return new ResponseEntity<>(task, HttpStatus.OK);
}
//pageNum传入1000也行
@GetMapping("/tasks")
public ResponseEntity<List<Task>> listTasks(@RequestParam(defaultValue = "1") @Range(min=1, max=100) Integer pageNum,
@RequestParam(defaultValue = "20") Integer pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<Task> taskList = taskService.listTasks(pageNum, pageSize);
return new ResponseEntity<>(taskList, HttpStatus.OK);
}
image.png
网友评论