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