单元测试--Java
使用mockito+powermock进行java单元测试
实例
如下一个正常业务代码,接下来就对HelloController.say方法进行单元测试用例代码开发。
业务代码
public class HelloController{
@Autowired
private HelloService helloService;
@Autowired
private UserService userService;
@GetMapping("/say")
public String say(Integer id){
HelloUser user=userService.findById(id);
if(user==null){
throw new RuntimeException("user not found");
}
return helloService.doHello(user);
}
}
一个正常用例
在HelloController.say()个方法体内,引用了另外两个Bean,userService和helloService。
正常服务启动,我们是必须要有这两个Bean的实例。但是在测试用例代码中其实我们是没必要知道UserService和和HelloService的具体实现的,所以这两个Bean我们可以通过mock的方式虚拟出来。
@RunWith(MockitoJUnitRunner.class)
public class TestHelloController{
@InjectMock
private HelloController helloController;
@Mock
private HelloService helloService;
@Mock
private UserService userService;
/**
* 正常用例
**/
@Test
public void testSay(){
Integer id = 1;
HelloUser user = new HlloUser();
//模拟方法调用的传参和返回值
Mockito.when(userService.findById(id)).thenReturn(user);
Mockito.when(helloService.doHello(user)).thenReturn("say hi!");
String resp=helloController.say(id);
//验证方法调用次数
Mockito.verfiy(userService,times(1)).findById(id);
Mockito.verfiy(helloService,times(1)).doHello(user);
//验证结果
Assert.assertTrue(resp.equals("say hi!"));
}
/**
* 异常用例
**/
@Test(expected=RuntimeException.class)
public void testSayException(){
//这里模拟userService调用findById无论传入什么参数,返回都是null
Mockito.when(userService.findById(Mockito.anyString())).thenReturn(null);
Integer id = 1;
helloController.say(id);
}
}
静态方法模拟
一些方法中有时候需要调用静态方法(如HelloController.create方法),但是静态方法内部,也不是该用例需要关心的,这个使用可以使用powermock对静态方法进行mock。
//业务代码
public class HelloController{
//...省略部分代码
@GetMapping("/create")
public String create(HelloUser user){
user.setNo(BusinessNoUtils.generateNo());
userService.save(user);
return user.getNo();
}
}
//用例代码--因为这里使用了Powermock,所以这里运行器一定是要使用PowerMockRunner.class
@Runwith(PowerMockRunner.class)
@PrepareForTest(BusinessNoUtils.class)
public class TestHelloController(){
//...省略部分代码
@Test
public void testCreate(){
HelloUser user= new HelloUser();
//需要声明,模拟静态方法
PowerMockito.mockStatic(BusinessNoUtils.class);
Mockito.when(BusinessNoUtils.generateNo()).thenReturn("111111");
Mockito.when(userService.save(Mockito.argThat(e->{
//简单判断传入参数是否正确
if(e.getNo().equals("111111")){
return true;
}
return false;
})));
String resp=helloController.create(user);
Assert.assertTrue(resp.equals("111111"));
}
}
new对象模拟
有一些业务代码中需要创建一个对象,这个对象中有些操作也没必要在业务代码中体现,而且这个对象可能会依赖其他东西。对于这种也可以通过Powermock的方式进行模拟。
// 业务代码
public HelloController{
//...省略其他代码
public String copyUser(String id){
HelloUser helloUser=userService.findById(id);
HelloUser destUser = new HelloUser();
String userNo=destUser.copyFrom(helloUser);
return userNo;
}
}
//用例代码
@Runwith(PowerMockRunner.class)
//这里的PrepareForTest引用的类是要 new 对象所在的类,这里要注意跟模拟静态方法不一样的地方
@PrepareForTest(HelloController.class)
public TestHelloController{
//...省略其他代码
@Test
public void testCopyUser(){
HelloUser rawUser = new HelloUser();
rawUser.setName("123");
Mockito.when(userService.findById(1)).thenReturn(rawUser);
HelloUser destUser = Mockito.mock(HelloUser.class);//模拟对象
//需要声明new对象返回值
PowerMockito.whenNew(HelloUser.class).withNoArguments().thenReturn(destUser);
//模拟copyFrom方法
Mockito.when(destUser.copyFrom(rawUser)).thenReturn("123");
String taskNo=controller.copyUser(1);
Mockito.verfiy(taskNo.equals("123"));
}
}
@Spy的使用
上面的代码是因为要展示用例,所以很多业务逻辑是写在Controller层的。但实际业务开发中,Controller层只是薄薄的一层,没有什么业务逻辑,但Controller层又是整个用例的入口。这个时候,我们不希望UserService在该用例中被模拟,而是希望这个用例下来,整条链路上我们自己写的代码都能覆盖到。这个时候我们就需要@Spy注解了。
//业务代码
public class HelloContrller{
@Autowired
UserService userService;
@GetMapping("/delete/{id}")
public boolean deleteUser(@PathVariable("id")String id){
return userService.deleteById(id);
}
}
public class UserService{
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
public boolean deleteById(String id){
HelloUser userInfo=Optional.ofNullable(userMapper.findById(id)).orElseThrow(()->new RuntionException("not found"));
//空方法
userMapper.deleteByNo(userInfo.getNo);
roleService.deleteByUserId(id);
return true;
}
}
public class RoleService{
@Autowired
private RoleMapper roleMapper;
public boolean deleteByUserId(String userId){
return roleMapper.deleteByUserId(userId);
}
}
//用例代码
@RunWith(MockitoJUnitRunner.class)
public class TestHelloController{
@InjectMocks HelloController helloController;
@Spy UserService userService ;
@Spy RoleService roleService ;
//dao层继续模拟
@Mock RoleMapper roleMapper;
@Mock UserMapper userMapper;
@Before
public void setUp(){
//由于没有自动注入,所以手动set进去
WhiteBox.setInternalState(userService,"userMapper",userMapper);
WhiteBox.setInternalState(userService,"roleService",roleService);
WhiteBox.setInternalState(roleService,"roleMapper",roleMapper);
}
public void testDelete(){
String userId = "1";
UserInfo userInfo = new UserInfo();
userInfo.setNo("123");
Mockito.when(userMapper.findById(userId)).thenReturn(userInfo);
//对于空方法调用处理
Mockito.doNothing(userMapper).when(deleteByNo("123"));
Mockito.when(roleMapper.deleteByUserId(uesrId)).thenReturn(true);
Assert.assertTrue(helloController.deleteUser(userId));
}
}
注解作用
- @RunWith(MockitoJUnitRunner.class) :@Runwith是一个运行器,@RunWith(MockitoJUnitRunner.class),表示使用MockitoJUnitRunner来运行。
- @Mock: 声明的对象,对函数调用均执行mock,不执行真正部分
- @Spy :声明的对象,对函数调用均执行真正部分
- @InjectMocks: 自动讲模拟对象或侦查域注入到被测试对象中。
- @PrepareForTest:表示声明哪些类需要被修改
总结
写好的单元测试用例要比实际写代码的工作量要多,这部分工作量没有太多技术性的东西(从另外的角度来看,可以对你的代码有更深入的理解),但确是比较重要的一部分。
如果项目是一锤子买卖,没有后续迭代,你可以不需要写测试用例,否则都需要写测试用例。
当需要进行代码重构的时候,这些测试用例会给你很大帮助。
当你改了一段祖传代码,如果有测试用例,会给你提供不少思路上的帮助。当然改完代码之后,记得同时修改测试用例。
网友评论