使用 Mockito 的 @InjectMocks 创建被测试类实例
初识 Mockito 这个测试框架后,我们要使用 Mock 的属性创建一个被测试类实例时,大概会下面这么纯手工来打造。
假定类 UserService
有一个属性 UserDao userDao
, 需要构造 UserService
实例时 Mock 内部状态
UserDao userDao = Mockito.mock(UserDao.class);
UserService testMe = new UserService(userDao);
如此,userDao 的行为就可以自由模拟了,这种纯手工方式都不需要给测试类添加
@RunWith(MockitoJunitRuner.class)
//或
MockitoAnnotations.initMocks(this);
因为上面两句是给 Mockito 的注解使用的。
如果所有的 Mock 对象全部通过手工来创建,那就不容易体现出 Mockito 的优越性出来。因此对于被测试对象的创建,Mock 属性的注入应该让 @Mock
和 @InjectMocks
这两个注解大显身手了。
标注在实例变量上的 @Mock
相当于是 Mockito.mock(Class)
创建了一个 Mock 对象,而 @InjectMock
标的实例会寻找到相应 Mock 属性想法构造出被测试类的实例。看下面的例子:
UserService 类
public class UserService { private UserDao userDao; public UserService(UserDao userDao) { System.out.println("Constructor called"); this.userDao = userDao; } public UserDao getUserDao() { return userDao; } }
UserServiceTest 类
@RunWith(MockitoJUnitRunner.class) public class UserServiceTest { @Mock private UserDao userDao; @InjectMocks private UserService testMe; @Test public void testInjectMocks() { System.out.println(testMe.getUserDao().getClass()); } }
Constructor called上面测试用例的输出为
class cc.unmi.UserDao$MockitoMock$878185941
证明了 Mock 对象 userDao
成功的通过构造函数注入了 testMe
实例。
除了通过构造函数注入 Mock 的属性外, @InjectMocks
还能通过 setter
方法,属性注入
。私有的构造函数,setter 方法,属性都无法阻止 @InjectMocks
注入 Mock 对象。
下面是理解自 Mockito 官方对 @InjectMocks
的 JavaDoc 说明,链接:InjectMocks - mockito-core 2.13.0 javadoc
- Mockito 尝试按
非默认构造函数
,setter 方法
,属性
的顺序来注入 Mock 对象。如果存在一个有参数的构造函数,那么setter 方法
和属性
注入都不会发生。也就是说非默认构造函数
不会与后两种方式同时发生,但找不到setter
注入的 Mock 对象还会尝试用属性
来直接注入。 - 如果
@InjectMocks
对象只有默认构造数,那么会调用该默认构造函数,并且依次采用下面两种方式注入属性。 非默认构造函数注入
: Mockito 会选择参数个数最多的构造函数(称之为最大构造函数) -- 这样可以尽可能注入多的属性。但是有多个最大构造函数,Mockito 究竟选择哪一个就混乱,测试时应该避免这种情况的发生。- 如果构造函数中含有不可 Mock 的参数(基本类型), 则该构造函数将被 @InjectMocks 忽略掉。
setter 方法注入
: 和 Spring 类似,Mockito 首先根据属性类型(或擦除类型)找到 Mock 对象。存在多个相同类型 Mock 对象则按名称(@Mock(name="userDao1")
)进行匹配,默认名称为空。不能按名称匹配到的话,可能会选择最后声明的那个,不确定性。属性 注入
: 按 Mock 对象的类型或是名称的匹配规则与setter 方法注入
是一样的。
现在来开始有事实验证上面理解的 @InjectMocks
理论:
调用最大构造函数,调用了非默认构造函数将不会采用 setter 方法
和 属性
注入
public class UserService { public UserDao userDao; private UserService(String s1) { System.out.println("Constructor 1 called"); } private UserService(String s1, String s2) { System.out.println("Constructor 2 called"); } public void setUserDao(UserDao userDao) { System.out.println("call setter"); this.userDao = userDao; } } @RunWith(MockitoJUnitRunner.class) public class UserServiceTest { @Mock private UserDao userDao; @InjectMocks private UserService testMe; @Test public void testInjectMocks() { System.out.println(testMe.userDao); } }
上面测试执行输出为:
Constructor 2 called
null
同时证明了私有的构造函数一样被调用。
@InjectMocks 调用了默认构造函数后还能同时应用 setter 方法
和 属性
注入两种式
public class UserService { public UserDao userDao; private BookDao bookDao; public UserService() { System.out.println("Constructor 0 called"); } private void setUserDao(UserDao userDao) { System.out.println("call setter"); this.userDao = userDao; } public BookDao getBookDao() { return this.bookDao; } } @RunWith(MockitoJUnitRunner.class) public class UserServiceTest { @Mock private UserDao userDao; @Mock private BookDao bookDao; @InjectMocks private UserService testMe; @Test public void testInjectMocks() { System.out.println(testMe.userDao.getClass()); System.out.println(testMe.getBookDao().getClass()); } }
测试代码输出如下:
Constructor 0 called
class cc.unmi.UserDao$MockitoMock$1978393893
class cc.unmi.BookDao$MockitoMock$910006861
默认构造函数调用了,userDao 通过 setter 方法注入的,bookDao 通过属性直接注入的。把 setUserDao(..) 方法和 bookDao 设置为私有也是为了证明可见性不是障碍,当然 public 的更不是事。
含有基本类型参数的构造函数将被 @InjectMocks 忽略掉
public class UserService { public UserDao userDao; public UserService() { System.out.println("Constructor 0 called"); } private UserService(UserDao userDao, boolean flag) { System.out.println("Constructor 2 called"); } } @RunWith(MockitoJUnitRunner.class) public class UserServiceTest { @Mock private UserDao userDao; @InjectMocks private UserService testMe; @Test public void testInjectMocks() { System.out.println(testMe.userDao.getClass()); } }
执行测试用例的输出为:
Constructor 0 called
class cc.unmi.UserDao$MockitoMock$286493746
由于无法构造出 Mock 的 boolean 类型,所以 UserService(UserDao userDao, boolean flag) 被忽略,调用了默认构造函数,并且 userDao 通过属性进行了注入。
多个相同类型的 Mock 对象通过名称进行匹配
public class UserService { public UserDao userDao2; private UserService(UserDao userDao1, String abc) { System.out.println("Constructor 2 called"); this.userDao2 = userDao1; } } @RunWith(MockitoJUnitRunner.class) public class UserServiceTest { @Mock(name = "userDao1") private UserDao userDao1; @Mock(name = "userDao2") private UserDao userDao2; @InjectMocks private UserService testMe; @Test public void testInjectMocks() { Assert.assertEquals(userDao1, testMe.userDao2); } }
输出为:
Constructor 2 called
UserService 类中对 userDao2 和 userDao1 名称进行错位安排是为了证明名称匹配是根据注入点处的名称对比的。例如
- 构造函数注入,根据参数名进行匹配
- setter 方法注入,根据 setter 方法名, 如 setUserDao1(..), 或 setUserDao2(..) 匹配的,与方法参数无关
- 属性注入自然是以属性名本身为准
同时该例也证明了构造函数 UserService(UserDao userDao1, String abc) 对 @InjectMocks 是可见的,因为 String 是非基本类型,也是可以 Mock String 类型的。
因此,需要我们留意的是,产品代码构造函数的变动可能会改变测试代码的行为,或是导致测试的失败。
@InjectMocks 只能注入 Mock 对象,例如以下均是 Mock 对象
- UserDao userDao = Mockito.mock(UserDao.class);
- @Mock private UserDao userDao;
- @Mock private UserDao userDao = new UserDao(); //Mockito 将会对 userDao 重新赋值为一个 Mock 对象
- UserDao userDao = spy(new UserDao());
如果是一个普通对象,例如下面的声明
private UserDao userDao = new UserDao(); @InjectMocksprivate UserService testMe;
@InjectMocks 如何费尽心思都无法把这个 userDao 注入到 testMe 测试对象中去的。对它 spy 一下就可以被注入了。
@Mock 和 @InjectMocks 会把自己赋的值丢弃
前面提到 @Mock private UserDao userDao = new UserDao(); 最终的 userDao 是一个 Mock 对象,@InjectMocks 也一样
@InjectMocks private UserService testMe = new UserService();
虽然会调用一下 new UserService()
创建一个对象,但最终的值是由 @InjectMocks 产生的。
备注一个使用 @Mock 对象创建被测试实例的错误
@RunWith(MockitoJUnitRunner.class) public class UserServiceTest { @Mock private UserDao userDao; private UserService testMe = new UserService(userDao); //此时 userDao 还是 null @Before public void setup() { testMe = new UserService(userDao); //这里的 userDao 才是一个 Mock 对象 } }
静态测试类的示例
@RunWith(PowerMockRunner.class) @PrepareForTest({ SpringContext.class,KeywordRuleCacheData.class }) public class KeywordRuleCacheDataRefreshDealTest { @InjectMocks KeywordRuleCacheDataRefreshDeal keywordRuleCacheDataRefreshDeal; @Test public void run() throws BaseAppException { PowerMockito.mockStatic(SpringContext.class); PowerMockito.mockStatic(KeywordRuleCacheData.class); PowerMockito.when(KeywordRuleCacheData.refushKeywordRuleCacheData()).thenReturn(true); keywordRuleCacheDataRefreshDeal.run(); } }