美文网首页
(原创)PowerMockito实战及心得

(原创)PowerMockito实战及心得

作者: mona_alwyn | 来源:发表于2019-11-06 23:19 被阅读0次

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

    相关文章

      网友评论

          本文标题:(原创)PowerMockito实战及心得

          本文链接:https://www.haomeiwen.com/subject/mihjbctx.html