目前开发中,单元测试遇到的问题
在业务代码开发完成以后,需要对新增代码进行单元测试,由于项目依赖的第三方组件以及外部系统接口较多,每次执行单元测试时都需要启动整个项目,加载各种依赖,而且由于网络限制有时还需要申请各种ACL,且项目启动耗时较长,有时仅仅为了跑一个仅有几行代码的单元测试,却要耗时几分钟等待项目的启动,严重违背单元测试的初衷。这种情况导致了大家写单元测试的积极性不高,甚至直接跳过单元测试,直接把代码发布到测试环境进行集成测试。
解决方案–Mock
上述问题有没有解决方案呐,答案是肯定是有的,那就是Mock,对外部依赖进行mock,仅执行自己的代码。目前mock的工具有很多,像PowerMock
,EasyMock
,Mockito
等。下面针对Mockito
的使用方法做一个简单的介绍
Junit4 + Mockito:
先来看一个例子:
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserInfoMapper userInfoMapper;
@Resource
private WhiteListCheckService whiteListCheckService;
@Resource
private UserLevelRemoteProxy userLevelRemoteProxy;
@Override
public UserInfoBo queryUserInfo(String userId){
UserInfoEntity userInfo = userInfoMapper.findById(userId);
boolean isWhiteListUser = whiteListCheckService.isWhiteListUser(userId);
String userLevel = userLevelRemoteProxy.queryUserLevelById(userId);
UserInfoBo userInfoBo = BeanUtils.copy(UserInfoBo.class, userInfo);
userInfoBo.setUserLevel(userLevel);
userInfoBo.isWhiteListUser(isWhiteListUser);
return userInfoBo;
}
}
上面是一个常用类型的service方法,只是省略了一些业务逻辑,方法中会调用mapper方法查询数据库,系统内部其他service方法,还有外部接口.现在我们使用Junit4+Mockito来对该方法进行单元测试:
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest{
@InjectMocks
private UserServiceImpl userService;
@Mock
private UserInfoMapper userInfoMapper;
@Mock
private UserLevelRemoteProxy userLevelRemoteProxy;
@Test
public void queryUserInfoTest(){
UserInfoEntity userInfo = Mockito.mock(UserInfoEntity.class);
Mockito.when(whiteListCheckService.isWhiteListUser(Mockito.anyString))
.thenReturn(true);
Mockito.when(userInfoMapper.findById(Mockito.anyString())).thenReturn(userInfo);
Mockito.when(userLevelRemoteProxy.queryUserLevelById(Mockito.anyString()))
.thenReturn("12Lev");
UserInfoBo userInfoBo = userService.queryUserInfo("123");
Assert.assertEquals("12Lev", userInfoBo.getUserLevel());
}
}
执行上述方法,会在执行userInfoMapper.findById
方法时,返回我们mock的UserInfoEntity
的实例,当执行whiteListCheckService.isWhiteListUser
方法时,会返回true
,执行userLevelRemoteProxy.queryUserLevelById
方法时,会返回12Lev
。我们在把依赖的接口进行Mock以后,就可以在不启动项目的前提下执行该单元测试了,进而提高了单元测试的执行速度。同时也可以对代码里的不同分支编写多个单元测试,进一步提升代码的单元测试覆盖率。
Mockito常用注解:
@Mock
:创建一个Mock实例@InjectMocks
:会把@Mock
和@Spy
注解的对象自动注入进来,一般用于创建需要被测试的对象实例@Spy
:允许创建部分模拟的对象
Mockito常用方法:
- 参数匹配器:
Mockito.anyString()
/Mockito.anyInt()
/Mockito.any()
- 方法执行校验器:
Mockito.verify()
- 锚点方法调用及指定返回值:
Mockito.when(mock.someMethod()).thenThrow(new RuntimeException).thenReturn("foo")
- 执行真实实例方法:
Mockito.spy()
更多方法请参考官方文档: https://javadoc.io/doc/org.mockito/mockito-core/latest/index-files/index-1.html
Tips:
-
Java 8 Lambda 匹配器的支持(
Since 2.1.0
):@Test public void testMethod(){ List<String> list = Mockito.mock(List.class); list.add("111"); list.add("222"); list.add("333"); Mockito.verify(list,Mockito.times(3)).add(Mockito.argThat(s -> s.length() <5));//list中最多被添加4个元素 }
-
Mocking
final type
,enums
andfinal methods
(Since 2.1.0
)Mockito从2.1.0版本开始支持对
final type
,enums
andfinal methods
进行mock,但是需要额外的配置,详情请参考: Mocking final types, enums and final methods -
对静态方法进行Mock(
Since 3.4.0
)需要把mockito-core依赖替换成mockito-inline,且对jdk版本有要求,若使用是jdk8或更早版本的,需要使用到ByteBuddy 依赖
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-inline</artifactId> <version>3.7.7</version> <scope>test</scope> </dependency> <!-- 若使用的是jdk8或更早版本,需要添加如下依赖 --> <dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>1.12.1</version> </dependency> <!-- 若使用的是jdk8或更早版本,需要添加如下依赖 --> <dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy-agent</artifactId> <version>1.12.1</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13</version> </dependency>
@Test public void mockStaticTest(){ LocalDate yearOf2000 = LocalDate.of(2000, 1, 1); try (MockedStatic theMock = Mockito.mockStatic(LocalDate.class)) { theMock.when(LocalDate::now).thenReturn(yearOf2000); Assert.assertEquals(2000, LocalDate.now().getYear()); } }
-
对于私有方法的mock
mockito是不支持对是由方法进行mock的,如果有这方面的需求,可以结合使用ProwerMock来做,至于mockito为什么不支持对私有方法进行mock,官方的说法如下:
总结
关于写单元测试,很多人认为是一件性价比不高的事情,与其花费大量时间对一些一眼就能看出执行结果的方法编写单元测试,还不如直接进行集成测试,跑一下主流程,没问题就可以提测了,但是这样做往往会忽略分支流程的测试覆盖,导致提测的代码质量不高,甚至会成为导致线上问题的隐患。所以,个人认为编写单元测试还是很有必要的,至少,要对新增代码进行单元测试覆盖。另外,现在有一些测试代码生成插件,可以帮助我们直接生成单元测试,大家感兴趣的话,可以尝试使用一下
参考