使用 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 类
1public class UserService {
2
3 private UserDao userDao;
4
5 public UserService(UserDao userDao) {
6 System.out.println("Constructor called");
7 this.userDao = userDao;
8 }
9
10 public UserDao getUserDao() {
11 return userDao;
12 }
13}UserServiceTest 类
1@RunWith(MockitoJUnitRunner.class)
2public class UserServiceTest {
3
4 @Mock
5 private UserDao userDao;
6
7 @InjectMocks
8 private UserService testMe;
9
10 @Test
11 public void testInjectMocks() {
12 System.out.println(testMe.getUserDao().getClass());
13 }
14}上面测试用例的输出为
Constructor called
class cc.unmi.UserDao$MockitoMock$878185941
证明了 Mock 对象 userDao 成功的通过构造函数注入了 testMe 实例。
除了通过构造函数注入 Mock 的属性外,@InjectMocks 还能通过 setter 方法,属性注入。私有的构造函数,setter 方法,属性都无法阻止 @InjectMocks 注入 Mock 对象。
下面是理解自 Mockito 官方对 @InjectMocks 的 JavaDoc 说明,链接:https://static.javadoc.io/org.mockito/mockito-core/2.13.0/org/mockito/InjectMocks.html
- Mockito 尝试按
非默认构造函数,setter 方法,属性的顺序来注入 Mock 对象。如果存在一个有参数的构造函数,那么setter 方法和属性注入都不会发生。也就是说非默认构造函数不会与后两种方式同时发生,但找不到setter注入的 Mock 对象还会尝试用属性来直接注入。 - 如果
@InjectMocks对象只有默认构造数,那么会调用该默认构造函数,并且依次采用下面两种方式注入属性。 非默认构造函数注入: Mockito 会选择参数个数最多的构造函数(称之为最大构造函数) -- 这样可以尽可能注入多的属性。但是有多个最大构造函数,Mockito 究竟选择哪一个就混乱,测试时应该避免这种情况的发生。- 如果构造函数中含有不可 Mock 的参数(基本类型), 则该构造函数将被 @InjectMocks 忽略掉。
setter 方法注入: 和 Spring 类似,Mockito 首先根据属性类型(或擦除类型)找到 Mock 对象。存在多个相同类型 Mock 对象则按名称(@Mock(name="userDao1"))进行匹配,默认名称为空。不能按名称匹配到的话,可能会选择最后声明的那个,不确定性。属性 注入: 按 Mock 对象的类型或是名称的匹配规则与setter 方法注入是一样的。
现在来开始有事实验证上面理解的 @InjectMocks 理论:
调用最大构造函数,调用了非默认构造函数将不会采用 setter 方法 和 属性 注入
1public class UserService {
2 public UserDao userDao;
3
4 private UserService(String s1) {
5 System.out.println("Constructor 1 called");
6 }
7
8 private UserService(String s1, String s2) {
9 System.out.println("Constructor 2 called");
10 }
11
12 public void setUserDao(UserDao userDao) {
13 System.out.println("call setter");
14 this.userDao = userDao;
15 }
16}
17
18@RunWith(MockitoJUnitRunner.class)
19public class UserServiceTest {
20
21 @Mock
22 private UserDao userDao;
23
24 @InjectMocks
25 private UserService testMe;
26
27 @Test
28 public void testInjectMocks() {
29 System.out.println(testMe.userDao);
30 }
31}上面测试执行输出为:
Constructor 2 called
null
同时证明了私有的构造函数一样被调用。
@InjectMocks 调用了默认构造函数后还能同时应用 setter 方法 和 属性 注入两种式
1public class UserService {
2
3 public UserDao userDao;
4 private BookDao bookDao;
5
6 public UserService() {
7 System.out.println("Constructor 0 called");
8 }
9
10 private void setUserDao(UserDao userDao) {
11 System.out.println("call setter");
12 this.userDao = userDao;
13 }
14
15 public BookDao getBookDao() {
16 return this.bookDao;
17 }
18}
19
20@RunWith(MockitoJUnitRunner.class)
21public class UserServiceTest {
22
23 @Mock
24 private UserDao userDao;
25
26 @Mock
27 private BookDao bookDao;
28
29 @InjectMocks
30 private UserService testMe;
31
32 @Test
33 public void testInjectMocks() {
34 System.out.println(testMe.userDao.getClass());
35 System.out.println(testMe.getBookDao().getClass());
36 }
37}测试代码输出如下:
Constructor 0 called
class cc.unmi.UserDao$MockitoMock$1978393893
class cc.unmi.BookDao$MockitoMock$910006861
默认构造函数调用了,userDao 通过 setter 方法注入的,bookDao 通过属性直接注入的。把 setUserDao(..) 方法和 bookDao 设置为私有也是为了证明可见性不是障碍,当然 public 的更不是事。
含有基本类型参数的构造函数将被 @InjectMocks 忽略掉
1public class UserService {
2
3 public UserDao userDao;
4
5 public UserService() {
6 System.out.println("Constructor 0 called");
7 }
8
9 private UserService(UserDao userDao, boolean flag) {
10 System.out.println("Constructor 2 called");
11 }
12}
13
14@RunWith(MockitoJUnitRunner.class)
15public class UserServiceTest {
16
17 @Mock
18 private UserDao userDao;
19
20 @InjectMocks
21 private UserService testMe;
22
23 @Test
24 public void testInjectMocks() {
25 System.out.println(testMe.userDao.getClass());
26 }
27}执行测试用例的输出为:
Constructor 0 called
class cc.unmi.UserDao$MockitoMock$286493746
由于无法构造出 Mock 的 boolean 类型,所以 UserService(UserDao userDao, boolean flag) 被忽略,调用了默认构造函数,并且 userDao 通过属性进行了注入。
多个相同类型的 Mock 对象通过名称进行匹配
1public class UserService {
2
3 public UserDao userDao2;
4
5 private UserService(UserDao userDao1, String abc) {
6 System.out.println("Constructor 2 called");
7 this.userDao2 = userDao1;
8 }
9}
10
11@RunWith(MockitoJUnitRunner.class)
12public class UserServiceTest {
13
14 @Mock(name = "userDao1")
15 private UserDao userDao1;
16
17 @Mock(name = "userDao2")
18 private UserDao userDao2;
19
20 @InjectMocks
21 private UserService testMe;
22
23 @Test
24 public void testInjectMocks() {
25 Assert.assertEquals(userDao1, testMe.userDao2);
26 }
27}输出为:
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());
如果是一个普通对象,例如下面的声明
1private UserDao userDao = new UserDao();
2
3@InjectMocks
4private UserService testMe;@InjectMocks 如何费尽心思都无法把这个 userDao 注入到 testMe 测试对象中去的。对它 spy 一下就可以被注入了。
@Mock 和 @InjectMocks 会把自己赋的值丢弃
前面提到 @Mock private UserDao userDao = new UserDao(); 最终的 userDao 是一个 Mock 对象,@InjectMocks 也一样
1@InjectMocks
2private UserService testMe = new UserService();虽然会调用一下 new UserService() 创建一个对象,但最终的值是由 @InjectMocks 产生的。
备注一个使用 @Mock 对象创建被测试实例的错误
1@RunWith(MockitoJUnitRunner.class)
2public class UserServiceTest {
3
4 @Mock
5 private UserDao userDao;
6
7 private UserService testMe = new UserService(userDao); //此时 userDao 还是 null
8
9 @Before
10 public void setup() {
11 testMe = new UserService(userDao); //这里的 userDao 才是一个 Mock 对象
12 }
13}链接:
永久链接 https://yanbin.blog/mockito-injectmocks-initialize-tested-instance/, 来自 隔叶黄莺 Yanbin's Blog[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。