程序员自测是很重要的一个环节,我认同测试驱动开发的理念,经过一段时间的测试代码的编写,发现测试代码需要保证几点,1.测试代码可重复跑,不能跑过一次,改了数据库数据就不能跑了。2.测试代码写好后,尽可能保持不变,哪怕代码变后,直接跑测试就能验证修改是否正确,而不是把测试代码,测试数据再改一遍。service层测试要与数据库解耦,不能因为数据库数据的变化影响测试,我曾经使用int.sql去对数据库做int操作来保证测试的进行,但是实践过程中会渐渐由于数据表结构更新导致int.sql维护不善,使得每跑一次测试都要修改int.sql。对于十分麻烦的工作,我一般的是不想继续做的,我的想法是无论代码,数据库怎么动,测试代码都是不用怎么改动的,直接跑就可以了,这样也方便项目重构。目前已经达到我对测试的预期了,故而总结现有技术和实现。。如果有更好的建议,也欢迎提出。
service测试使用mockito,它可以保证service的获得的业务数据是预期的数据,得到的结果一定是预期的结果。
mockito的作用是动态代理调用替换mapper层,改用自己配置的数据。这样就可以直接做service单层的单元测试。不会出现由于数据库的数据变了,导致单元测试跑不过的情况,直接执行就可以了。
建议做法,创建一个mock数据类,存放所有测试数据 - mock所有的service或者mapper方法,让其返回设定的数据,所有的测试类继承它。
public class MockData {
public static final LockerInfoService lockerInfoService = mock(LockerInfoService.class);
public static final MedicineHistoryService medicineHistoryService = mock(MedicineHistoryService.class);
public static final ParcelService parcelService = mock(ParcelService.class);
public static final ParcelLogService parcelLogService = mock(ParcelLogService.class);
static {
when(lockerInfoService.findByLockerCode("006000001")).thenReturn(createLockerInfo001());
when(lockerInfoService.findByLockerCode("006000002")).thenReturn(createLockerInfo002());
when(medicineHistoryService.findByMHIdInFiveMinute("exist")).thenReturn(createExistMedicineHistory());
when(medicineHistoryService.findByMHIdInFiveMinute("not_exist")).thenReturn(createNotExistMedicineHistory());
when(parcelService.findByParcelCode("1")).thenReturn(createDropOffProcessingParcel());
when(parcelService.findByParcelCode("2")).thenReturn(createAlreadyDropOffParcel());
when(parcelService.findByParcelCode("5")).thenReturn(createAlreadyPickedUpParcel());
when(parcelService.findByPickupCode("006000001","222222")).thenReturn(createAlreadyDropOffParcel());
when(parcelService.findInLockerParcel("006000001", null)).thenReturn(Arrays.asList(createAlreadyDropOffParcel()));
when(parcelService.getEffectiveCodes("006000001")).thenReturn(Arrays.asList(createAlreadyDropOffParcel().getPickupCode()));
when(parcelService.getEffectiveParcels("006000001")).thenReturn(Arrays.asList(createAlreadyDropOffParcel()));
when(parcelService.selectByParcelCode("006000001","1")).thenReturn(createDropOffProcessingParcel());
when(parcelService.selectByParcelCode("006000001","2")).thenReturn(createAlreadyDropOffParcel());
when(parcelService.selectByParcelCode("006000001","5")).thenReturn(createAlreadyPickedUpParcel());
}
public static LockerInfo createLockerInfo001() {
LockerInfo lockerInfo = new LockerInfo();
lockerInfo.setLockerCode("006000001");
lockerInfo.setAddressDetail("深圳北大医院");
lockerInfo.setSecret("$2a$04$N5bj6tbPthUtnbfLQ2hIRuU5KGrMXkdYpy5mZP66EDkchTyjk.9D.");
return lockerInfo;
}
public static LockerInfo createLockerInfo002() {
LockerInfo lockerInfo = new LockerInfo();
lockerInfo.setLockerCode("006000002");
lockerInfo.setAddressDetail("南山医院");
lockerInfo.setSecret("$2a$04$N5bj6tbPthUtnbfLQ2hIRuU5KGrMXkdYpy5mZP66EDkchTyjk.9D.");
return lockerInfo;
}
// 存在的病历数据
public static MedicineHistory createExistMedicineHistory() {
MedicineHistory medicineHistory = new MedicineHistory();
medicineHistory.setMedicineHistoryId("01");
medicineHistory.setHospitalCode("001");
medicineHistory.setHospitalName("深圳北大医院");
medicineHistory.setPickupMobile("13456450931");
medicineHistory.setDataStatus(DataStatusEnum.EXIST.getValue());
return medicineHistory;
}
// 不存在的病历数据
public static MedicineHistory createNotExistMedicineHistory() {
MedicineHistory medicineHistory = new MedicineHistory();
medicineHistory.setMedicineHistoryId("02");
medicineHistory.setHospitalCode("002");
medicineHistory.setHospitalName("南山医院");
medicineHistory.setDataStatus(DataStatusEnum.NO_EXIST.getValue());
return medicineHistory;
}
// 未投递包裹
public static Parcel createDropOffProcessingParcel() {
Parcel parcel = new Parcel();
parcel.setParcelCode("1");
parcel.setParcelStatus(ParcelStatusEnum.DROP_OFF_PROCESSING.getValue());
parcel.setMedicineHistoryId("exist");
parcel.setPickupMobile("18676660540");
return parcel;
}
// 已投递包裹
public static Parcel createAlreadyDropOffParcel() {
Parcel parcel = new Parcel();
parcel.setParcelCode("2");
parcel.setParcelStatus(ParcelStatusEnum.ALREADY_DROP_OFF.getValue());
parcel.setMedicineHistoryId("exist");
parcel.setPickupMobile("18676660540");
parcel.setDropoffUser("医院工作人员");
parcel.setPickupCode("332211");
parcel.setLockerCode(createLockerInfo001().getLockerCode());
return parcel;
}
// 已取出包裹
public static Parcel createAlreadyPickedUpParcel() {
Parcel parcel = new Parcel();
parcel.setParcelCode("5");
parcel.setParcelStatus(ParcelStatusEnum.ALREADY_PICKED_UP.getValue());
parcel.setMedicineHistoryId("exist");
parcel.setPickupMobile("18676660540");
parcel.setDropoffUser("医院工作人员");
parcel.setPickupCode("患者");
parcel.setPickupType(PickupTypeEnum.PATIENT.getValue());
return parcel;
}
}
mock数据建议使用方法的形式,比如一个取件人的数据,就写一个User getPickupUser()
的方法,调用方法参数是mock数据,返回的结果也是用改数据去assert
。不用static bean
,在static{}
赋值的原因是赋值的数据放在一起,看起来恶心;使用get()
可以直接去改对象的构造,看看对象属性值,一目了然。
static FileParcel getParcel() {
FileParcel parcel = new FileParcel();
parcel.setId(2);
parcel.setFileId(1);
parcel.setDropoffUser("韩雷");
parcel.setDropoffUserNo("FC001");
parcel.setPickupUser("李梅梅");
parcel.setPickupUserNo("FC002");
parcel.setReceiveStatus(ReceiveStatusEnum.NOTPICKUP.getValue());
parcel.setParcelStatus(ParcelStatusEnum.UN_DROP_OFF.getValue().byteValue());
return parcel;
}
@Test
public void parcelInfo() {
mobileService.setUserRepository(userRepository);
mobileService.setParcelRepository(parcelRepository);
mobileService.setFileInfoRepository(fileRepository);
MobileParcelVO parcelVO = mobileService.parcelInfo(getParcel().getId());
assert parcelVO !=null;
assert parcelVO.getId() == getParcel().getId().intValue();
assert parcelVO.getCanDelete().byteValue() == CanDeleteEnum.ALLOW_DELETE.getValue();
}
service的注入类写法要写成set的形式,让mock对象能注入到service对象中。
private ParcelService parcelService;
@Autowired
public void setParcelService(ParcelService parcelService) {
this.parcelService = parcelService;
}
在测试类中,通过new的形式构造service类对象,将mock的service对象set到service对象中。传入特定的参数,使用assert判断返回值是否符合预期;通过verify判断方法是否执行某些函数,参数是否符合预期(用于save,delete操作)。
public static LockerServiceClientService lockerServiceClientService = new LockerServiceClientService();
static {
lockerServiceClientService.setLockerInfoService(lockerInfoService);
lockerServiceClientService.setMedicineHistoryService(medicineHistoryService);
lockerServiceClientService.setParcelService(parcelService);
lockerServiceClientService.setParcelLogService(parcelLogService);
}
@Test
public void medicineHistoryInfo() {
// 验证成功
MedicineHistoryInfoVO exist = lockerServiceClientService.medicineHistoryInfo("exist");
assert DataStatusEnum.EXIST.getValue().equals(exist.getDataStatus());
// 验证不存在-数据源不存在
try {
lockerServiceClientService.medicineHistoryInfo("not_exist");
} catch (LockerWebException e) {
assert e.getCode() == ResponseCode.MEDICINE_HISTORY_NOT_EXIST;
}
// 验证没有查到数据 - 没有拿到数据源结果
MedicineHistoryInfoVO no_exist = lockerServiceClientService.medicineHistoryInfo("随意的key");
assert no_exist == MedicineHistoryInfoVO.NO_EXIST_VO;
}
@Test
public void dropoffSuccessBiz() {
ParcelDTO parcel = new ParcelDTO();
Parcel dropingParcel = createDropOffProcessingParcel();
parcel.setQueryParcel(dropingParcel);
Parcel updateParcel = new Parcel();
BeanUtils.copyProperties(parcel, updateParcel);
updateParcel.setDropoffUser("医院工作人员");
updateParcel.setParcelStatus(ParcelStatusEnum.ALREADY_DROP_OFF.getValue().byteValue());
updateParcel.setDropoffIsClose(IsCloseEnum.YES.getValue());
updateParcel.setQueryParcel(null);
AsyncService asyncService = mock(AsyncService.class);
lockerServiceClientService.setAsyncService(asyncService);
lockerServiceClientService.dropoffSuccessBiz(parcel);
verify(parcelService, times(1)).save(updateParcel);
verify(asyncService, times(1)).sendSmsPickupCode(dropingParcel.getPickupCode(),
dropingParcel.getLockerAddress(), dropingParcel.getPickupMobile(), dropingParcel.getMedicineHistoryId());
}
public class LockerServiceClientServiceTest extends MockData {
Logger logger = LoggerFactory.getLogger(LockerServiceClientServiceTest.class);
public static LockerServiceClientService lockerServiceClientService = new LockerServiceClientService();
static {
lockerServiceClientService.setLockerInfoService(lockerInfoService);
lockerServiceClientService.setMedicineHistoryService(medicineHistoryService);
lockerServiceClientService.setParcelService(parcelService);
lockerServiceClientService.setParcelLogService(parcelLogService);
}
@Test
public void lockerSecret() {
LockerInfo lockerInfo = createLockerInfo001();
lockerServiceClientService.lockerSecret(lockerInfo.getLockerCode(), "123456");
}
@Test
public void medicineHistoryInfo() {
// 验证成功
MedicineHistoryInfoVO exist = lockerServiceClientService.medicineHistoryInfo("exist");
assert DataStatusEnum.EXIST.getValue().equals(exist.getDataStatus());
// 验证不存在-数据源不存在
try {
lockerServiceClientService.medicineHistoryInfo("not_exist");
} catch (LockerWebException e) {
assert e.getCode() == ResponseCode.MEDICINE_HISTORY_NOT_EXIST;
}
// 验证没有查到数据 - 没有拿到数据源结果
MedicineHistoryInfoVO no_exist = lockerServiceClientService.medicineHistoryInfo("随意的key");
assert no_exist == MedicineHistoryInfoVO.NO_EXIST_VO;
}
@Test
public void dropoffSuccessBiz() {
ParcelDTO parcel = new ParcelDTO();
Parcel dropingParcel = createDropOffProcessingParcel();
parcel.setQueryParcel(dropingParcel);
Parcel updateParcel = new Parcel();
BeanUtils.copyProperties(parcel, updateParcel);
updateParcel.setDropoffUser("医院工作人员");
updateParcel.setParcelStatus(ParcelStatusEnum.ALREADY_DROP_OFF.getValue().byteValue());
updateParcel.setDropoffIsClose(IsCloseEnum.YES.getValue());
updateParcel.setQueryParcel(null);
AsyncService asyncService = mock(AsyncService.class);
lockerServiceClientService.setAsyncService(asyncService);
lockerServiceClientService.dropoffSuccessBiz(parcel);
verify(parcelService, times(1)).save(updateParcel);
verify(asyncService, times(1)).sendSmsPickupCode(dropingParcel.getPickupCode(),
dropingParcel.getLockerAddress(), dropingParcel.getPickupMobile(), dropingParcel.getMedicineHistoryId());
}
}
对于dao
层的测试,主要是为了测试sql语句是否正确执行。我的测试方式是了解开发数据库,做dao的测试,只测试sql是否成功执行。选择spring-test
,导入spring
的IOC容器,测试。对于insert
,update
,delete
操作,建议开启事务回滚,不污染数据库数据,方便二次测试。使用spring-boot-test
,只要在测试类上加上回滚注解@Transactional
,方法执行完后,默认回滚,而spring-test
需要在测试方法上加上回滚注解Rollback
使用spring-boot-test
导入IOC的注解,并开启回滚
@RunWith(SpringRunner.class)
@SpringBootTest
@WebAppConfiguration
@Transactional
public class SpringBootEnvironmentTest {
}
使用spring-test
导入IOC的注解
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:applicationContext.xml"})
@Transactional(transactionManager="transactionManager")
public class BaseTest {
@Test
@Rollback
public void insertAndSelect() {
}
}
controller层主要做参数效验这类工作,基本不会有业务逻辑,单元测试在我看来不是那么重要,可以集成测试中联合service层,dao层一起测试。
对于controller层的测试的方式,我一般是启动项目,使用postman发送http请求或自己写httpclient发送http请求做测试。这个类似工具,技术很多,我就不介绍了。