在上一篇学习笔记:How to mock Resthighlevelclient? 我提到了PowerMockito
是Unit Test
中应对final
和static
的利器,那么这里就简单记录一下自己的实战。
零。准备工作
首先是引入依赖包,当前最新的是版本是2.0.2
org.powermock
powermock-module-junit4
2.0.2
test
org.powermock
powermock-api-mockito2
2.0.2
test
其次是阅读文档:
一个是javadoc
上的powermock-api-mockito2/2.0.2/index.html,还有就是Github
上的https://github.com/powermock/powermock/wiki/Mockito
前者有点纯接口文档的意思,后者会带有一些解释和示例,而且后者的副标题是Using PowerMock with Mockito
,所以后者可能会更容易看懂,如果有使用Mockito
的经验是最佳的。
壹。有点不同
使用PowerMockito
对于Mockito
来说,是有些不同的,简单归纳一下就是:
1)要在你写的UT Class
前先加上@RunWith(PowerMockRunner.class)
,再加上@PrepareForTest
2)如果你想mock
的对象涉及final
或static
,要它所用到的class
添加在@PrepareForTest
中
用代码来展示的话就是下面这样
package com.a.b.c.d.api;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
...
import org.elasticsearch.client.RestHighLevelClient;
import static org.powermock.api.mockito.PowerMockito.*;
@RunWith(PowerMockRunner.class)
@PrepareForTest({RestClient.class, RestClientBuilder.class, SearchSourceBuilder.class,
SearchRequest.class, RestHighLevelClient.class, Aggregation.class, Aggregations.class, Terms.class, AggregationBuilders.class,...})
@PowerMockIgnore({"org.apache.logging.log4j.*", "javax.management.*"})
public class ABCDHandlerTest {
}
需要注意的是有多个class
需要@PrepareForTest
,要在()
中再加{}
包起来,如果只有一个的话可以直接写在()
中。而@PowerMockIgnore
则可以把你不想测的给忽略掉。
贰。常规操作
<1> PowerMockito.mock
Creates a mock object that supports mocking of final and native methods.
这个方法是多态的,我这里只摘选了最简单的那个的解释。
这个很好懂,可以实现对final
对象的mock
操作,同Mockito.mock
的用法是一样的.
以心心念念的ElasticSearch.RestHighLevelClient
为例:
private RestHighLevelClient restHighLevelClient = mock(RestHighLevelClient.class);
这个就等同于完成了我们fuction
代码里的声明。
<2> PowerMockito.mockstatic
Enable static mocking for all methods of a class.
这个也是多态的,更多解释请查阅powermock-api-mockito2/2.0.2/index.html。
尽管它也是声明class
的操作,但更多的是当我们需要mock
这个class
中的某个static
方法才会用到。
代码可以提前声明,也可以连起来写,更有利于阅读。以ElasticSearch.RestClient
为例,这是mock RestHighLevelClient
的其中一步:
mockStatic(RestClient.class);
when(RestClient.builder(httpHost)).thenReturn(restClientBuilder);
<3> PowerMockito.whenNew
Allows specifying expectations on new invocations.
代码中通过New
操作实例化对象,当我们需要mock
之的时候,对应的操作就是PowerMockito.whenNew
,它还可以实现无参数withNoArguments
、带参数withArguments
(一个以及多个):
whenNew(RestHighLevelClient.class).withArguments(restClientBuilder).thenReturn(restHighLevelClient);
whenNew(HttpHost.class).withArguments(host, port, "http").thenReturn(httpHost);
whenNew(SearchSourceBuilder.class).withNoArguments().thenReturn(searchSourceBuilder);
至于更多的其他常规武器,就在文档里找找吧。
叁。趟过的小坑
<1> Partial Mocking
部分模拟,我不知道这么直译是否合适。
当时确实是碰到了一个难点,两位同事Steven
跟JingYan
帮着调了一下午试过各种方法都没弄好。
试到最后,感觉问题就是一个对象明明已经被mock
了,但却不是完全mock
的状态,debug
的时候它的hashcode
为0
,当调用它的一个方法时就会报出"令人着迷"的NullPointerException
。
同事说是因为它内部有个什么写保护,我不太明白。
然后当天晚上我就无奈的刷着上面两篇文档,当读到下面这一段时,脑袋里犹如一道光芒照下,于是就解决了问题。
需要mock
实际代码是这一句,大致功能是从ElasticSearch
的查询结果searchResponse
中获取一个聚合,再从中获取某个单项结果
Terms terms = searchResponse.getAggregations().get(String strA);
而其中get()
的具体实现为
package org.elasticsearch.search.aggregations;
...
public class Aggregations implements Iterable, ToXContentFragment {
...
/**
* Returns the aggregation that is associated with the specified name.
*/
@SuppressWarnings("unchecked")
public final A get(String name) {
return (A) asMap().get(name);
}
...
}
看着平平无奇,也不知道为啥就无法完全mock
,由于过去了将近三四周,其中的各种曲折,我也记不得细节了,直接贴解决方案吧:
List aggregationList = new ArrayList<>();
aggregationList.add(aggregation);
Aggregations aggregations = new Aggregations(aggregationList);
Aggregations aggregationsSpy = spy(aggregations);
when(searchResponse.getAggregations()).thenReturn(aggregationsSpy);
doReturn(terms).when(aggregationsSpy).get(anyString());
我个人的理解就是最后的aggregationsSpy
是一个半真半假的对象,如果有哪位对此有深入的理解,请留言。
<2> 同一个class的不同实例,只能mock一次
直接上代码吧
MatchQueryBuilder primaryIdMatchQuery = QueryBuilders.matchQuery(request.getIdFieldName(), request.getPrimaryId());
MatchQueryBuilder secondaryIdMatchQuery = request.ids.length > 2
? QueryBuilders.matchQuery(request.getIdFieldName(), request.getSecondaryId())
: null;
如上,primaryIdMatchQuery
和secondaryIdMatchQuery
都是MatchQueryBuilder
的实例对象,如果在UT
中为他们分别mock
一次
# Wrong Solution
private MatchQueryBuilder matchQueryBuilder1 = mock(MatchQueryBuilder.class);
private MatchQueryBuilder matchQueryBuilder2 = mock(MatchQueryBuilder.class);
when(QueryBuilders.matchQuery(request.getIdFieldName(), request.getPrimaryId())).thenReturn(matchQueryBuilder1);
if (request.ids.length > 2) {
when(QueryBuilders.matchQuery(request.getIdFieldName(), request.getSecondaryId())).thenReturn(matchQueryBuilder2);
}
就会出现multi-threaded tests
问题:
正确的处理应该是只
mock
一次,然后返回时将二者一视同仁:
# Correct Solution
private MatchQueryBuilder matchQueryBuilder = mock(MatchQueryBuilder.class);
when(QueryBuilders.matchQuery(request.getIdFieldName(), request.getPrimaryId())).thenReturn(matchQueryBuilder);
if (request.ids.length > 2) {
when(QueryBuilders.matchQuery(request.getIdFieldName(), request.getSecondaryId())).thenReturn(matchQueryBuilder);
}
<3> 链式代码需要一步一步分别mock才能正常工作
这个应该好理解,就拿上面的searchResponse.getAggregations().get(String strA);
来说,就需要分两步来mock
,至于更多我也写过,就是拼接Query
的代码,那写的叫一个痛苦。
<4> 有些参数无法any
大家知道,在mock
操作的时候,大多是时候并不需要给出具体的参数,比如" the string"
,一般给个" "
或者anyString()
就能过。
但是有些方法就是必须给出代码里指定的" the string"
才能过,这里就不示例了,应该能碰到,特别是在mock Query.withColumn()
的时候,具体为啥我也不明白。
肆。小结一下
这篇博客写下来,自己都觉得很是潦草,因为部分想写的东西都忘了。
之前为了完成工作任务,感觉自己花了不到两周时间就从UT 小白
成长为UT 新贵
,此间还撸代码到凌晨那个点,然后就想着一定要写个日记记下来。但是仅仅去过去了不到一个月,由于懒惰,再加上工作内容又切换到其他方面了,忘了许多,为了避免进一步的忘却,只能勉强凭着些许的记忆简单记录一下。
所以写东西,还是要趁着热乎。