目录
- 一、项目介绍
- 二、业务代码
- 2.1 导入依赖
- 2.2 entity
- 2.3 Dao
- 2.4 业务代码
- 三、单元测试
- 3.1 生成Test方法
- 3.2 引入测试类
- 3. 3 测试前准备
- 3.4 测试
- 3.4.1 name和phone参数校验
- 3.4.2 测试数据库访问
- 3.4.3 数据库反例
- 总结
前面我们提到了《【单元测试】一文读懂java单元测试》
简单介绍了《【单元测试】单元测试之Mockito的使用》
今天一起来看看项目中mockito怎么用的
项目源码GitHub
一、项目介绍
这个案例比较简单,模拟了一个数据统计系统,由地推员输入客户的姓名和手机号,系统根据客户的手机号和归属地和所属的运营商将客户群体分组,分配给相应的销售组,最后构建用户对象存入数据表。
二、业务代码
2.1 导入依赖
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.8.2</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.mockito</groupId>-->
<!-- <artifactId>mockito-core</artifactId>-->
<!-- <version>4.3.1</version>-->
<!-- </dependency>-->
<dependency><groupId>org.mockito</groupId><artifactId>mockito-inline</artifactId><version>4.3.1</version><scope>test</scope>
</dependency>
注意:mockito-inline
和mockito-core
不能同时打开
2.2 entity
PhoneNumber :
public class PhoneNumber {private final String number;private final String pattern = "^0?[1-9]{2,3}-?\\d{8}$";public String getNumber() {return number;}//在含参构造器中进行参数校验public PhoneNumber (String number) throws ValidationException {if (number == null) {throw new ValidationException("number 不能为空");} else if (isValid(number)) {throw new ValidationException("number 格式错误");}this.number = number;}private boolean isValid(String number) {return number.matches(pattern);}private static String getAreaCode(String number) {//具体实现逻辑return "a";}private static String getOperatorCode(String number) {//具体实现逻辑return "b";}
}
SalesRep:
public class SalesRep {private String repId;public SalesRep(String repId) {this.repId = repId;}public String getRepId() {return repId;}public void setRepId(String repId) {this.repId = repId;}
}
User:
public class User {private String name;private String phone;private String repId;public User() {}public User(String name, String phone, String repId) {this.name = name;this.phone = phone;this.repId = repId;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getPhone() {return phone;}public void setPhone(String phone) {this.phone = phone;}public String getRepId() {return repId;}public void setRepId(String repId) {this.repId = repId;}
}
2.3 Dao
UserDao :
public class UserDao {public User save(String name, String phone, String repId) throws SQLException {return new User(name, phone, repId);}
}
SalesDao:
public class SalesDao {public SalesRep findRep(String areaCode, String operatorCode) {if ("a".equals(areaCode) && "b".equals(operatorCode)) {return new SalesRep("Echo");}return null;}
}
2.4 业务代码
RegistrationServiceImpl:
public class RegistrationServiceImpl implements RegistrationService {SalesDao salesDao = new SalesDao();UserDao userDao = new UserDao();@Overridepublic User register(String name, String phone) throws Exception {// 参数校验if (name == null || name.length() == 0) {throw new ValidationException("number 不能为空");}if (phone == null || !isValid(phone)) {throw new ValidationException("phone 格式错误");}// 获取手机号归属地编号和运营商编号 然后通过编号找到区域内是 SalesRepString areaCode = FindUtils.getAreaCode(phone);String operatorCode = FindUtils.getOperatorCode(phone);User user;try {SalesRep rep = salesDao.findRep(areaCode, operatorCode);// 最后创建用户,落盘,然后返回user = userDao.save(name, phone, rep.getRepId());} catch (SQLException e) {throw new DAOException("SQLException thrown " + e.getMessage());}return user;}private boolean isValid(String phone) {String pattern = "^(13[0-9]|14[5|7]|15[0|1|2|3|4|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\\d{8}$";boolean flag = phone.matches(pattern);return flag;}}
其他代码见GitHub
三、单元测试
3.1 生成Test方法
1)点击我们需要生成测试的类,右击鼠标Generate->Test
2)选择对应的Junit版本和测试方法
3)这样在test目录下生成测试类
3.2 引入测试类
使用@Spy注解引入RegistrationServiceImpl这个待测试的类
这里有两个数据库操作salesDao.findRep和userDao.save,现在模拟salesDao正例操作,userDao反例操作,抛出异常
首先对userDao进行mock,模拟出对象,进行打桩
@InjectMocks注解的作用是将Mock出来的UserDao对象注入到RegistrationServiceImpl类当中
3. 3 测试前准备
引入@BeforeEach前置处理器,开启@Spy和@Mock
@BeforeEach
void setUp(){MockitoAnnotations.openMocks(this);
}
3.4 测试
3.4.1 name和phone参数校验
如果是空就或者0抛出异常
@Test
void register() throws Exception {String name = null;String phone = "15012345678";try {registrationService.register(name, phone);Assertions.fail("这里说明程序挂了");// 如果执行代码能快速定位} catch (Exception e) {Assertions.assertTrue(e instanceof ValidationException);}
}
这里name为null,会抛出异常,catch会捕捉到异常,通过 Assertions.assertTrue去接收异常,Assertions.fail(“这里说明程序挂了”);用于定位异常;
鼠标右击第三个,以单元测试覆盖路运行
运行结果,单元测试行覆盖率只有27%,我们只测试了26、27行,绿色代表我们覆盖了的地方,红色代表我们没有覆盖的地方。
接下来,我们对phone这个参数进行测试,
@Test
void register() throws Exception {String name = null;String phone = "15012345678";try {registrationService.register(name, phone);Assertions.fail("这里说明程序挂了");// 如果执行代码能快速定位} catch (Exception e) {Assertions.assertTrue(e instanceof ValidationException);}name ="Hanson";phone = null;try {registrationService.register(name, phone);Assertions.fail("这里说明程序挂了");// 如果执行代码能快速定位} catch (Exception e) {Assertions.assertTrue(e instanceof ValidationException);}
}
测试:
单元测试行覆盖率变成了38%,
3.4.2 测试数据库访问
@Test
void register() throws Exception {String name = null;String phone = "15012345678";try {registrationService.register(name, phone);Assertions.fail("这里说明程序挂了");// 如果执行代码能快速定位} catch (Exception e) {Assertions.assertTrue(e instanceof ValidationException);}name ="Hanson";phone = null;try {registrationService.register(name, phone);Assertions.fail("这里说明程序挂了");// 如果执行代码能快速定位} catch (Exception e) {Assertions.assertTrue(e instanceof ValidationException);}phone = "15012345678";MockedStatic<FindUtils> staticService = Mockito.mockStatic(FindUtils.class);staticService.when(() -> FindUtils.getAreaCode("15012345678")).thenReturn("a");// 可以返回具体的操作staticService.when(() -> FindUtils.getOperatorCode("15012345678")).thenReturn("b");// 可以返回具体的操作// 数据库操作正常Mockito.when(salesDao.findRep("a","b")).thenCallRealMethod();Mockito.when(userDao.save(name,phone,"Hanson")).thenCallRealMethod();User user = registrationService.register(name, phone);Assertions.assertEquals("Hanson",user.getRepId());
}
测试结果:
单元测试行覆盖率从38%变成了88%,为什么不是100%呢?那是因为这些都是正例,所有catcah没有捕获到异常
3.4.3 数据库反例
我们通过thenThrow()方法返回SQLException模拟异常,通过catch捕捉异常进行断言
@Test
void register() throws Exception {String name = null;String phone = "15012345678";try {registrationService.register(name, phone);Assertions.fail("这里说明程序挂了");// 如果执行代码能快速定位} catch (Exception e) {Assertions.assertTrue(e instanceof ValidationException);}name ="Hanson";phone = null;try {registrationService.register(name, phone);Assertions.fail("这里说明程序挂了");// 如果执行代码能快速定位} catch (Exception e) {Assertions.assertTrue(e instanceof ValidationException);}phone = "15012345678";MockedStatic<FindUtils> staticService = Mockito.mockStatic(FindUtils.class);staticService.when(() -> FindUtils.getAreaCode("15012345678")).thenReturn("a");// 可以返回具体的操作staticService.when(() -> FindUtils.getOperatorCode("15012345678")).thenReturn("b");// 可以返回具体的操作// 数据库操作正常Mockito.when(salesDao.findRep("a","b")).thenCallRealMethod();Mockito.when(userDao.save(name,phone,"Hanson")).thenCallRealMethod();User user = registrationService.register(name, phone);Assertions.assertEquals("Hanson",user.getRepId());// 数据库操作异常Mockito.when(userDao.save(name,phone,"Hanson")).thenThrow(new SQLException());try {registrationService.register(name,phone);} catch (Exception e) {Assertions.assertTrue(e instanceof DAOException);}
测试:
单元测试方法覆盖率和行覆盖率都是100%,完毕
总结
首先,确定测试类,对测试类进行有引入,其次确定是否有其他对象的注入,例如demo中的salesDao和userDao两个对象,还有可能是一些Service,我们需要用@Spy
和@Mock
两个注解将对象注入进来,并在测试类对象是添加@InjectMocks
将对象注入到测试类对象中;
其次我们要开启mock和spy方法(使用前置处理器,MockitoAnnotations.openMocks(this);
)
测试方法正反例写,静态方法使用Mockito.mockStatic
方法将静态方法所属的类mock出来,在对这个方法进行打桩;
对于try-catch,对结果进行分类,以异常捕获和未捕获进行校验;
Jnuit文章见《【单元测试】一文读懂java单元测试》
单元测试之Mockito见文章《【单元测试】单元测试之Mockito的使用》
觉得有用的话还请来个三连!!!