目录
- 一、项目介绍
- 二、业务代码
- 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();
@Override
public 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 格式错误");
}
// 获取手机号归属地编号和运营商编号 然后通过编号找到区域内是 SalesRep
String 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的使用》
觉得有用的话还请来个三连!!!