使用 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 类

UserServiceTest 类

上面测试用例的输出为

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

  1. Mockito 尝试按 非默认构造函数, setter 方法, 属性 的顺序来注入 Mock 对象。如果存在一个有参数的构造函数,那么 setter 方法 和 属性  注入都不会发生。也就是说 非默认构造函数 不会与后两种方式同时发生,但找不到 setter 注入的 Mock 对象还会尝试用 属性 来直接注入。
  2. 如果 @InjectMocks 对象只有默认构造数,那么会调用该默认构造函数,并且依次采用下面两种方式注入属性。
  3. 非默认构造函数注入: Mockito 会选择参数个数最多的构造函数(称之为最大构造函数) -- 这样可以尽可能注入多的属性。但是有多个最大构造函数,Mockito 究竟选择哪一个就混乱,测试时应该避免这种情况的发生。
  4. 如果构造函数中含有不可 Mock 的参数(基本类型), 则该构造函数将被 @InjectMocks 忽略掉。
  5. setter 方法注入: 和 Spring 类似,Mockito 首先根据属性类型(或擦除类型)找到 Mock 对象。存在多个相同类型 Mock 对象则按名称(@Mock(name="userDao1"))进行匹配,默认名称为空。不能按名称匹配到的话,可能会选择最后声明的那个,不确定性。
  6. 属性 注入: 按 Mock 对象的类型或是名称的匹配规则与 setter 方法注入 是一样的。

现在来开始有事实验证上面理解的 @InjectMocks 理论:

调用最大构造函数,调用了非默认构造函数将不会采用 setter 方法 和 属性 注入

上面测试执行输出为:

Constructor 2 called
null

同时证明了私有的构造函数一样被调用。

@InjectMocks 调用了默认构造函数后还能同时应用 setter 方法 和 属性 注入两种式

测试代码输出如下:

Constructor 0 called
class cc.unmi.UserDao$MockitoMock$1978393893
class cc.unmi.BookDao$MockitoMock$910006861

默认构造函数调用了,userDao 通过  setter 方法注入的,bookDao 通过属性直接注入的。把 setUserDao(..) 方法和 bookDao  设置为私有也是为了证明可见性不是障碍,当然 public 的更不是事。

含有基本类型参数的构造函数将被 @InjectMocks 忽略掉

执行测试用例的输出为:

Constructor 0 called
class cc.unmi.UserDao$MockitoMock$286493746

由于无法构造出 Mock 的 boolean 类型,所以 UserService(UserDao userDao, boolean flag) 被忽略,调用了默认构造函数,并且 userDao 通过属性进行了注入。

多个相同类型的 Mock 对象通过名称进行匹配

输出为:

Constructor 2 called

UserService 类中对 userDao2 和 userDao1 名称进行错位安排是为了证明名称匹配是根据注入点处的名称对比的。例如

  1. 构造函数注入,根据参数名进行匹配
  2. setter 方法注入,根据 setter 方法名, 如 setUserDao1(..), 或 setUserDao2(..) 匹配的,与方法参数无关
  3. 属性注入自然是以属性名本身为准

同时该例也证明了构造函数 UserService(UserDao userDao1, String abc) 对 @InjectMocks 是可见的,因为 String 是非基本类型,也是可以 Mock String 类型的。

因此,需要我们留意的是,产品代码构造函数的变动可能会改变测试代码的行为,或是导致测试的失败。

@InjectMocks 只能注入 Mock 对象,例如以下均是 Mock 对象

  1. UserDao userDao = Mockito.mock(UserDao.class);
  2. @Mock private UserDao userDao;
  3. @Mock private UserDao userDao = new UserDao();    //Mockito 将会对 userDao 重新赋值为一个  Mock 对象
  4. UserDao userDao = spy(new UserDao());

如果是一个普通对象,例如下面的声明

@InjectMocks 如何费尽心思都无法把这个  userDao  注入到 testMe  测试对象中去的。对它 spy 一下就可以被注入了。

@Mock 和 @InjectMocks 会把自己赋的值丢弃

前面提到 @Mock private UserDao userDao = new UserDao(); 最终的 userDao 是一个 Mock  对象,@InjectMocks  也一样

虽然会调用一下 new UserService() 创建一个对象,但最终的值是由 @InjectMocks 产生的。

 


备注一个使用 @Mock 对象创建被测试实例的错误

链接:

  1. Mockito JavaDoc -- Annotation Type InjectMocks

本文链接 https://yanbin.blog/mockito-injectmocks-initialize-tested-instance/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

8 Comments
Inline Feedbacks
View all comments
yushuai_w
yushuai_w
6 years ago

public class IndexServiceImpl implements IndexService {
public TranslateService translateService;

private void setTranslateService(TranslateService translateService) {
this.translateService = translateService;
}

@Override
public String output() throws Exception {
String description = read();
//TranslateService translateService = new TranslateServiceImpl();
Map rules = translateService.get(description);
ComputeService computeService = new ComputeServiceImpl(rules);
return computeService.get(description);
}
}
@RunWith(MockitoJUnitRunner.class)
public class IndexServiceTest {
@InjectMocks
private IndexServiceImpl indexService;
@Mock
private TranslateService translateService;

@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}

@Test
public void whenInputThenCorrectOutput() throws Exception {
//部分模拟
indexService = spy(IndexServiceImpl.class);

//模拟翻译服务
when(translateService.get(anyString())).thenReturn(exchangeRule);

//主要测试内容: 输出
String output = indexService.output();
verify(indexService).read();
verify(translateService).get(anyString());
Assert.assertNotNull(output);
Assert.assertFalse(output.isEmpty());

}
}

yushuai_w
yushuai_w
6 years ago

博主大大,好,请问我这样注入 TranslateServise 为什么无法注入成功 ?
@RunWith(MockitoJUnitRunner.class)
public class IndexServiceTest {
@InjectMocks
private IndexServiceImpl indexService;
@Mock
private TranslateService translateService;

@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}

@Test
public void whenInputThenCorrectOutput() throws Exception {
//部分模拟
indexService = spy(IndexServiceImpl.class);

//模拟翻译服务
when(translateService.get(anyString())).thenReturn(exchangeRule);

//主要测试内容: 输出
String output = indexService.output();
verify(indexService).read();
verify(translateService).get(anyString());
Assert.assertNotNull(output);
Assert.assertFalse(output.isEmpty());

}
public class IndexServiceImpl implements IndexService {
public TranslateService translateService;

private void setTranslateService(TranslateService translateService) {
this.translateService = translateService;
}

@Override
public String output() throws Exception {
String description = read();
//TranslateService translateService = new TranslateServiceImpl();
Map rules = translateService.get(description);
ComputeService computeService = new ComputeServiceImpl(rules);
return computeService.get(description);
}
}

yushuai_w
yushuai_w
6 years ago
Reply to  Yanbin

博主您好,您是说 indexService = spy(IndexServiceImpl.class); 这句有问题吗?这句去掉的话,会报错。 2. IndexService 里的TranlateService一直为空,注入没成功呢。

yushuai_w
yushuai_w
6 years ago
Reply to  Yanbin

收到,感谢博主大大~!

laixintao
6 years ago

好复杂的行为,像python这种动态的语言可以运行的时候替换掉对象的方法。比如可以在测试开启的时候把模块的某个方法替换掉,做到Mock