初识 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 类
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class UserService { private UserDao userDao; public UserService(UserDao userDao) { System.out.println("Constructor called"); this.userDao = userDao; } public UserDao getUserDao() { return userDao; } } |
UserServiceTest 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@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 说明,链接: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 方法
和 属性
注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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 方法
和 属性
注入两种式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
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 忽略掉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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 对象通过名称进行匹配
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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());
如果是一个普通对象,例如下面的声明
1 2 3 4 |
private UserDao userDao = new UserDao(); @InjectMocks private UserService testMe; |
@InjectMocks 如何费尽心思都无法把这个 userDao 注入到 testMe 测试对象中去的。对它 spy 一下就可以被注入了。
@Mock 和 @InjectMocks 会把自己赋的值丢弃
前面提到 @Mock private UserDao userDao = new UserDao(); 最终的 userDao 是一个 Mock 对象,@InjectMocks 也一样
1 2 |
@InjectMocks private UserService testMe = new UserService(); |
虽然会调用一下 new UserService()
创建一个对象,但最终的值是由 @InjectMocks 产生的。
备注一个使用 @Mock 对象创建被测试实例的错误
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@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 对象 } } |
链接:
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());
}
}
博主大大,好,请问我这样注入 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);
}
}
@RunWith(MockitoJUnitRunner.class)
MockitoAnnotations.initMocks(this);
二者取其一即可。
@InjectMocks
private IndexServiceImpl indexService;
应该会调用 setter 方法 setTranslateService(TranslateService translateService) 来注入的.
测试方法中的 indexService 有什么属性值。
用了 @InjectMocks 的话就不能在测试方法中取 indexService 再次赋值。
博主您好,您是说 indexService = spy(IndexServiceImpl.class); 这句有问题吗?这句去掉的话,会报错。 2. IndexService 里的TranlateService一直为空,注入没成功呢。
indexService = spy(IndexServiceImpl.class);
你的 indexService 被重新赋值了,在 spy 之前你可以检查 indexService 中的 translateService 属性是不为空的。这里有两个问题
1. spy 应作用于已创建的实例,可以这样 IndexService spyIndexService = spy(indexService)
2. @InjextMocks 创建好的 indexService 的值不应被赋值覆盖
spyIndexService 之后 when(spyIndexService)...., spyIndexService.out() 都应该基于 spyIndexService 进行处理了。
收到,感谢博主大大~!
好复杂的行为,像python这种动态的语言可以运行的时候替换掉对象的方法。比如可以在测试开启的时候把模块的某个方法替换掉,做到Mock
Java 可没有这么自由, Mockito 是通过生成子类来替换被 Mock 对象的行为,所以限制很多,但代码更流畅。JMockit 是借助了 -javaagent, Instrumentation API 来直接替换类实现代码,也就强大的多。