前言:
什么是mock
在面向对象的程序设计中,模拟对象(mock object)是以可控的方式模拟真实对象行为的假对象。在编程过程中,通常通过模拟一些输入数据,来验证程序是否达到预期结果。
为什么使用Mock对象
1、在某些非常复杂的业务逻辑,会准备大量的数据。
2、有的时候会依赖数据库,中间件、文件系统等外部环境,这个时候我们不能控制这些外部依赖的对象。
试想一下,如果我们依赖真实的数据库环境,那么每次的单元测试结果可能都是不一样的为了解决上述两个问题,我们需要使用Mock技术。
所谓的Mock测试就是在测试过程中,对于一些不容易构造的、或者和这次单元测试无关但是上下文又有依赖的对象,用一个虚拟的对象(Mock对象)来模拟,以便单元测试能够进行。
比如有一段代码的依赖为:
当我们要进行单元测试的时候,就需要给A注入B和C但是C又依赖了D,D又依赖了E。这就导致了A的单元测试很难进行。
但是当我们使用Mock来进行模拟对象后,我们就可以把这种依赖解耦,只关心A本身的测试,它所依赖的B和C,全部使用Mock出来的对象,并且给MockB和MockC指定一个明确的行为。就像这样:
因此,当我们使用Mock后,对于那些难以构建的对象,就变成了个模拟对象,只需要提前的做Stubbing(桩)即可。所谓的做桩数据,也就是告诉Mock对象,当与之交互时执行何种行为过程。比如当调用B对象的b()方法时,我们期望返回一个true,这就是一个设置桩数据的预期。
相关的Mock工具
1 Mockito、EasyMock
EasyMock 以及 Mockito 都因为可以极大地简化单元测试的书写过程而被许多人应用在自己的工作中,但是这两种 Mock 工具都不可以实现对静态函数、构造函数、私有函数、Final 函数以及系统函数的模拟,但是这些方法往往是我们在大型系统中需要的功能。
2 powermock
PowerMock是一个扩展了其它如EasyMock等mock框架的、功能更加强大的框架。PowerMock使用一个自定义类加载器和字节码操作来模拟静态方法,构造函数,final类和方法,私有方法,去除静态初始化器等等。通过使用自定义的类加载器,简化采用的IDE或持续集成服务器不需要做任何改变。熟悉PowerMock支持的mock框架的开发人员会发现PowerMock很容易使用,因为对于静态方法和构造器来说,整个的期望API是一样的。PowerMock旨在用少量的方法和注解扩展现有的API来实现额外的功能。目前PowerMock支持EasyMock和Mockito。
目录
Mockito
Mockito底层原理
Mockito底层使用了动态代理,用到了CGLIB。因此需要被mock的对象,Mockito都会生成一个子类继承该类,这也就是为什么final类、private方法、static方法不可以被Mock的原因
Mockito应用:
现有如下代码:
实体类
@Entity
@Data
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true,nullable = false,length = 50)
private String username;
private String password;
@CreationTimestamp
private Date createDate;
public User(Long id,String username) {
this.id = id;
this.username = username;
}
}
Repository
public interface UserDao extends JpaRepository<User,Long> {
boolean updateUser(User user);
}
Service
@Service
// lombok的注解,为private final定义的变量进行自动注入,可省略写大量@Autowired
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UserServiceImpl implements IUserService {
private final UserDao userRepository;
@Override
public User findOne(Long id) {
return userRepository.getOne(id);
}
@Override
public boolean updateUsername(Long id, String username) {
User user = findOne(id);
if(user == null) {
return false;
}
user.setUsername(username);
return userRepository.updateUser(user);
}
}
Test
public class IUserServiceTest {
private IUserService userService;
// @Mock
private UserDao userRepository;
@Before
public void setUp() throws Exception {
/*对所有注解了@Mock的对象进行模拟*/
// MockitoAnnotations.initMocks(this);
/*如果不使用注解,可以对单个对象进行mock*/
userRepository = Mockito.mock(UserDao.class);
/*构造测试对象*/
userService = new UserServiceImpl(userRepository);
/*打桩,构建当userRepository getOne函数执行参数为1的时候,设置返回的结果User*/
Mockito.when(userRepository.getOne(1L)).thenReturn(new User(1L,"jack"));
/*打桩,构建当userRepository getOne函数执行参数为1的时候,设置返回的结果null*/
Mockito.when(userRepository.getOne(2L)).thenReturn(null);
/*打桩,构建当userRepository getOne函数执行参数为1的时候,设置抛出异常*/
Mockito.when(userRepository.getOne(3L)).thenThrow(new IllegalArgumentException("the id is not support"));
/*打桩,构建当userRepository updateUser执行任意User类型的参数,返回的结果都是true*/
Mockito.when(userRepository.updateUser(Mockito.any(User.class))).thenReturn(true);
/*打桩,给void方法 */
Mockito.doAnswer(invocation -> {
System.out.println("进入Mock");
return null;
}).when(userRepository).addUser(Mockito.any());
/*模拟方法设置返回期望值*/
List spy = Mockito.spy(new LinkedList<>());
/*这里会抛出IndexOutOfBoundsException*/
// Mockito.when(spy.get(0)).thenReturn("foo");
/*所以要使用下面代码*/
Mockito.doReturn("foo").when(spy).get(0);
}
@Test
public void testUpdateUsernameSuccess() throws Exception {
Long userId = 1L;
String newUsername = "new Jack";
/*测试service方法*/
boolean updated = userService.updateUsername(userId,newUsername);
/*检查结果*/
Assert.assertThat(updated, Matchers.is(true));
/*Mock对象一旦创建,就会自动记录自己的交互行为。通过verify(mock).someMethod()方法,来验证方法是否被调用。*/
/*验证调用上面的service方法后是否 userRepositroy.getOne(1L)调用过。*/
Mockito.verify(userRepository).getOne(userId);
/*updateUsername 函数中我们调用了已经打桩了的其他的函数,现在我们来验证进入其他函数中的参数*/
/*构造参数捕获器,用于捕获方法参数进行验证*/
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
/*验证updateUser方法是否呗调用过,并且捕获入参*/
Mockito.verify(userRepository).updateUser(userCaptor.capture());
/*获取参数updateUser*/
User updateUser = userCaptor.getValue();
/*验证入参是否是预期的*/
Assert.assertThat(updateUser.getUsername(),Matchers.is(newUsername));
/*保证这个测试用例中所有被Mock的对象的相关方法都已经被Verify过了*/
Mockito.verifyNoMoreInteractions(userRepository);
/*如果有一个交互没有被verify,则会报错
org.mockito.exceptions.verification.NoInteractionsWanted:
No interactions wanted here:
-> at com.wuwii.service.IUserServiceTest.testUpdateUsernameSuccess(IUserServiceTest.java:74)
But found this interaction on mock 'iUserRepository':
-> at com.wuwii.service.impl.UserServiceImpl.findOne(UserServiceImpl.java:21)
****/
}
// @Test
public void testUpdateUsernameFailed() throws Exception {
Long userId = 2L;
String newUsername = "new Jack";
/*没有经过mock的updateUser方法,它的返回值是false*/
boolean updated = userService.updateUsername(userId,newUsername);
Assert.assertThat(updated,Matchers.is(true));
/*验证userRepository的getOne(2L)这个方法是否被调用过(这个是被测试过的,此步骤通过)*/
Mockito.verify(userRepository).getOne(2L);
/*验证userRepository的updateUser(null)这个方法是否被调用过(这个方法是没有被调用过的)*/
Mockito.verify(userRepository).updateUser(null);
Mockito.verifyNoMoreInteractions(userRepository);
}
}
创建Mock对象
我们需要对userService进行测试,就需要模拟userRepository对象
我们在setUp()方法中,模拟对象并打桩。
模拟对象有两种方式:
1.对注解@Mock的对象进行模拟MockitoAnnotations.initMocks(this)
2.对单个对象手动Mock: userRepositroy = Mockito.mock(IUserRepositroy.class)
数据打桩
数据打桩,方法非常多,主要分下面几种:
1.最基本的用法就是调用when以及thenReturn方法了。它的作用就是指定当我们调用被代理的对象的某一个方法以及参数的时候,返回什么值。
2.提供参数匹配器,灵活匹配参数。any()、any(Class<T> type) 、anyBoolean()、 anyByte() anyChar()、 anyInt()、 anyLong()等等,它支持复杂的过滤,可以使用正则 Mockito.matches(".*User$"),开头结尾验证 endsWith(String suffix)、 startsWith(String prefix),判空验证 isNotNull()、 isNull() 。也还可以使用argThat(ArgumentMatchermatcher),如:ArgumentMatcher只有一个方法boolean matches(T argument);传入入参,返回一个boolean表示是否匹配。Mockito.argThat(argument -> argument.getUsername.length() > 6)
3.Mockito还提供了了两个表示行为的方法:thenAnswer(Answer<?> answer);、thenCallRealMethod();分别表示自定义处理调用后的行为,以及调用真实的方法。这两个方法在有些测试用例中还是很有用的。
4.对于同一个方法,Mockito可以是顺序与次数关连的。也就是说可以实现同一个方法 ,第一次调用返回一个值,第二次调用返回一个值,甚至第三次调用抛出异常等等。只需要连续的调用thenXXXX即可。
如果为一个返回Void的方法设置桩数据。上面的方法都是表示的是有返回值的方法,而由于一个方法没有返回值,因此我们不能调用when方法,比如:doAnswer(Answer answer)、doNothing()、doReturn(Object toBeReturned)、doThrow(Class<? extends Throwable> toBeThrown)、doCallRealMethod()。它们使用方法其实和上面thenXXXX是一样的.
验证测试方法的结果:
使用断言来检查结果。
验证Mock对象的调用
其实,在这里我们如果只是验证方法结果的正确的话,就非常简单,但是在复杂的方法调用堆栈中,往往可能出现结果正确,但是过程不正确的情况。比如updateUsername方法返回false有两种可能,一直可能是用户没有找到,还有一种可能就是userRepository.updateUser(userPO)返回false。因此如果我们只使用Assert.assertFalse(updated);来验证结果,可能就会忽略某些错误。
因此我们在测试中还需要验证指定的方法userRepository.getOne(userId);是否运行过,而且我们还是用了参数捕获器,抓取中间的方法参数。用来验证。
提供了verify(T mock,VerificationMode mode)方法。VerificationMode有很多作用。
/*验证指定方法 get(3) 没有被调用*/ verify(mock,never()).get(3); |
verifyZeroInteractions和verifyNoMoreInteractions验证所有mock的方法是否都调用过了
PowerMock
为什么使用PowerMock
Mockito 并不是完美的,它不提供对静态方法、构造方法、私有方法以及 Final 方法的模拟支持。而程序员时常都会发现自己有对以上这些方法的模拟需求,特别是当一个已有的软件系统摆在面前时。幸好 , 还有 PowerMock。
powermock的依赖
可以看出来,它有两个重要的依赖:javassist和objenesis。
javassist是一个修改java字节码的工具包,objenesis是一个绕过构造方法来实例化一个对象的工具包。由此看来,PowerMock的本质是通过修改字节码来实现对静态和final等方法的mock的
PowerMock的简单实现原理:
- 当某个测试方法被注解@PrepareForTest标注以后,在运行测试用例时,会创建一个新的org.powermock.core.classloader.MockClassLoader实例,然后加载该测试用例使用到的类(系统类除外)。
- PowerMock会根据你的mock要求,去修改写在注解@PrepareForTest里的class文件(当前测试类会自动加入注解中),以满足特殊的mock需求。例如:去除final方法的final标识,在静态方法的最前面加入自己的虚拟实现等。
- 如果需要mock的是系统类的final方法和静态方法,PowerMock不会直接修改系统类的class文件,而是修改调用系统类的class文件,以满足mock需求。
PowerMock使用
--参考PowerMock实战手册
springboot和powermock整合
1 重要注解
@SpringBootTest // 表明这是一个springboot测试类,会自动加载springboot主启动程序
@RunWith(PowerMockRunner.class) //使用powermock自己的Runner
@PowerMockRunnerDelegate(SpringRunner.class) //将powermock整合到spring容器中
@PowerMockIgnore({"javax.*.*", "com.sun.*", "org.xml.*", "org.apache.*"})@PrepareForTest({HSSFWorkbook.class,HSSFCellStyle.class})public class Demo {
@Test
public void test() throws Exception {
EmployeeService service = PowerMockito.mock(EmployeeService.class);
PowerMockito.when(service.hello()).thenReturn(999);
int result = service.hello();
Assert.assertEquals(999, result);
}
}
下面主要对上面几个注解做相关解释:
@SpringBootTest:表明这是一个springboot测试类,会自动加载springboot主启动程序
@RunWith(PowerMockRunner.class): 使用powermock自己的Runner
@PowerMockRunnerDelegate(SpringRunner.class): 将powermock整合到spring容器中
@PowerMockIgnore({"javax.*.*", "com.sun.", "org.xml.", "org.apache.*"}) : 这个注解很重要,这也是powermock2.0.0与1.x版本重大不一样的地方,因为powermock自带一个类加载器,使用该注解来禁止powermock类加载器加载一些类,避免和JVM类加载器冲突
@PrepareForTest({HSSFWorkbook.class,HSSFCellStyle.class}): 这个注解是告诉PowerMock为我提前准备一个xxx的class,根据我测试预期的行为去准备
至此,springboot和powermock的整合就完成了!
2 PrepareForTest不能随便加
首先来看一段代码:不使用@PrepareForTest
@SpringBootTest
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringRunner.class)
@PowerMockIgnore({"javax.*.*", "com.sun.*", "org.xml.*", "org.apache.*"})
// @PrepareForTest({HSSFWorkbook.class,HSSFCellStyle.class}) 不使用该注解
public class Demo {
@Test
public void test() throws Exception{
HSSFWorkbook wb = new HSSFWorkbook();
wb.createSheet();
}}
程序运行成功!
使用@PrepareForTest
@SpringBootTest@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringRunner.class)
@PowerMockIgnore({"javax.*.*", "com.sun.*", "org.xml.*", "org.apache.*"})
@PrepareForTest({HSSFWorkbook.class})
public class Demo {
@Test
public void test() throws Exception{
HSSFWorkbook wb = new HSSFWorkbook();
wb.createSheet();
}}
我们在test()测试中完全没有用到powermock,但是为什么会失败呢?
原因:@PrepareForTest中的HSSFWorkbook.class,会告诉powermock提前准备这个类文件,那么当程序执行的时候,需要的该类的时候,就会使用到powermock准备的类
到目前为止,读者可能会认为 HSSFWorkbook wb = new HSSFWorkbook();将会创建powermock准备的HSSFWorkbook对象,那么我debug程序,一探究竟
可以看到,这里new HSSFWorkbook()对象完全是一个正常的对象,而非powermock的对象,并且在该类中使用的也是这个真对象
直到运行到MockGateway这个类 才出现问题,在powermock中会有大量的代理类,拦截器,这些类中会使用到pokwermock的HSSFWorkbook的对象,而非真正的HSSFWorkbook对象,因此会出现问题
3 不是所有的类都可以Powermock
一个私有类是完全可以powermock的,那么是不是所有的类都可以powermock吗?
答案是:否定的()
@SpringBootTest@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringRunner.class)
@PowerMockIgnore({"javax.*.*", "com.sun.*", "org.xml.*", "org.apache.*"})
@PrepareForTest({HSSFWorkbook.class})
public class Demo {
@Test
public void test3() throws Exception{
PowerMockito.mock(HSSFCellStyle.class);
}
}
定位至HSSFCellStyle 133行
我们发现这是一个编译期常量,跟ThreadLocal,是没办法跟powermock对象一起创建的
切记:powermock对象是无法改变编译期常量的
4 @InjectMock @Mock区别
@InjectMocks
private SomeHandler someHandler;
@Mock 或者 @Spy
private OneDependency oneDependency; // 此mock将被注入到someHandler
这里的@InjectMocks和@Autowired功能完完全全一样,唯一不同的是,@InjectMocks可以使oneDependency这个Mock对象自动注入到someHandler这个对象中。注意:①@InjectMocks所表示的对象及someHandler是一个普通的对象 ②Mock所表示的对象及oneDependency是一个Mock对象
5 @Mock和@MockBean的区别
@MockBean 会被装配到相关的类中 代替@Autowired
@Mock 不会被装配到相关的类中 无法代替@Autowired
6 Mock方法中的嵌套方法
Mockito.when(alarmRulesDao.changeAlarmLevel(Mockito.anyInt(),Mockito.anyInt()))
.thenReturn(-1);
Integer changeNumber = alarmRulesDao.changeAlarmLevel(changeAlarmlevelRequest.getId(), changeAlarmlevelRequest.getAlarmLevel());
即使Mock了changeAlarmLevel方法,其中的
changeAlarmlevelRequest.getId()
changeAlarmlevelRequest.getAlarmLevel()
还是会正常执行的
7 mock对象中的参数不要再做运算
this.getHSSFWorkbook(downloadVO.getSheetName(), downloadList));
mock的时候不能
Mockito.anyString(),Mockito.anyList()
而要
Mockito.any(),Mockito.anyList() 因为mock对象中的参数执行了相关运算