如果一个 Java 类在初始化时会有外部依赖,这就给单元测试创建它的实例时造成困难。当然被测试类可以改造为依赖全部构造时注入或创建实例后延迟注入,这里不考虑这种改造。单说下面的例子
public class OrderService { private PriceInquiry priceInquiry = new PriceInquiry(); ......... public double totalPrice() { return priceInquiry.retrieve(....); } }
假如上面的代码是不能改动的,并且在 new PriceInquiry()
时依赖于网络环境,所以单机情况不能创建成功。也就使得测试时试图
new OrderService();
会失败。并且试图用 Mockito 的 @InjectMocks
也不行
@RunWith(MockitoJUnitRunner.class) public class OrderServiceTest { @Mock private PriceInquiry priceInquiry; @InjectMocks private OrderService testMe; @Test public void fooTest() { .... } }
会出类似下面的借
org.mockito.exceptions.base.MockitoException: Cannot instantiate @InjectMocks field named 'testMe' of type 'class cc.unmi.OrderService'. You haven't provided the instance at field declaration so I tried to construct the instance. However the constructor or the initialization block threw an exception : xxxxxxxxxxxxxxxx
想要千方百计先创建出实例再转换掉内部的 priceInquiry
属性值的打算也落空了,因为无论是用 new
还是 @InjectMocks
怎么都跳不过构造函数的执行(实例成员的初始化会放到构造函数中去,没有声明构造函数会有一个默认构造函数)
因此上面的需求就是如何在测试类跳过构造函数,初步想到的办法有四
ObjectInputstream.readObject() 反列化出 OrderService 对象,但前提是先要有序列化出的字节数据,所以不好操作,还会有 serialVersionUID
不一致的问题
@Test public void fooTest() { OrderService testMe = createTestedInstance(OrderService.class); PriceInquiry priceInquiry = Mockito.mock(PriceInquiry.class); Whitebox.setInternalState(testMe, "priceInquiry", priceInquiry); //通过反射转换掉内部属性以使用 Mock 对象 //Your test here } @SuppressWarnings("unchecked") private <T> T createTestedInstance(Class<T> clazz) { try { Field singleoneInstanceField = Unsafe.class.getDeclaredField("theUnsafe"); singleoneInstanceField.setAccessible(true); Unsafe unsafe = (Unsafe) singleoneInstanceField.get(null); return (T)unsafe.allocateInstance(clazz); } catch (Exception e) { throw new RuntimeException("cannot instantiate " + clazz + " bypassing default constructor"); } }
但是上面的代码编译时会有警告
[WARNING] COMPILATION WARNING : ...........................................sun.misc.Unsafe is internal proprietary API and may be removed in a future release
并且是没法用 SuppressWarnings
抑制住的警告,如果用 Maven 时配置了用 -Werror
编译选项的话将无法构建成功。除非不用 -Werror
选项,否则用他法来通过构建还不容易搞
又要体验到 JMockit 比 Mockito 强大之处,我们不是一般问题还不愿意祭出 JMockit 来
@Test public void fooTest() { new MockUp<OrderService>() { @mockit.Mock public void $init() { //这样就不会调用 OrderService 实际的构造函数 } }; OrderService testMe = new OrderService(); Whitebox.setInternalState(testMe, "priceInquiry", mockedPriceInquiry); //Your test here }
通常情况下我只是用 JMockit 来辅助 Mockito, 因为更习惯于 Mockito 流畅的打桩(Stubbing) 和校验(Verifying) API。
写本文之前只想到前面三种方式,借此机会又重新看了一个 JMockit 的 Deencapsulation
API,发现一个更直截了当的方式,方法名为 newUninitializedInstance(clazz)
。顾名思义,就是构造实例不初始化内部状态,恰恰是我所追求的。于是事情变得更为明了
@Test public void fooTest() { OrderService testMe = Deencapsulation.newUninitializedInstance(OrderService.class); Deencapsulation.setField(testMe, "priceInquiry", mockedPriceInquiry); //Your test here }
连设置内部状态的 API Deencapsulation
也提供了,用不着模仿着 Mockito 1 做了一个 Whitebox
类来进行反射操作。
最后,在测试中着重推荐用第四种方式,第三种方式也行,它们都是 JMockit 提供的实现。用 JMockit 来辅助 Mockito 跳过构造函数创建实例,而后替换实例的内部状态,再然后就是 Mockito 的事情了。如果被测试类的外部依赖能够通过构造函数或 setter 方法来注入就更简单了,常规手段而无需跳过构造函数就能创建被测试类的实例了。