(原创)PowerMockito实战及心得

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

零。准备工作

首先是引入依赖包,当前最新的是版本是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的对象涉及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, 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; 

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

你可能感兴趣的:((原创)PowerMockito实战及心得)