Java云同桌系列(二十五)——单元测试
为什么要做单元测试?
- 检测特定的、明确的、细颗粒的功能
- 单元测试不仅仅保证当前代码的正确性,还用来保证代码修复、改进或重构之后的正确性
1. Mockito简介
Mockito是当前主流的单元测试Mock框架,主要用于对一些不易构造或外部依赖强的对象采用虚拟的方式在封闭的环境中进行测试。
官方文档:http://site.mockito.org/mockito/docs/current/org/mockito/Mockito.html
2. 原理浅谈
Mockito本质上是应用了Proxy代理模式,在真实对象调用之前,经过代理对象,进行判断决定相应的处理。一般使用该框架的目的,都是为了使用代理对象返回一个预设的返回值。
更底层的研究,之所以能返回预设的返回值,是因为保存被代理方法的返回值、参数名、入参信息等等,通过使用了一种名为“Stub“的方式设置了返回值,该方式通常被称之为”打桩“,可以理解为满足形式要求但没有实现实际功能的占坑/代理代码,不会影响原有流程。
3. Mockito使用详解
3.1 初始化对象
初始化对象更推荐注解的方式,方便快捷
-
mock 对象 使用@Mock 修饰需要mock的对象,对于使用Dubbo框架的其他服务的对象,还需要使用@DubboReference修饰后才可以成功mock -
真实对象 使用@Spy 修饰需要真实调用其方法的对象 -
注入对象(通常是被测试对象) 使用@InjectMocks 修饰对象,会将 @Mock、@Spy对象自动注入进去 -
运行器 @Runwith 不同于SpringBootTest使用SpringRunner.class,mockito需要使用MockitoJUnitRunner.class
初始化代码示例:
@RunWith(MockitoJUnitRunner.class)
public class MockDemoMockitoTest {
//mock对象
@Mock
UserService userService;
//真实对象
// @Spy
@InjectMocks
//被测试对象,@mock与 @spy 的对象会自动注入进去
MockDemo mockDemo;
@Before
public void before() throws Exception {
//每次单元测试执行前执行此逻辑
}
@After
public void after() throws Exception {
//每次单元测试执行后执行此逻辑
}
@Test
public void mockitoTest() {
//given 测试预设
//when 执行操作
//then 结果断言
}
}
MockIto官方较为推荐测试驱动开发的格式编写单元测试,即使用//given //when //then 注释为测试用法基石
3.2 Mock方法
-
有返回值方法 最常用的便是When(被mock的方法.(预设参数)).thenReturn(预设的返回值) 方式
Mockito.when(userService.getOneByIdCard("111")).thenReturn(new User());
被mock的原逻辑 User user = userService.getOneByIdCard(idCard);
? 指定参数时,也可以忽略,使用any()作为参数,表示任意参数都可以返回指定的值
-
无返回值方法 通过情况该种Mock比较少见,不可以用上一种方法,因为When()不可以传入返回值为void 的方法,需要使用doAnswer(预设逻辑).when(mock对象).mock方法(预设参数) Mockito.doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
//获取方法执行时的第0个参数
User user = (User)invocationOnMock.getArgument(0);
user.setNickName("修改无返回值的方法内部逻辑");
return null;
}
}).when(userService).updateById(any());
被mock的原逻辑 //无返回值的方法
userService.updateById(user);
-
mock 抛出异常 mock 某个方法抛出异常来测试某些异常处理逻辑doThrow(mock抛出的异常).when(mock对象).mock方法(预设参数); Mockito.doThrow(new RuntimeException()).when(userService).updateById(any());
3.3 断言
有关断言的工具类比较多,此处只简单列两类个人比较常用的断言,更多断言请读者自行百度
-
断言值 //断言值
Assert.assertEquals("修改无返回值的方法内部逻辑",user.getNickName());
-
断言执行次数 //断言指定方法的任意参数,被执行指定次数
Mockito.verify(userService,Mockito.times(1)).getOneByIdCard(any());
3.4 前置后置处理
使用@Before 和 @After 修饰方法,可以在所有测试方法执行之前,执行之后执行特定的代码
@Before
public void before() {
//所有测试方法执行前执行
}
@After
public void after(){
//所有测试方法执行后执行
}
4. 完整示例代码
被测试类
@Service
public class MockDemo {
@Autowired
UserService userService;
public User mockitoTest(String idCard){
User user = userService.getOneByIdCard(idCard);
userService.updateById(user);
return user;
}
}
测试类
@RunWith(MockitoJUnitRunner.class)
public class MockDemoMockitoTest {
@Mock
UserService userService;
@InjectMocks
MockDemo mockDemo;
@Before
public void before() throws Exception {
}
@After
public void after() throws Exception {
}
@Test
public void mockitoTest() {
Mockito.when(userService.getOneByIdCard("111")).thenReturn(new User());
Mockito.doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
User user = (User)invocationOnMock.getArgument(0);
user.setNickName("修改无返回值的方法内部逻辑");
return null;
}
}).when(userService).updateById(any());
User user = mockDemo.mockitoTest("111");
Assert.assertEquals("修改无返回值的方法内部逻辑",user.getNickName());
Mockito.verify(userService,Mockito.times(1)).getOneByIdCard(any());
}
我们完整的梳理一遍
- 测试类中调用执行mockitoTest()并传入参数为111
- 然后执行getOneByIdCard()时,我们有mock的逻辑,参数也是和mock逻辑要求的参数一致,然后该方法按照mock逻辑返回一个新的User对象
- 然后执行到无返回值的updateById()方法,该方法也在测试类中被mock,我们在该mock逻辑中,先获取到传入updateById方法的参数User对象,然后将其NickName属性修改为固定字符串,然后结束
- 至此,主方法运行完毕,回到测试方法的断言部分
- 首先断言返回的User对象的NickName属性是否是我们mock设置的一个固定字符串
- 然后断言,getOneByIdCard()是否执行了一次
- 测试类全部执行完毕,如果断言全部正确,则不会报任何问题,然后任一断言不通过,则会报ComparisonFailure
5. PowerMock
PowerMock是基础单元测试框架的升级版,提供了一些更加强大的mock功能。相比Mockito框架,PowerMock解决了Mockito不能mock私有方法、静态方法的缺陷
5.1 Maven依赖
? 不同于Mockito已经集成到Springboot依赖中,PowerMock需要单独添加依赖,并且依赖版本与Mockito依赖版本有一些限制,;两个依赖对应的版本对应关系建议百度,此处列出我当前使用无问题的版本
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.7.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>1.7.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.8.9</version>
<scope>test</scope>
</dependency>
5.2 mock方法
相比于Mockito,PowerMock的用法基本保持一致,只是提供了更多种的mock手段
powerMock测试类一般需要加如下两个注解
//运行器 选择PowerMockRunner
@RunWith(PowerMockRunner.class)
//修饰被测试的方法(若mock私有方法时加此注解)
@PrepareForTest(MockDemo.class)
-
mock静态方法 只需要在调用when前使用MockStatic方法 //mock静态方法
PowerMockito.mockStatic(MockDemo.class);
PowerMockito.when(mockDemo.updateNickName(any())).thenReturn(user.setNickName("嘿嘿,静态方法被我Mock了"));
-
mock 私有方法 被mock的方法 public String powerMockPrivateTest(){
//private method
return getString();
}
mock逻辑,特别注意此处需要方法调用对象,是Spy真实对象,而非mock对象 MockDemo spy = PowerMockito.spy(new MockDemo());
PowerMockito.when(spy,"getString").thenReturn("private method mock return");
5.3 完整示例代码
被测试方法
public class MockDemo {
@Autowired
UserService userService;
public User powerMockTest(String idCard){
User user = userService.getOneByIdCard(idCard);
user = MockDemo.updateNickName(user);
return user;
}
public String powerMockPrivateTest(){
return getString();
}
private String getString(){
return "private method return";
}
static User updateNickName(User user){
return user.setNickName("静态方法修改");
}
}
测试方法
@RunWith(PowerMockRunner.class)
@PrepareForTest(MockDemo.class)
public class MockDemoPowerMockTest {
//mock对象
@Mock
UserService userService;
//真实对象
// @Spy
@InjectMocks
//被测试对象,@mock与 @spy 的对象会自动注入进去
MockDemo mockDemo;
@Before
public void before() throws Exception {
//每次单元测试执行前执行此逻辑
}
@After
public void after() throws Exception {
//每次单元测试执行后执行此逻辑
}
@Test
public void powerMockTest() throws Exception {
//given 测试预设
User user = new User();
Mockito.when(userService.getOneByIdCard(any())).thenReturn(user);
//mock静态方法
PowerMockito.mockStatic(MockDemo.class);
PowerMockito.when(mockDemo.updateNickName(any())).thenReturn(user.setNickName("嘿嘿,静态方法被我Mock了"));
//when
User result = mockDemo.powerMockTest("");
//then
//断言执行了Mock的静态方法,昵称为指定mock字符串
Assert.assertEquals("嘿嘿,静态方法被我Mock了",result.getNickName());
}
@Test
public void powerMockPrivateTest() throws Exception {
MockDemo spy = PowerMockito.spy(new MockDemo());
PowerMockito.when(spy,"getString").thenReturn("private method mock return");
String s = spy.powerMockPrivateTest();
//assert this return string from mock
Assert.assertEquals("private method mock return",s);
}
6. 延伸
-
SquareTest插件推荐 对于编写大量的单元测试时,可以使用SquareTest 插件,帮助生成结构代码,也就是测试类的一些常用注解,when结构代码,需要的对象也都会按默认值赋值,省去大量时间编写这些结构代码,可能更加专注于测试主要逻辑 -
IDEA单元测试覆盖率 很多时候需要注意某个类的单元测试覆盖率,此时可以点击执行单元测试类的绿色按钮,第一种是run,第二种是debug,第三种就是统计覆盖率了 此时回到被测试类,会被看到绿色色块的就是被测试覆盖的代码,红色就是测试未覆盖,切到左侧的project侧边栏,还可以看到该类的覆盖百分比
|