Mockito无法mock/spy情况下的解决方法

Mockito不支持mock/spy如下的类型:

  • final classes
  • anonymous classes
  • primitive types

如果你mock这些类型,会抛出如下错误信息:
org.mockito.exceptions.base.MockitoException: Cannot mock/spy class java.lang.String Mockito cannot mock/spy following: - final classes - anonymous classes - primitive types

而如果你有确实有此需求,可以通过如下方法解决。先定义一个工具类,里面包含如下方法:

public static void injectField(final Object injectable, final String fieldname, final Object value) {
  try {
    final java.lang.reflect.Field field = injectable.getClass().getDeclaredField(fieldname);
    final boolean origionalValue = field.isAccessible();
    field.setAccessible(true);
    field.set(injectable, value);
    field.setAccessible(origionalValue);
  } catch (final NoSuchFieldException | IllegalAccessException e) {
    throw new RuntimeException(e.getMessage(), e);
  }
}

比如你有如下类需要测试:

public class Greeting {
  @Inject
  private String greeting;

  @Override
  public String toString() {
    return this.greeting;
  }
}

那么,你的测试类大概如下:

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;

public class GreetingTest {
  @Test
  public void testToString() throws Exception {
    Greeting greeting = new Greeting();
    injectField(greeting, "greeting", "Hello, world");
    assertThat(greeting.toString(), is("Hello, world"));
  }
}

以上参考:https://dzone.com/articles/field-injection-when-mocking-frameworks-fail

当然,如果你使用Spring Test,你可以使用org.springframework.test.util.ReflectionTestUtils。 类似ReflectionTestUtils.setField(testObject, "person", mockedPerson);

或者,如果你使用Mockito 1.*,可以使用
import org.mockito.internal.util.reflection.FieldSetter;
new FieldSetter(obj, obj.getClass().getDeclaredField("p")).set(new P());

//or
org.mockito.internal.util.reflection.Whitebox#setInternalState(Object target, String field, Object value)

针对Mockito 2.*,可以使用:
org.mockito.internal.util.reflection.FieldSetter.setField(Object target, Field field, Object value)

使用Mockito来mock autowired字段

依赖注入(DI)是像Spring,EJB这样的控制反转(IOC)类容器的一个很强大的特性。把注入的值封装成类的私有字段是一个很好的主意,但是像这样把autowired的字段封装起来也降低了可测试性。本文将介绍针对这种情况,Mockito是如何来mock私有的autowired字段的。

首先被测试的类依赖的第一个类是这样子的,它是一个Spring单例bean,这个类将会在测试中被mock掉。


@Repository
public class OrderDao {
  public Order getOrder(int irderId){
    throw new UnsupportedOperationException("Fail is not mocked!");
  }
}

接下来是被测试的类依赖的第二个类,它也是Spring的一个Component。在测试中,这个类将会被spied(partially mocked)。它的方法calculatePriceForOrder将会原样被调用(不被stub),另外一个方法将会被stubbed。

@Service
public class PriceService {

  public int getActualPrice(Item item){
    throw new UnsupportedOperationException("Fail is not mocked!");
  }

  public int calculatePriceForOrder(Order order){
    int orderPrice = 0;
    for (Item item : order.getItems()){
      orderPrice += getActualPrice(item);
    }
    return orderPrice;
  }

}

然后是被测试的类,它使用autowire上面的两个依赖的类。

@Service
public class OrderService {

  @Autowired
  private PriceService priceService;

  @Autowired
  private OrderDao orderDao;

  public int getOrderPrice(int orderId){
    Order order = orderDao.getOrder(orderId);
    return priceService.calculatePriceForOrder(order);
  }

}

最后是测试类,它使用字段级别的注解:

  • @InjectMocks – 含有此注解的字段,Mockito将会实例化被测试的类,并且尝试把用@Mock或者@Spy注解的字段注入到被测试对象的私有字段中。
  • @Mock – 含有此注解的字段,Mockito会创建对应类的mock实例。
  • @Spy – 含有此注解的字段,Mockito会创建对应类的spy实例。
  • public class OrderServiceTest {
    
      private static final int TEST_ORDER_ID = 15;
      private static final int TEST_SHOES_PRICE = 2;
      private static final int TEST_SHIRT_PRICE = 1;
    
      @InjectMocks
      private OrderService testingObject;
      @Spy
      private PriceService priceService;
      @Mock
      private OrderDao orderDao;
    
      @Before
      public void initMocks(){
        MockitoAnnotations.initMocks(this);
      }
    
      @Test
      public void testGetOrderService(){
        Order order = new Order(Arrays.asList(Item.SHOES, Item.SHIRT));
        Mockito.when(orderDao.getOrder(TEST_ORDER_ID)).thenReturn(order);
        //notice different Mockito syntax for spy
        Mockito.doReturn(TEST_SHIRT_PRICE).when(priceService).getActualPrice(Item.SHIRT);
        Mockito.doReturn(TEST_SHOES_PRICE).when(priceService).getActualPrice(Item.SHOES);
        //call testing method
        int actualOrderPrice = testingObject.getOrderPrice(TEST_ORDER_ID);
        Assert.assertEquals(TEST_SHIRT_PRICE + TEST_SHOES_PRICE, actualOrderPrice);
      }
    
    }
    

    让我们看下在执行这个测试类的时候会发生什么:
    1)首先JUnit框架会在执行每个测试方法之前执行@Before注解的方法initMocks。
    2)initMocks这个方法会调用Mockito的方法(MockitoAnnotations.initMocks(this))初始化被被注解的字段。如果没有这个调用,那么被注解的字段将会是null。也可以使用@RunWith(MockitoJUnitRunner.class)来注解被测试的类来达成相同的效果。
    3)最后当所有的字段被初始化完成后,开始执行测试方法(被@Test注解的方法)。

    这个例子中不包含Spring context的创建,这里的Spring注解(比如@Service,@Repository)只是为了说明实际生产中代码的例子。测试本身不包含对Spring的任何依赖并且或忽略Spring的任何注解。事实上这里可以换成EJB的注解,或者甚至换成普通的私有字段(不被任何IoC容器管理),测试依然可以执行。

    开发者可能认为每次测试之前的 MockitoAnnotations.initMocks(this)调用是不必要的开销。但是其实这样很方便,因为它重置了被测试的对象和重新初始化了mock对象。当你的测试类里有多个测试方法(@Test注解的方法)时,可以避免互相干扰。

    @Spy注解的对象可以通过两种方法来初始化:
    1)如果有默认的构造器,则可以由Mockito框架来自动创建;
    2)如果仅有非默认构造器,可以显示实例化它。比如
    @Spy
    private PriceService priceService = new PriceService();

    类似的,被@InjectMocks注解的被测试对象也可以显式实例化。

    参考:https://lkrnac.net/blog/2014/01/mock-autowired-fields/

    2018年度读书报告

    今天是2019年1月16日,18年已经过去半月有余。一直在犹豫要不要写年终总结,毕竟已经连续三年没写年终总结了。

    可能和往年一样,拖延一段时间,这件事情就过去了。不过最终还是决定要写一下。要知道,一旦坐下来开始写,这件事基本也就成了。

    回想上次写年终总结还是2015年,一眨眼几年就过去了。回忆过去的这几年貌似一直很忙,被各种事情推着走,却很少静下心来做做总结,想想事情。

    年终总结其实是很好的一个机会,回顾过去的一年做的事情,想想明年要做些什么。

    可能会写几篇,本篇为2018年度读书报告。

    去年看过的书

    平时看过的书,一般会在豆瓣上标记了。整理了下,去年完整看完的书大约有十几本,另外还有一些专业的参考书和没看完的就不在这里列出了。标题后面的星星为个人评分。

    • 月亮与六便士 (威廉·萨默塞特·毛姆 )☆☆☆☆☆
    • 人类简史 (尤瓦尔·赫拉利)☆☆☆☆☆
    • 岛上书店   (加布瑞埃拉·泽文)☆☆☆☆
    • 浮生六记(沈复 )☆☆☆☆
    • 刻意练习 (安德斯·艾利克森)☆☆☆
    • 冥想 (斯瓦米·拉玛)☆☆☆
    • 深度思考 (莫琳·希凯)☆☆☆
    • 易经杂说(南怀瑾)☆☆☆
    • 金刚经说什么 (南怀瑾) ☆☆☆
    • 禅与摩托车维修艺术 (罗伯特·M.波西格)☆☆☆
    • 腾讯传 (吴晓波)☆☆
    • 小王子 (圣埃克苏佩里)

    最喜欢的一本是《月亮与六便士》,“当人们都在看着地上的六便士时,他却抬头看见了月亮”。正如高晓松所说的,生活不只是眼前的苟且,还有诗和远方。不过终归,大多数的我们还是俗人。作为俗人,我给这本书五星。

    读起来最气势磅礴的是《人类简史》,作者博学多识,从十万年前有生命迹象开始写到21世纪,在历史学之外,人类学、生态学、基因学等领域的知识信手拈来,从宏观角度切入的研究往往得出颇具新意而又耐人寻味的观点,读起来非常有趣。也给五星。

    最温馨的是《岛上书店》,在各个网站排行榜上都看到在推荐这本书,很温情的故事,叙事方式也很特别。书中关于书店的出路与电子书的关系的讨论,值得思考。推荐。去年看的类似的书籍是《解忧杂货铺》,也是值得一看的。

    传记类的看了《腾讯传》和香奈儿前CEO莫琳·希凯的《深度思考》,感觉都很一般,尤其是的《腾讯传》,公关味道很重,不是很喜欢。本来对传记类的书就不感冒,后续此类不再关注。

    关于个人修养类的书看的比较多,占了1/3,涉及冥想,佛与禅,易经等,算是了解个大概,还没找到内心的认同感。不过读多了发现各种哲学宗教等多少会有一些相似之处。立竿见影的效果看不出来,潜移默化总会有一些吧。

    2017年我的年度书籍是叔本华的《人生的智慧》,那么2018年我把此荣誉留给毛姆的《月亮与六便士》。

    读书心得

    庄子说,吾生也有涯,而知也无涯;以有涯随无涯,殆已!意思是说人生有限,知识是无限的,用有限的人生去追求无限的知识,就会搞得精疲力尽。

    读书也是如此,所以,看书要有选择的读,不是读的越多越好。佛经里说,莫贪。其实,贪恋读书,也是贪。

    关于选书,一般我会看豆瓣的评价。不过,评分高的,有时候也未必适合。有两种情况要特别小心:

    • 评分的人很少,虽然评分高,但是只有少数人评分,这种情况,未必是好书。
    • 高分的书,其实未必适合你。尤其是某些专业性的书,可能具备一定门槛,如果基础不好,可能看不懂。

    新年读书规划

    18年实体书和电子书一共买了有几十本,只读了10几本书。真可谓买书如山倒,读书如抽丝。新的一年,计划把存量的书看完。

    统计了下近几年的读书记录,发现每年的年初和年末会读书比较多,剩下的时间读的很少。这个一方面和工作的忙闲有关,另外也是个人缺乏持续性。所以,今年要坚持做到,慢慢读,周周读(就不说日日读了)。

    另外,在读书方法上,今年要更加专注,按照主题读书,这样才能把同一主题的相关内容关联起来,效果会更好。

    最后,专业书今年会多看一点,保持专业竞争力。毕竟,winter is coming!

    新年寄语

    有句话说的好,道理懂那么多,却依然过不好自己的一生。读书也是一样,读万卷书,行万里路,知行合一。

    公众号阅读:https://mp.weixin.qq.com/s/-8fbNA-4xUrXGCBHJ9c3Ug

    上牌记-苏A转浙A

    风二摇到了杭州车牌,今天要去上牌。

    14年5月,杭州开始车牌摇号和竞价的时候,那时候风二还没有车,连驾照都没有,不过他还是报名了摇号。宣布限号的当天晚上,好多人都去4S店订了车,可风二想了想,连驾照都没有,买了也白搭。

    14年10月,风二终于考完了驾照,拿到驾照的路上,直接去了4S店。没有本地牌照,就上了苏A的牌。当时选择南京,是因为他在那里上过学。不过没曾想到,多年后的今天,从南京转到杭州,竟然省了不少事情。

    这个月26号,是每月例行摇号的日子。早上,风二发了条微博,祝自己好运。白天一直在忙工作,快下班忙完事情,看了眼手机,发现竟然收到了中签的短信。风二感叹,早上发的微博,有魔性。

    中签,摇了55次。

    今天要去上牌。

    说起今天上牌,幸亏之前多在网上查了一下。知道中签的第二天,风二就决定要速度把牌给上了,虽然指标的有效期是一年。但是,风二就是如此雷厉风行的人,确定要办的事情,立马搞定。

    从网上查了许多攻略,正规流程是先去原车牌所在地南京提档,然后回杭州上牌。不过突然发现有人在网上说可以不用去南京提档了,直接去杭州办理电子转档上牌,看了下时间是最近发布的消息,所以时效性应该比较好。继续查了下网上的新闻,从今年9月份开始的新的政策。

    【在15个试点城市之间(南京、苏州、泰州、杭州、宁波、嘉兴、南昌、上饶、广州、深圳、东莞、中山、南宁、柳州、重庆),推行非营运小微型载客汽车档案电子化网上转递,对机动车所有人住所在试点城市间迁入、迁出情形(为变更登记,不涉及车辆过户)的,申请人不再需要提取纸质档案;对办理车辆转籍的,申请人可以直接到车辆迁入地车辆管理所申请,无须再回迁出地验车,减少群众两地间往返。】

    关键词: 仅转籍,不涉及过户。也就是说,对于车主不变的情况可以电子转档。

    于是,今天,风二去上牌了。不用去南京,直接去杭州石祥路589号杭州汽车城。

    带材料:身份证,行驶证,保险单,车辆登记证,打印的中签指标。

    风二10点多到了汽车城北门,时间稍微晚了点。那边现在是工地,有个入口不小心进去了,结果被拦住了,发现是工地,不是北门。继续往前走一小段就到了北门,门外好多黄牛,代理办理这些手续的。风二是有备而来,之前查了攻略,所以不理他们。但是有一个人一直跟着,风二很不耐烦,就打开车窗。对方问办什么手续,答外地车牌迁入,电子转档,对方问了句需要帮忙吗,回答不需要。对方也就没继续跟了,估计他也知道电子转档很简单,不需要代办。顺便还给风二指了下车道。

    进去后,里面有许多车道,有转移登记的,有年检验车的,要注意选择车道。风二开进去之后发现有一个车道竟然没车,愣头愣脑的把车开到了验车的口子。预登记后,然后停车,开引擎盖,放上三脚架,警察叔叔拍照合影。然后就完了。警察叔叔让把车开到停车库,顺便给个电话号码,找这个人免费拓印。

    按照电话号码打过去,对方问有没有拆车牌,没有的话找个人给一起拆掉,顺便就拓印了。当然,要收费的,30搞定。拆下车牌,拿着拓印,把拓印和材料交到刚才验车的窗口。风二后来想了下,其实拆车牌和拓印这一步可以提前做的,在验车之前排队的时候就找人拆好拓好。但是由于来的时候没有人,不用排队,所以就一直开到验车点了。风二想,运气真不错。

    然后就去办事大厅等了,叫号的地方会叫名字,然后交到名字就过去拿号。拿到号之后就等叫号办理。几分钟就办好了,120块钱缴费,然后去机器上选车牌号码,之后下个邮政快递单(寄行驶证和车辆登记证),车牌会后面单独寄到家。最后上2楼拿临牌回家。最后一步拿临牌的时候正好赶上12点下班了,下午一点上班不过会提前上班,所以,去万达广场吃了个午餐,回来后直接去拿临牌就回家了。

    10点多到的,基本上整个流程2个小时之内就搞定了,要是早点去的话,估计中午还可以回家吃午饭。

    clone github repo提示无权限

    今天要从GitHub clone一个代码库:

    git clone git@github.com:gshine/exampleofjsonrpc4j.git

    提示报错:

    git@github.com: Permission denied (publickey).
    fatal: Could not read from remote repository.
    
    Please make sure you have the correct access rights
    and the repository exists.
    
    

    查看了下已经把公钥上传到了github(如果你之前没配置过,可以参考这里生成并配置密钥。)

    后来从网上查了下,执行了下面这个命令就好了。

    ssh-add ~/.ssh/***_rsa
    

    其中”***_rsa”换成你自己的密钥文件。配置完后可以通过

    ssh -T git@github.com

    来测试下联通性。

    很奇怪,之前已经配置好的竟然突然出现这个问题,怀疑可能是ssh版本升级导致的。

    总结下出现此问题,可以从以下几个方面来排查:

    • SSH key是否配置正确,参考此链接
    • 密钥是否加到key-chain里,此问题可以通过ssh-add来解决。
    • 本地是否有多个ssh密钥对,如果是的话,需要在~/.ssh/config里面针对不同的host配置不同的私钥。参考此链接
    • 可能代码库不存在

    在macOS High Sierra中配置Apache/PHP/Mysql

    macOS内置了Apache http server,但是每个版本的配置方式都不太一样。导致之前配好的升级系统后就失效了。在升级到macOS High Sierra之后,之前配置本地安装的wordpress又不能用了,按照下面这篇文章配置后就可以了。

    在macOS High Sierra版本中可以按照这篇文章配置:

    https://coolestguidesontheplanet.com/install-apache-mysql-php-and-phpmyadmin-on-macos-high-sierra-10-13/

    macOS High Sierra之前的版本可以参考这个链接:

    https://discussions.apple.com/docs/DOC-3083

     

    CDP与DMP

    1 概念

    CDP:Customer Data Platforms,客户数据平台
    DMP: Data Management Platform,数据管理平台,一般特指针对数字营销(Digital Marketing)。

    2 差别

    两者共同的数据来源:一方数据,二方数据,三方数据。CDP和DMP可以互为补充,帮助彼此达到更好的效果,但是两者还是有所区别。

    CDP 包含一方数据在内的所有数据,以一方数据为主。
    DMP 主要是第三方数据,可能有少量一方数据。

    CDP 可以用于营销的各个方面
    DMP 专门用于广告商和代理商改善广告投放效果

    CDP 包含个人身份信息,也包含匿名的信息。
    DMP 通常是匿名的(cookies, devices, and IP addresses),不包含个人身份信息(PII),匿名是DMP作为在不侵犯用户隐私的前提下实现受众信息交换的基础。

    CDP 数据保留时间长,以便于客户生命周期分析。
    DMP 数据保留时间短,因为仅仅用于广告投放。

    3 CDP的优势
    • Storing 1st Party Data, Including PII (Personally Identifiable Information)–客户身份匹配
    • Single, Unified Data Storage –方便快速的查询
    • Raw, Detailed Data with Unlimited Storage Capacity –深度,充分的分析
    • Applications in the Entire Martech Stack –不仅仅是广告,可以在整个营销域应用
    • Complete Customer Profile –个性化的客户体验
    参考资料

    https://www.treasuredata.com/learn/cdp-vs-dmp/

    Linux shell终端常用任务管理命令

    1 终端常用命令
    &:在要执行的命令后面加&,表示要在后台运行此命令
    jobs:列出所有的job, 结果中会有jobId,可以用在bg,fg中。
    bg [$jobId]:把挂起的job在后台继续恢复运行
    fg [$jobId]:把挂起的job恢复到前台继续运行
    2 终端常用快捷键
    control+c:发送terminal信号给进程,相当于终止进程。
    control+z:把当前进程挂起到后台。可以通过bg或者fg来恢复运行。
    control+d:输入结束符,表示不再有输入。对于终端来说,输入结束则意味着回话结束,所以常用来退出当前终端session。

    Linux中文乱码排查思路

    1 终端中文乱码排查思路:

    echo $LANG
    
    locale
    
    locale -a | grep zh
    
    LANG=zh_CN.utf8
    
    # 把gbk编码的转码成utf-8
    cat info.log | iconv -f gbk -t utf-8
    
    # 终端,系统,文件 三个编码一致才不会乱码
    
    

    2 vim中的乱码排查思路:

    关注这四个options (termencoding, encoding, fileencoding, fileencodings),
    可以通过:set来查看,或者在~/.vimrc中查看。

    vim set常用选项

    1) 显示/隐藏行号

    :set nu
    :set nonu
    

    2)显示/设置文件编码

    #vim保存文件时用的编码
    :set fileencoding
    :set fileencoding=gbk
    
    #vim显示文件内容的编码
    :set encoding
    :set encoding=gbk
    
    #vim在打开文件时会根据fileencodings选项来识别文件编码,
    #可以同时设置多个编码,vim会根据顺序来猜测所打开文件的编码。
    #因此,如果在vim出现中文乱码,可以检查下此选项。
    :set fileencodings
    :set fileencodings=ucs-bom,utf-8,default,latin1,gbk
    

    3)显示所有set命令

    :set
    :set all
    

    4) 显示/隐藏不可见字符

    :set list
    :set nolist
    

    以上命令在命令模式执行,如果希望持久生效,可以把上述命令(去掉前面的:)存到~/.vimrc文件里(如果没有,可以创建该文件),则不必每次都重新设置,每次启动vim时会读取此文件配置。