使用PowerMockito做测试

2018-12-12  本文已影响59人  阿呆少爷

典型的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.603Z2018-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

参考文章

  1. Mockito与PowerMock的使用基础教程
上一篇下一篇

猜你喜欢

热点阅读