Powermockito和Mockito使用总结
最近公司在推进Java应用的单元测试,要求将单元测试的覆盖率提高到50%以上,保证上线代码充分自测。公司单元测试框架选用了 Junit 4.12
,Mock框架选用了 Mockito
和 PowerMock
,同时选用 JaCoCo
来做覆盖率检测,下面详细介绍一下我在使用这几个框架的一些经验。
依赖引入
<dependency>
<groupId>org.mockitogroupId>
<artifactId>mockito-coreartifactId>
<version>2.8.9version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.powermockgroupId>
<artifactId>powermock-module-junit4artifactId>
<version>1.7.4version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.powermockgroupId>
<artifactId>powermock-api-mockito2artifactId>
<version>1.7.4version>
<scope>testscope>
dependency>
复制代码
PowerMockito的使用
Mockito、EasyMock、JMock等比较流行Mock框架有个共同的缺点,都不能mock静态、final、私有方法等,而PowerMock可以完美解决以上框架的不足,接下来让我们看看无所不能的PowerMock是如何解决上述问题,我们先从一个实例来看,下面的代码是需要单测的类,被测类中既有对Spring Bean的调用,同时又包含对Redis的静态读取,另外还有 getInstance()
单例类的使用,现在,我们一一来mock上述的外部类。
被单测类(主要测试queryStudentScoreByKeyword方法)
@Component
public class StudentService {
@Resource
private StudentDao studentDao;
public List queryStudentScoreByKeyword(String name) {
System.out.println("invoke StudentService.queryStudentScoreByKeyword ...");
List cacheList = RedisUtils.getArray(name, StudentBo.class);
if (CollectionUtils.isNotEmpty(cacheList)) {
return cacheList;
}
String keyword = processKeyword(name);
List students = studentDao.queryStudentByKeyWord(keyword);
List ids = students.stream().map(Student::getId).collect(Collectors.toList());
List personList = SchoolManageProxy.getInstance().queryPerson(ids);
List studentBos = CommonUtils.toBo(personList, students);
// 高亮结果
highlightResult(studentBos, name);
// 缓存到Redis
RedisUtils.setArray(name, studentBos, 10 * 60);
return studentBos;
}
private String processKeyword(String name) {
System.out.println("invoke StudentService.processKeyword ...");
String newName = name;
// do somethings
return newName;
}
private void highlightResult(List result, String name) {
System.out.println("invoke StudentService.highlightResult ...");
// do keyword highlight
}
}
复制代码
单测类
@RunWith(PowerMockRunner.class)
@PrepareForTest({SchoolManageProxy.class, RedisUtils.class, StudentService.class})
// @PowerMockIgnore({"javax.management.*", "javax.net.ssl.*"})
@SuppressStaticInitializationFor({"cn.ganzhiqiang.ares.unittest.SchoolManageProxy"})
public class StudentServiceTest {
@Mock
private StudentDao mockStudentDao;
@InjectMocks
private StudentService studentServiceUnderTest;
@Before
public void setUp() {
initMocks(this);
}
@Test
public void testQueryStudentScoreByKeyword() throws Exception {
studentServiceUnderTest = PowerMockito.spy(studentServiceUnderTest);
PowerMockito.mockStatic(RedisUtils.class);
PowerMockito.mockStatic(SchoolManageProxy.class);
// mock单例调用
SchoolManageProxy mockSchoolManageProxy = PowerMockito.mock(SchoolManageProxy.class);
PowerMockito.when(SchoolManageProxy.getInstance()).thenReturn(mockSchoolManageProxy);
when(mockSchoolManageProxy.queryPerson(anyList())).thenReturn(Collections.emptyList());
// mock掉对Redis的静态调用
PowerMockito.when(RedisUtils.getArray(eq("tom"), eq(StudentBo.class))).thenReturn(Collections.emptyList());
// 显示的mock掉静态的void的方法(可以不mock)
PowerMockito.doNothing().when(RedisUtils.class, "setArray", anyString(), anyList(), anyInt());
// mock私有方法processKeyword
PowerMockito.doReturn("tom").when(studentServiceUnderTest, "processKeyword", anyString());
// 跳过私有方法highlightResult的执行
PowerMockito.suppress(PowerMockito.method(StudentService.class, "highlightResult"));
// 使用Mockito来mock服务的调用
when(mockStudentDao.queryStudentByKeyWord(anyString())).thenReturn(Collections.emptyList());
// Run the test
final List result = studentServiceUnderTest.queryStudentScoreByKeyword("tom");
}
}
复制代码
使用mockito来mock实例
首选我们先用Mockito来mock对Spring Bean的调用, Mockito.mock
可以mock一个实例,我们这里选用 @Mock
注解,效果是一样的。
// 使用Mockito来mock服务的调用
when(mockStudentDao.queryStudentByKeyWord(anyString())).thenReturn(Collections.emptyList());
复制代码
mock对Redis的静态调用
接下来我们使用PowerMock来mock对静态方法的调用,注意需要将 RedisUtils
类,加入 @PrepareForTest
注解中,我们既mock了 getArray
方法,也mock了 setArray
方法,其实 setArray
不需要mock这里显式的mock了
PowerMockito.mockStatic(RedisUtils.class);
// mock掉对Redis的静态调用
PowerMockito.when(RedisUtils.getArray(eq("tom"), eq(StudentBo.class))).thenReturn(Collections.emptyList());
// 显式的mock掉静态的void的方法(可以不mock)
PowerMockito.doNothing().when(RedisUtils.class, "setArray", anyString(), anyList(), anyInt());
复制代码
mock单例类
mock单例类相对来说复杂一点,逻辑上先用Powermock mock出单例类,然后再给单例类的 getInstance
方法打桩,返回之前mock,再对mock类实际调用的方法打桩即可,代码如下
PowerMockito.mockStatic(SchoolManageProxy.class);
// Powermock mock出单例类
SchoolManageProxy mockSchoolManageProxy = PowerMockito.mock(SchoolManageProxy.class);
// 给单例类的getInstance方法打桩
PowerMockito.when(SchoolManageProxy.getInstance()).thenReturn(mockSchoolManageProxy);
// 对mock类queryPerson的方法打桩
when(mockSchoolManageProxy.queryPerson(anyList())).thenReturn(Collections.emptyList());
复制代码
mock私有方法
可以看到 queryStudentScoreByKeyword
方法调用了该类的私有方法 processKeyword
,如果该方法耗时过长,使用powermock也可以mock该私有方法, 需要注意的studentServiceUnderTest需要用spy()来mock
// mock 实例
// spy的标准是:如果不打桩,默认执行真实的方法,如果打桩则返回桩实现。
studentServiceUnderTest = PowerMockito.spy(studentServiceUnderTest);
// mock私有方法processKeyword
// doReturn(...) when(...)不做真实调用,但是when(...) thenReturn(...)还是会真实调用原方法,只是返回了指定的结果
PowerMockito.doReturn("tom").when(studentServiceUnderTest, "processKeyword", anyString());
复制代码
PowerMock跳过方法执行
使用PowerMock也可以跳过私有方法的执行
// 跳过私有方法highlightResult的执行
PowerMockito.suppress(PowerMockito.method(StudentService.class, "highlightResult"));
复制代码
总结
笔者之前写代码很少会写单测,自从公司强制要求提高单测覆盖率之后,虽然开发效率变慢了,但是确实引起我对单测的重视,进而研究了一下PowerMockito、Mokcito等Mock框架。Powermock之所以无所不能,是因为它使用了自定义的加载器和字节码操作技术,与此同时,它还十分简单易用,确实是个很优秀的框架。
Demo地址: github.com/LJWLgl/mock…
参考文档