(原创)PowerMockito实战及心得

在上一篇学习笔记:How to mock Resthighlevelclient? 我提到了PowerMockitoUnit Test中应对finalstatic的利器,那么这里就简单记录一下自己的实战。

零。准备工作

首先是引入依赖包,当前最新的是版本是2.0.2

<!--https://mvnrepository.com/artifact/org.powermock/powermock-module-junit4-->
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>2.0.2</version>
    <scope>test</scope>
</dependency>
<!--https://mvnrepository.com/artifact/org.powermock/powermock-api-mockito2-->
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito2</artifactId>
    <version>2.0.2</version>
    <scope>test</scope>
</dependency>

其次是阅读文档:
一个是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的对象涉及finalstatic,要它所用到的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

部分模拟,我不知道这么直译是否合适。
当时确实是碰到了一个难点,两位同事StevenJingYan帮着调了一下午试过各种方法都没弄好。
试到最后,感觉问题就是一个对象明明已经被mock了,但却不是完全mock的状态,debug的时候它的hashcode0,当调用它的一个方法时就会报出"令人着迷"的NullPointerException。
同事说是因为它内部有个什么写保护,我不太明白。
然后当天晚上我就无奈的刷着上面两篇文档,当读到下面这一段时,脑袋里犹如一道光芒照下,于是就解决了问题。

Partial Mocking

需要mock实际代码是这一句,大致功能是从ElasticSearch的查询结果searchResponse中获取一个聚合,再从中获取某个单项结果

Terms terms = searchResponse.getAggregations().get(String strA);

而其中get()的具体实现为

package org.elasticsearch.search.aggregations;
...

public class Aggregations implements Iterable<Aggregation>, ToXContentFragment {
    ...
    /**
     * Returns the aggregation that is associated with the specified name.
     */
    @SuppressWarnings("unchecked")
    public final <A extends Aggregation> A get(String name) {
        return (A) asMap().get(name);
    }
   ...
}

看着平平无奇,也不知道为啥就无法完全mock,由于过去了将近三四周,其中的各种曲折,我也记不得细节了,直接贴解决方案吧:

        List<Aggregation> 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; 

如上,primaryIdMatchQuerysecondaryIdMatchQuery都是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问题:

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 新贵,此间还撸代码到凌晨那个点,然后就想着一定要写个日记记下来。但是仅仅去过去了不到一个月,由于懒惰,再加上工作内容又切换到其他方面了,忘了许多,为了避免进一步的忘却,只能勉强凭着些许的记忆简单记录一下。
所以写东西,还是要趁着热乎。

?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,100评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,308评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,718评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,275评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,376评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,454评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,464评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,248评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,686评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,974评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,150评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,817评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,484评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,140评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,374评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,012评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,041评论 2 351

推荐阅读更多精彩内容