【java测试】Junit、Mock+代码覆盖率
原文见此处
单元测试是编写测试代码,用来检测特定的、明确的、细颗粒的功能。单元测试并不一定保证程序功能是正确的,更不保证整体业务是准备的。
单元测试不仅仅用来保证当前代码的正确性,更重要的是用来保证代码修复、改进或重构之后的正确性。
一般来说,单元测试任务包括
- 接口功能测试:用来保证接口功能的正确性。
- 局部数据结构测试(不常用):用来保证接口中的数据结构是正确的
- 比如变量有无初始值
- 变量是否溢出
- 边界条件测试
- 变量没有赋值(即为NULL)
- 变量是数值(或字符)
- 主要边界:最小值,最大值,无穷大(对于DOUBLE等)
- 溢出边界(期望异常或拒绝服务):最小值-1,最大值+1
- 临近边界:最小值+1,最大值-1
- 变量是字符串
- 引用“字符变量”的边界
- 空字符串
- 对字符串长度应用“数值变量”的边界
- 变量是集合
- 空集合
- 对集合的大小应用“数值变量”的边界
- 调整次序:升序、降序
- 变量有规律
- 比如对于Math.sqrt,给出n^2-1,和n^2+1的边界
- 所有独立执行通路测试:保证每一条代码,每个分支都经过测试
- 代码覆盖率
- 语句覆盖:保证每一个语句都执行到了
- 判定覆盖(分支覆盖):保证每一个分支都执行到
- 条件覆盖:保证每一个条件都覆盖到true和false(即if、while中的条件语句)
- 路径覆盖:保证每一个路径都覆盖到
- 相关软件
- Cobertura:语句覆盖
- Emma: Eclipse插件Eclemma
- 代码覆盖率
- 各条错误处理通路测试:保证每一个异常都经过测试
JUNIT
JUnit是Java单元测试框架,已经在Eclipse中默认安装。目前主流的有JUnit3和JUnit4。JUnit3中,测试用例需要继承TestCase
类。JUnit4中,测试用例无需继承TestCase
类,只需要使用@Test
等注解。
Junit3
先看一个Junit3的样例
1 package com.ishansong.efficiency.avatar.api.service.impl; 2 3 import com.google.common.collect.Lists; 4 import com.google.common.collect.Maps; 5 import com.ishansong.efficiency.avatar.api.configuration.apollo.ApolloConfigBusiness; 6 import com.ishansong.efficiency.avatar.api.dao.entity.generated.*; 7 import com.ishansong.efficiency.avatar.api.dao.mapper.generated.ShowWholeEqpResultEntityMapper; 8 import com.ishansong.efficiency.avatar.api.enums.StatisticsLvEnum; 9 import com.ishansong.efficiency.avatar.api.manager.*; 10 import com.ishansong.efficiency.avatar.api.service.DataMarketDBSyncService; 11 import com.ishansong.efficiency.avatar.api.service.DataMarketService; 12 import com.ishansong.efficiency.avatar.api.utils.DateUtils; 13 import com.ishansong.efficiency.avatar.api.utils.DingMsgUtils; 14 import com.ishansong.efficiency.avatar.api.utils.YearMonth; 15 import com.ishansong.efficiency.avatar.api.view.model.IndexModelData; 16 import com.ishansong.efficiency.avatar.api.view.model.MonthlyIndexModel; 17 import lombok.extern.slf4j.Slf4j; 18 import org.springframework.beans.factory.annotation.Autowired; 19 import org.springframework.stereotype.Service; 20 import org.springframework.util.CollectionUtils; 21 import org.springframework.util.StringUtils; 22 import tk.mybatis.mapper.entity.Example; 23 import tk.mybatis.mapper.weekend.WeekendSqls; 24 25 import java.time.LocalDate; 26 import java.util.*; 27 import java.util.stream.Collectors; 28 29 import static java.util.stream.Collectors.toList; 30 31 @Slf4j 32 @Service 33 public class DataMarketDBSyncServiceImpl implements DataMarketDBSyncService { 34 @Autowired 35 private DataMarketDBSyncManager dataMarketDBSyncManager; 36 @Autowired 37 private ShowWholeEqpResultEntityMapper showWholeEqpResultEntityMapper; 38 @Autowired 39 private StoryTeamUserWeekManager storyTeamUserWeekManager; 40 @Autowired 41 private StoryTeamUserManager storyTeamUserManager; 42 @Autowired 43 private StoryTeamUserTestManager storyTeamUserTestManager; 44 @Autowired 45 private DataMarketService dataMarketService; 46 @Autowired 47 private OrganizationManager organizationManager; 48 @Autowired 49 private OrganizationTestManager organizationTestManager; 50 @Autowired 51 private StoryManager storyManager; 52 @Autowired 53 private StoryTestManager storyTestManager; 54 @Autowired 55 private ApolloConfigBusiness apolloConfigBusiness; 56 57 @Override 58 public List<MonthlyIndexModel> queryMonthsIndexModelList(Integer years, Integer maxMonth, int count, String metricCode) { 59 List<ShowWholeEqpResultEntity> list = dataMarketDBSyncManager.queryByMaxMonth(years, maxMonth, count, metricCode); 60 return this.transferShowEntity2IndexModel(list); 61 } 62 63 @Override 64 public List<MonthlyIndexModel> queryWeeksIndexModelList(String maxTitleSort, int count, String metricCode) { 65 List<ShowWholeEqpResultEntity> list = dataMarketDBSyncManager.queryByMaxWeeks(maxTitleSort, count, metricCode); 66 return this.transferShowEntity2IndexModel(list); 67 } 68 69 @Override 70 public ShowWholeEqpResultEntity addOne(ShowWholeEqpResultEntity entity) { 71 entity.setCtime(new Date().getTime()); 72 List<ShowWholeEqpResultEntity> saveList = new ArrayList<>(1); 73 saveList.add(entity); 74 showWholeEqpResultEntityMapper.insertBatch(saveList); 75 return null; 76 } 77 78 @Override 79 public List<ShowWholeEqpResultEntity> query(String model, String typeCode, Integer years, Integer months, String title, String titleSort) { 80 WeekendSqls<ShowWholeEqpResultEntity> weekendSqls = WeekendSqls.custom(); 81 if (! StringUtils.isEmpty(model)) { 82 weekendSqls.andEqualTo(ShowWholeEqpResultEntity::getModel, model); 83 } 84 if (! StringUtils.isEmpty(typeCode)) { 85 weekendSqls.andEqualTo(ShowWholeEqpResultEntity::getTypeCode, typeCode); 86 } 87 if (! StringUtils.isEmpty(title)) { 88 weekendSqls.andEqualTo(ShowWholeEqpResultEntity::getTitle, title); 89 } 90 if (! StringUtils.isEmpty(titleSort)) { 91 weekendSqls.andEqualTo(ShowWholeEqpResultEntity::getTitleSort, titleSort); 92 } 93 if (years != null) { 94 weekendSqls .andEqualTo(ShowWholeEqpResultEntity::getYears, years); 95 } 96 if (months != null) { 97 weekendSqls .andEqualTo(ShowWholeEqpResultEntity::getMonths, months); 98 } 99 Example example = new Example.Builder(ShowWholeEqpResultEntity.class) 100 .andWhere(weekendSqls).build(); 101 return showWholeEqpResultEntityMapper.selectByExample(example); 102 } 103 104 @Override 105 public ShowWholeEqpResultEntity update(ShowWholeEqpResultEntity entity) { 106 showWholeEqpResultEntityMapper.updateByPrimaryKeySelective(entity); 107 ShowWholeEqpResultEntity oldEntity = showWholeEqpResultEntityMapper.selectByPrimaryKey(entity); 108 return oldEntity; 109 } 110 111 @Override 112 public void delete(Long id) { 113 ShowWholeEqpResultEntity entity = new ShowWholeEqpResultEntity(); 114 entity.setId(id); 115 showWholeEqpResultEntityMapper.deleteByPrimaryKey(entity); 116 } 117 118 /** 119 * 检查某个月份中对应的记录是否存在,不存在则自动添加 120 */ 121 @Override 122 public void checkAndAddByMonth(String year_month) { 123 try { 124 String[] codeArr = new String[]{"Produce", "Quality", "Efficiency"}; 125 Map<String, String> codeMap = new HashMap<>(); 126 codeMap.put("Produce", "产出"); 127 codeMap.put("Quality", "质量"); 128 codeMap.put("Efficiency", "效率"); 129 if (StringUtils.isEmpty(year_month)) return; 130 // 检查月度记录:应该存在3条记录才正确 131 List<ShowWholeEqpResultEntity> monthList = showWholeEqpResultEntityMapper.selectForMonth(year_month); 132 Map<String, ShowWholeEqpResultEntity> monthMap = new HashMap<>(); 133 if (!CollectionUtils.isEmpty(monthList)) { 134 monthList.forEach(m->monthMap.put(m.getTypeCode(), m)); 135 } 136 List<ShowWholeEqpResultEntity> saveList = new ArrayList<>(); 137 // 检查是否存在 138 for (String code : codeArr) { 139 if (monthMap.get(code)==null) { 140 saveList.add(this.newShowResMonth(year_month, code, codeMap.get(code))); 141 } 142 } 143 144 // 得到月份对应的周 145 String[] ym = year_month.split("-"); 146 List<YearMonth> weeks = DateUtils.getAllWeekOfMonth(ym[0], ym[1]); 147 Map<String, String> weekSEMap = new HashMap<>(); 148 List<String> titleSorts = new ArrayList<>(); 149 for (YearMonth week : weeks) { 150 titleSorts.add(week.getStart()); 151 weekSEMap.put(week.getStart(), week.getEnd()); 152 } 153 List<ShowWholeEqpResultEntity> weekList = this.queryByWeeks(titleSorts); 154 Map<String, List<ShowWholeEqpResultEntity>> weekEntityMap = new HashMap<>(); 155 if (! CollectionUtils.isEmpty(weekList)) { // code 分组 156 weekEntityMap = weekList.stream().collect(Collectors.groupingBy(w -> w.getTypeCode())); 157 } 158 for (String code : codeArr) { 159 List<ShowWholeEqpResultEntity> weList = weekEntityMap.get(code); 160 Map<String, ShowWholeEqpResultEntity> oneWeekMap = new HashMap<>(); 161 if (!CollectionUtils.isEmpty(weList)) { 162 weList.forEach(w->oneWeekMap.put(w.getTitleSort(), w)); 163 } 164 // 遍历 165 for (String titleSort : titleSorts) { 166 String weekEnd = weekSEMap.get(titleSort); 167 String title = titleSort.substring(5)+"/"+weekEnd.substring(5); 168 if (oneWeekMap.get(titleSort)==null) { 169 saveList.add(this.newShowResWeek(title, titleSort, code, codeMap.get(code))); 170 } 171 } 172 } 173 174 // 检查批量保存 175 if (!CollectionUtils.isEmpty(saveList)) { 176 showWholeEqpResultEntityMapper.insertBatch(saveList); 177 } 178 } catch (Exception e) { 179 log.warn("checkAndAddByMonth:{}", e); 180 } 181 } 182 183 private List<ShowWholeEqpResultEntity> queryByWeeks(List<String> titleSorts) { 184 Example example = new Example.Builder(ShowWholeEqpResultEntity.class) 185 .andWhere( 186 WeekendSqls.<ShowWholeEqpResultEntity>custom() 187 .andIn(ShowWholeEqpResultEntity::getTitleSort, titleSorts) 188 ) 189 .build(); 190 return showWholeEqpResultEntityMapper.selectByExample(example); 191 } 192 193 194 private ShowWholeEqpResultEntity newShowResMonth(String year_month, String typeCode, String typeName) { 195 ShowWholeEqpResultEntity entity = new ShowWholeEqpResultEntity(); 196 entity.setModel("month"); 197 entity.setTitle(year_month); 198 String[] ym = year_month.split("-"); 199 entity.setYears(Integer.parseInt(ym[0])); 200 entity.setMonths(Integer.parseInt(ym[1])); 201 entity.setScore(60.0); 202 entity.setTypeCode(typeCode); 203 entity.setTypeName(typeName); 204 entity.setCtime(new Date().getTime()); 205 return entity; 206 } 207 208 private ShowWholeEqpResultEntity newShowResWeek(String title, String titleSort, String typeCode, String typeName) { 209 ShowWholeEqpResultEntity entity = new ShowWholeEqpResultEntity(); 210 entity.setModel("week"); 211 entity.setTitle(title); 212 entity.setTitleSort(titleSort); 213 entity.setScore(60.0); 214 entity.setTypeCode(typeCode); 215 entity.setTypeName(typeName); 216 entity.setCtime(new Date().getTime()); 217 return entity; 218 } 219 220 221 private List<MonthlyIndexModel> transferShowEntity2IndexModel(List<ShowWholeEqpResultEntity> list) { 222 List<MonthlyIndexModel> resList = Lists.newArrayList(); 223 try { 224 // 将数据转换成 MonthlyIndexModel 225 if (CollectionUtils.isEmpty(list)) { 226 return resList; 227 } 228 list.forEach(s -> { 229 MonthlyIndexModel indexModel = new MonthlyIndexModel(); 230 indexModel.setYear(s.getYears()); 231 indexModel.setMonth(s.getMonths()); 232 indexModel.setTitle(s.getTitle()); 233 indexModel.setTitleSort(s.getTitleSort()); 234 // 组装 indexModelData 235 List<IndexModelData> indexModelData = new ArrayList<>(1); 236 IndexModelData data = new IndexModelData(); 237 data.setName(s.getTypeName()); 238 data.setScore(s.getScore()); 239 data.setCode(s.getTypeCode()); 240 indexModelData.add(data); 241 indexModel.setIndexModelData(indexModelData); 242 resList.add(indexModel); 243 }); 244 } catch (Exception e) { 245 log.warn("{}", e); 246 return resList; 247 } 248 return resList; 249 } 250 251 @Override 252 public String syncDataMarket2DB() { 253 // 先进行聚合 254 return this.syncDataMarket2DB(null, null, null, null); 255 } 256 257 Map<Long, String> orgIdNameMap = Maps.newHashMap(); 258 @Override 259 public void exceptStorySendDingTalk(String currDay, Integer currYear, Integer currMonth) { 260 String NEWLINE = "\n\n"; 261 // 有问题的统计记录 262 List<StoryTeamUserWeekEntity> storyTeamUserWeekEntities; 263 if (currYear != null && currMonth != null) { 264 storyTeamUserWeekEntities = storyTeamUserWeekManager.queryAll(currYear, currMonth); 265 } else { 266 storyTeamUserWeekEntities = storyTeamUserWeekManager.queryByCtimeIsCurrMonth();// 查询两月内数据 267 } 268 if (CollectionUtils.isEmpty(storyTeamUserWeekEntities)) return; 269 270 // 筛选出问题故事点 271 List<StoryEntity> stories; 272 if (currYear != null && currMonth != null) { 273 stories = storyManager.queryByYearAndMonth(currYear, currMonth); 274 } else { 275 stories = storyManager.queryByCtimeIsCurrMonth();// 查询两月内数据 276 } 277 if (CollectionUtils.isEmpty(stories)) return; 278 279 // 查询出所有的teamId 280 List<Long> teamIds = null; 281 // 提测准时率问题 282 List<StoryEntity> submitTestOnTimeExcepStorys = dataMarketService.getWeekLvByType(teamIds, stories, storyTeamUserWeekEntities, StatisticsLvEnum.ForTestOntimeRatio.getValue()); 283 // 上线准时率问题 284 List<StoryEntity> onlineOnTimeExcepStorys = dataMarketService.getWeekLvByType(teamIds, stories, storyTeamUserWeekEntities, StatisticsLvEnum.ForPublishOntimeRatio.getValue()); 285 // 评审通过率问题 286 // List<StoryEntity> reviewExcepStorys = dataMarketService.getWeekLvByType(teamIds, stories, storyTeamUserWeekEntities, StatisticsLvEnum.ReviewSuccessRatio.getValue()); 287 // 提测通过率问题 288 List<StoryEntity> submitTestExcepStorys = dataMarketService.getWeekLvByType(teamIds, stories, storyTeamUserWeekEntities, StatisticsLvEnum.TestRatio.getValue()); 289 290 // 收集需求id 291 Set<Long> pidSet = new HashSet<>(); 292 if (!CollectionUtils.isEmpty(submitTestOnTimeExcepStorys)) { 293 submitTestOnTimeExcepStorys.forEach(s->pidSet.add(s.getStoryId())); 294 } 295 if (!CollectionUtils.isEmpty(onlineOnTimeExcepStorys)) { 296 onlineOnTimeExcepStorys.forEach(s->pidSet.add(s.getStoryId())); 297 } 298 // if (!CollectionUtils.isEmpty(reviewExcepStorys)) { 299 // reviewExcepStorys.forEach(s->pidSet.add(s.getStoryId())); 300 // } 301 if (!CollectionUtils.isEmpty(submitTestExcepStorys)) { 302 submitTestExcepStorys.forEach(s->pidSet.add(s.getStoryId())); 303 } 304 if (CollectionUtils.isEmpty(pidSet)) { 305 return; 306 } 307 // 查询父需求记录 308 List<StoryTeamUserEntity> teamUserEntities = storyTeamUserManager.queryByStoryIds(pidSet); 309 if (CollectionUtils.isEmpty(teamUserEntities)) return; 310 Map<Long, String> creatorMap = new HashMap<>(); 311 teamUserEntities.forEach(s->creatorMap.put(s.getStoryId(), s.getCreator())); 312 313 List<OrganizationEntity> allOrgs = organizationManager.queryAll(); 314 allOrgs.forEach(o -> orgIdNameMap.put(o.getId(), o.getTitle())); 315 boolean allowSend = false; 316 // 汇总消息allowSend:需求数据 317 StringBuilder builder = new StringBuilder(); 318 String title = "## 阿凡达【待处理异常需求】提醒 "; 319 if (currMonth !=null) { 320 title = "## 阿凡达 "+currYear+"."+currMonth+"【待处理异常需求】提醒 "; 321 } 322 323 builder.append(title).append(NEWLINE); 324 builder.append("> 注:(用户)为需求创建人。").append(NEWLINE); 325 builder.append("[阿凡达详情](http://avatar.bingex.com/dashboard)").append(NEWLINE); 326 int rowNum = 1; 327 328 if (! CollectionUtils.isEmpty(submitTestOnTimeExcepStorys)) { 329 allowSend = true; 330 builder.append("**").append(rowNum++).append("、提测准时、通过率异常需求").append("**").append(NEWLINE); 331 builder.append("> 请检查提测时间和是否通过").append(NEWLINE); 332 for (StoryEntity s : submitTestOnTimeExcepStorys) { 333 String creator = creatorMap.get(s.getStoryId()); 334 creator = StringUtils.isEmpty(creator)?"无":creator; 335 builder.append("* [").append(s.getName()).append("(准时率)").append("](https://www.tapd.cn/21483581/prong/stories/view/") 336 .append(s.getParentStoryId()==0L?s.getStoryId() : s.getParentStoryId()).append(")(").append(creator).append(")").append(NEWLINE); 337 } 338 } 339 if (! CollectionUtils.isEmpty(submitTestExcepStorys)) { 340 allowSend = true; 341 // 如果没有记录 前面的提测准时率问题需求 342 if (CollectionUtils.isEmpty(submitTestOnTimeExcepStorys)) { 343 builder.append("**").append(rowNum++).append("、提测准时、通过率异常需求").append("**").append(NEWLINE); 344 builder.append("> 请检查提测时间和是否通过").append(NEWLINE); 345 } 346 // 追加问题需求 347 for (StoryEntity s : submitTestExcepStorys) { 348 String creator = creatorMap.get(s.getStoryId()); 349 creator = StringUtils.isEmpty(creator)?"无":creator; 350 builder.append("* [").append(s.getName()).append("(通过率)").append("](https://www.tapd.cn/21483581/prong/stories/view/") 351 .append(s.getParentStoryId()==0L?s.getStoryId() : s.getParentStoryId()).append(")(").append(creator).append(")").append(NEWLINE); 352 } 353 } 354 355 if (! CollectionUtils.isEmpty(onlineOnTimeExcepStorys)) { 356 allowSend = true; 357 builder.append("**").append(rowNum++).append("、上线准时率异常需求").append("**").append(NEWLINE); 358 builder.append("> 请检查实际上线时间、预计结束时间和状态").append(NEWLINE); 359 for (StoryEntity s : onlineOnTimeExcepStorys) { 360 String creator = creatorMap.get(s.getStoryId()); 361 creator = StringUtils.isEmpty(creator)?"无":creator; 362 builder.append("* [").append(s.getName()).append("](https://www.tapd.cn/21483581/prong/stories/view/") 363 .append(s.getParentStoryId()==0L?s.getStoryId() : s.getParentStoryId()).append(")(").append(creator).append(")").append(NEWLINE); 364 } 365 } 366 // 产品指标去掉 367 // if (! CollectionUtils.isEmpty(reviewExcepStorys)) { 368 // allowSend = true; 369 // builder.append("**").append(rowNum++).append("、评审通过率异常需求").append("**").append(NEWLINE); 370 // builder.append("> 请检查需求评审时间和通过状态").append(NEWLINE); 371 // for (StoryEntity s : reviewExcepStorys) { 372 // String creator = creatorMap.get(s.getStoryId()); 373 // creator = StringUtils.isEmpty(creator)?"无":creator; 374 // builder.append("* [").append(s.getName()).append("](https://www.tapd.cn/21483581/prong/stories/view/") 375 // .append(s.getParentStoryId()==0L?s.getStoryId() : s.getParentStoryId()).append(")(").append(creator).append(")").append(NEWLINE); 376 // } 377 // } 378 379 if (allowSend) { 380 DingMsgUtils.sendMarkdown(title, builder.toString(), apolloConfigBusiness.getAvatarDataMarketExceptionDingDingUrl()); 381 } 382 383 // 需求信息补全提醒 384 this.alarmStoryInfoLack(); 385 } 386 387 // 父需求信息完整性告警:只进行月度告警 388 private void alarmStoryInfoLack() { 389 // 换行 390 String NEWLINE = "\n\n"; 391 // 根据月份查询父需求 story 392 List<StoryEntity> storyEntities = storyManager.queryByCtimeIsCurrMonth(); // storyManager.queryByYearAndMonth(currYear, currMonth); 393 if (CollectionUtils.isEmpty(storyEntities)) { 394 return; 395 } 396 // 筛选出父需求 397 List<StoryEntity> parentStoryList = storyEntities.stream().filter(s -> s.getParentStoryId() == 0).collect(toList()); 398 if (CollectionUtils.isEmpty(parentStoryList)) { 399 return; 400 } 401 Map<Long, StoryEntity> allStoryMap = new HashMap<>(); 402 // 添加一个不存在的预设置, 方式空条件查询 403 parentStoryList.forEach(s-> allStoryMap.put(s.getStoryId(), s)); 404 List<StoryTeamUserEntity> teamUserEntities = storyTeamUserManager.queryByCtimeIsCurrMonth(); // storyTeamUserManager.queryByStoryIdSet(parentStoryIdSet); 405 // 检查项目名称、需求来源 406 List<StoryEntity> twoLackList = new ArrayList<>(); 407 List<StoryEntity> demanderLackList = new ArrayList<>(); 408 List<StoryEntity> projectLackList = new ArrayList<>(); 409 for (StoryTeamUserEntity teamUser : teamUserEntities) { 410 if (StringUtils.isEmpty(teamUser.getDemander()) && StringUtils.isEmpty(teamUser.getProject())) { // 都缺少 411 twoLackList.add(allStoryMap.get(teamUser.getStoryId())); 412 } 413 else if (StringUtils.isEmpty(teamUser.getDemander())) { // 检查需求来源 414 demanderLackList.add(allStoryMap.get(teamUser.getStoryId())); 415 } 416 else if (StringUtils.isEmpty(teamUser.getProject())) { // 检查项目名称 417 projectLackList.add(allStoryMap.get(teamUser.getStoryId())); 418 } 419 } 420 // teamUserMap 421 Map<Long, StoryTeamUserEntity> teamUserMap = new HashMap<>(); 422 teamUserEntities.forEach(st->teamUserMap.put(st.getStoryId(), st)); 423 424 List<OrganizationEntity> allOrgs = organizationManager.queryAll(); 425 allOrgs.forEach(o -> orgIdNameMap.put(o.getId(), o.getTitle())); 426 boolean allowSend = false; 427 // 组装告警信息 428 // 汇总消息:需求数据 429 StringBuilder builder = new StringBuilder(); 430 String title = "## TAPD 开发团队待补充信息需求提醒"; 431 builder.append(title).append(NEWLINE); 432 builder.append("> 注:(用户)为需求创建人。").append(NEWLINE); 433 int rowNum = 1; 434 Map<String, String> demanderMap = new HashMap<>(); 435 Map<String, String> projectMap = new HashMap<>(); 436 if (! CollectionUtils.isEmpty(twoLackList)) { 437 allowSend = true; 438 builder.append("**").append(rowNum++).append("、待补充【需求来源】和【项目名称】需求").append("**").append(NEWLINE); 439 for (StoryEntity s : twoLackList) { 440 if (demanderMap.get(s.getName()) != null) continue; 441 StoryTeamUserEntity teamUserEntity = teamUserMap.get(s.getStoryId()); 442 String teamName = teamUserEntity!=null? orgIdNameMap.get(teamUserEntity.getTeamId()):"无小组"; 443 String creator = teamUserEntity!=null? teamUserEntity.getCreator():"无"; 444 builder.append("* [").append(s.getName()).append("](https://www.tapd.cn/21483581/prong/stories/view/") 445 .append(s.getParentStoryId()==0L?s.getStoryId() : s.getParentStoryId()).append(")(").append(creator).append(")").append(NEWLINE); 446 demanderMap.put(s.getName(), s.getName()); 447 } 448 } 449 if (! CollectionUtils.isEmpty(demanderLackList)) { 450 allowSend = true; 451 builder.append("**").append(rowNum++).append("、待补充【需求来源】需求").append("**").append(NEWLINE); 452 for (StoryEntity s : demanderLackList) { 453 if (demanderMap.get(s.getName()) != null) continue; 454 StoryTeamUserEntity teamUserEntity = teamUserMap.get(s.getStoryId()); 455 String teamName = teamUserEntity!=null? orgIdNameMap.get(teamUserEntity.getTeamId()):"无小组"; 456 String creator = teamUserEntity!=null? teamUserEntity.getCreator():"无"; 457 builder.append("* [").append(s.getName()).append("](https://www.tapd.cn/21483581/prong/stories/view/") 458 .append(s.getParentStoryId()==0L?s.getStoryId() : s.getParentStoryId()).append(")(").append(creator).append(")").append(NEWLINE); 459 demanderMap.put(s.getName(), s.getName()); 460 } 461 } 462 if (! CollectionUtils.isEmpty(projectLackList)) { 463 allowSend = true; 464 builder.append("**").append(rowNum++).append("、待补充【项目名称】需求").append("**").append(NEWLINE); 465 for (StoryEntity s : projectLackList) { 466 if (projectMap.get(s.getName()) != null) continue; 467 StoryTeamUserEntity teamUserEntity = teamUserMap.get(s.getStoryId()); 468 String teamName = teamUserEntity!=null? orgIdNameMap.get(teamUserEntity.getTeamId()):"无小组"; 469 String creator = teamUserEntity!=null? teamUserEntity.getCreator():"无"; 470 builder.append("* [").append(s.getName()).append("](https://www.tapd.cn/21483581/prong/stories/view/") 471 .append(s.getParentStoryId()==0L?s.getStoryId() : s.getParentStoryId()).append(")(").append(creator).append(")").append(NEWLINE); 472 projectMap.put(s.getName(), s.getName()); 473 } 474 } 475 if (allowSend) { 476 DingMsgUtils.sendMarkdown(title, builder.toString(), apolloConfigBusiness.getAvatarDataMarketExceptionDingDingUrl()); 477 } 478 } 479 480 481 Map<Long, String> testOrgIdNameMap = Maps.newHashMap(); 482 // 父需求信息完整性告警:只进行月度告警 483 @Override 484 public void alarmTestStoryInfoLack() { 485 // 换行 486 String NEWLINE = "\n\n"; 487 // 根据月份查询父需求 story 488 List<StoryTestEntity> storyEntities = storyTestManager.queryByCtimeIsCurrMonth(); // storyManager.queryByYearAndMonth(currYear, currMonth); 489 if (CollectionUtils.isEmpty(storyEntities)) { 490 return; 491 } 492 // 筛选出父需求 493 List<StoryTestEntity> parentStoryList = storyEntities.stream().filter(s -> s.getParentStoryId() == 0).collect(toList()); 494 if (CollectionUtils.isEmpty(parentStoryList)) { 495 return; 496 } 497 Map<Long, StoryTestEntity> allStoryMap = new HashMap<>(); 498 parentStoryList.forEach(s-> { 499 allStoryMap.put(s.getStoryId(), s); 500 }); 501 List<StoryTeamUserTestEntity> teamUserEntities = storyTeamUserTestManager.queryByCtimeIsCurrMonth(); // storyTeamUserManager.queryByStoryIdSet(parentStoryIdSet); 502 // 检查项目名称、需求来源 503 List<StoryTestEntity> twoLackList = new ArrayList<>(); 504 List<StoryTestEntity> demanderLackList = new ArrayList<>(); 505 List<StoryTestEntity> projectLackList = new ArrayList<>(); 506 for (StoryTeamUserTestEntity teamUser : teamUserEntities) { 507 if (StringUtils.isEmpty(teamUser.getDemander()) && StringUtils.isEmpty(teamUser.getProject())) { // 都缺少 508 StoryTestEntity storyTest = allStoryMap.get(teamUser.getStoryId()); 509 if (storyTest != null) { 510 twoLackList.add(storyTest); 511 } 512 } 513 else if (StringUtils.isEmpty(teamUser.getDemander())) { // 检查需求来源 514 StoryTestEntity storyTest = allStoryMap.get(teamUser.getStoryId()); 515 if (storyTest != null) { 516 demanderLackList.add(storyTest); 517 } 518 } 519 else if (StringUtils.isEmpty(teamUser.getProject())) { // 检查项目名称 520 StoryTestEntity storyTest = allStoryMap.get(teamUser.getStoryId()); 521 if (storyTest != null) { 522 projectLackList.add(storyTest); 523 } 524 } 525 } 526 // teamUserMap 527 Map<Long, StoryTeamUserTestEntity> teamUserMap = new HashMap<>(); 528 teamUserEntities.forEach(st->teamUserMap.put(st.getStoryId(), st)); 529 530 List<OrganizationTestEntity> allOrgs = organizationTestManager.queryAll(); 531 allOrgs.forEach(o -> testOrgIdNameMap.put(o.getId(), o.getTitle())); 532 boolean allowSend = false; 533 // 组装告警信息 534 // 汇总消息:需求数据 535 StringBuilder builder = new StringBuilder(); 536 String title = "## TAPD 测试团队待补充信息需求提醒"; 537 builder.append(title).append(NEWLINE); 538 builder.append("> 注:(用户)为需求创建人。").append(NEWLINE); 539 int rowNum = 1; 540 Map<String, String> demanderMap = new HashMap<>(); 541 Map<String, String> projectMap = new HashMap<>(); 542 if (! CollectionUtils.isEmpty(twoLackList)) { 543 allowSend = true; 544 builder.append("**").append(rowNum++).append("、待补充【需求来源】和【项目名称】需求").append("**").append(NEWLINE); 545 for (StoryTestEntity s : twoLackList) { 546 if (demanderMap.get(s.getStoryName()) != null) continue; 547 StoryTeamUserTestEntity teamUserEntity = teamUserMap.get(s.getStoryId()); 548 String teamName = teamUserEntity!=null? testOrgIdNameMap.get(teamUserEntity.getTeamId()):"无小组"; 549 String creator = teamUserEntity!=null? teamUserEntity.getCreator():"无"; 550 builder.append("* [").append(s.getStoryName()).append("](https://www.tapd.cn/21483581/prong/stories/view/") 551 .append(s.getParentStoryId()==0L?s.getStoryId() : s.getParentStoryId()).append(")(").append(creator).append(")").append(NEWLINE); 552 demanderMap.put(s.getStoryName(), s.getStoryName()); 553 } 554 } 555 if (! CollectionUtils.isEmpty(demanderLackList)) { 556 allowSend = true; 557 builder.append("**").append(rowNum++).append("、待补充【需求来源】需求").append("**").append(NEWLINE); 558 for (StoryTestEntity s : demanderLackList) { 559 if (demanderMap.get(s.getStoryName()) != null) continue; 560 StoryTeamUserTestEntity teamUserEntity = teamUserMap.get(s.getStoryId()); 561 String teamName = teamUserEntity!=null? testOrgIdNameMap.get(teamUserEntity.getTeamId()):"无小组"; 562 String creator = teamUserEntity!=null? teamUserEntity.getCreator():"无"; 563 builder.append("* [").append(s.getStoryName()).append("](https://www.tapd.cn/21483581/prong/stories/view/") 564 .append(s.getParentStoryId()==0L?s.getStoryId() : s.getParentStoryId()).append(")(").append(creator).append(")").append(NEWLINE); 565 demanderMap.put(s.getStoryName(), s.getStoryName()); 566 } 567 } 568 if (! CollectionUtils.isEmpty(projectLackList)) { 569 allowSend = true; 570 builder.append("**").append(rowNum++).append("、待补充【项目名称】需求").append("**").append(NEWLINE); 571 for (StoryTestEntity s : projectLackList) { 572 if (projectMap.get(s.getStoryName()) != null) continue; 573 StoryTeamUserTestEntity teamUserEntity = teamUserMap.get(s.getStoryId()); 574 String teamName = teamUserEntity!=null? testOrgIdNameMap.get(teamUserEntity.getTeamId()):"无小组"; 575 String creator = teamUserEntity!=null? teamUserEntity.getCreator():"无"; 576 builder.append("* [").append(s.getStoryName()).append("](https://www.tapd.cn/21483581/prong/stories/view/") 577 .append(s.getParentStoryId()==0L?s.getStoryId() : s.getParentStoryId()).append(")(").append(creator).append(")").append(NEWLINE); 578 projectMap.put(s.getStoryName(), s.getStoryName()); 579 } 580 } 581 if (allowSend) { 582 DingMsgUtils.sendMarkdown(title, builder.toString(), apolloConfigBusiness.getAvatarDataMarketExceptionDingDingUrl()); 583 } 584 } 585 586 /** 587 * 产研整体效能趋势 数据计算和同步 588 * queryWeeks: YearAndMonth.toString格式 589 */ 590 @Override 591 public String syncDataMarket2DB(Integer queryYear, Integer queryMonth, String currDay, Boolean syncAll) { 592 try { 593 // 默认不同步全部 594 if (syncAll == null) { 595 syncAll = false; 596 } 597 String queryWeeks = null; 598 // 如果不需要同步全部,说明是定时任务过来 599 if (syncAll) { 600 queryYear = null; 601 queryMonth = null; 602 // 删除全部数据 603 dataMarketDBSyncManager.clearAll(); 604 } else { 605 // 计算年、月 606 LocalDate localDate = LocalDate.now(); 607 if (queryYear == null) { 608 queryYear = localDate.getYear(); 609 } 610 if (queryMonth == null) { 611 queryMonth = localDate.getMonthValue(); 612 } 613 614 // 删除一个月的月数据 615 // dataMarketDBSyncManager.clearMonthData(queryYear, queryMonth); 616 617 // 如果传递的日期为空,使用当前日期的周 618 if (StringUtils.isEmpty(currDay)) { 619 int monthValue = localDate.getMonthValue(); 620 String months = ""+monthValue; 621 if (monthValue<10) { 622 months = "0"+monthValue; 623 } 624 currDay = localDate.getYear()+"-"+months+"-"+localDate.getDayOfMonth(); 625 } 626 String[] dateWeekInterval = DateUtils.getDateWeekInterval(currDay); 627 queryWeeks = new YearMonth(dateWeekInterval[0], dateWeekInterval[1]).toString(); 628 // 删除一个月的周段数据 629 // dataMarketDBSyncManager.clearWeekData(dateWeekInterval[0]); 630 } 631 632 List<OrganizationEntity> organizationEntities = organizationManager.queryAll(); 633 List<Long> smallTeamIds = Lists.newArrayList(); 634 List<OrganizationEntity> smallTeams = organizationEntities.stream().filter( 635 k->!k.getCtime().equals(0l) 636 ).collect(toList()); 637 smallTeams.stream().forEach(small -> smallTeamIds.add(small.getId())); 638 // 月度数据 639 dataMarketDBSyncManager.syncDataMarket2DBForMonths(smallTeamIds, queryYear, queryMonth); 640 // 周段数据 641 dataMarketDBSyncManager.syncDataMarket2DBForWeeks(smallTeamIds, queryWeeks); 642 return "OK"; 643 } catch (Exception e) { 644 return "ERROR: " + e.getMessage(); 645 } 646 } 647 648 649 }
- // 测试java.lang.Math
- // 必须继承TestCase
- public class Junit3TestCase extends TestCase {
- public Junit3TestCase() {
- super();
- }
- // 传入测试用例名称
- public Junit3TestCase(String name) {
- super(name);
- }
- // 在每个Test运行之前运行
- @Override
- protected void setUp() throws Exception {
- System.out.println("Set up");
- }
- // 测试方法。
- // 方法名称必须以test开头,没有参数,无返回值,是公开的,可以抛出异常
- // 也即类似public void testXXX() throws Exception {}
- public void testMathPow() {
- System.out.println("Test Math.pow");
- Assert.assertEquals(4.0, Math.pow(2.0, 2.0));
- }
- public void testMathMin() {
- System.out.println("Test Math.min");
- Assert.assertEquals(2.0, Math.min(2.0, 4.0));
- }
- // 在每个Test运行之后运行
- @Override
- protected void tearDown() throws Exception {
- System.out.println("Tear down");
- }
- }
如果采用默认的TestSuite,则测试方法必须是public void testXXX() [throws Exception] {}
的形式,并且不能存在依赖关系,因为测试方法的调用顺序是不可预知的。
上例执行后,控制台会输出
- Set up
- Test Math.pow
- Tear down
- Set up
- Test Math.min
- Tear down
从中,可以猜测到,对于每个测试方法,调用的形式是:
- testCase.setUp();
- testCase.testXXX();
- testCase.tearDown();
运行测试方法
在Eclipse中,可以直接在类名或测试方法上右击,在弹出的右击菜单中选择Run As -> JUnit Test。
在Mvn中,可以直接通过mvn test
命令运行测试用例。
也可以通过Java方式调用,创建一个TestCase
实例,然后重载runTest()
方法,在其方法内调用测试方法(可以多个)。
- TestCase test = new Junit3TestCase("mathPow") {
- // 重载
- protected void runTest() throws Throwable {
- testMathPow();
- };
- };
- test.run();
更加便捷地,可以在创建TestCase
实例时直接传入测试方法名称,JUnit会自动调用此测试方法,如
- TestCase test = new Junit3TestCase("testMathPow");
- test.run();
Junit TestSuite
TestSuite是测试用例套件,能够运行过个测试方法。如果不指定TestSuite,会创建一个默认的TestSuite。默认TestSuite会扫描当前内中的所有测试方法,然后运行。
如果不想采用默认的TestSuite,则可以自定义TestSuite。在TestCase中,可以通过静态方法suite()
返回自定义的suite。
- import junit.framework.Assert;
- import junit.framework.Test;
- import junit.framework.TestCase;
- import junit.framework.TestSuite;
- public class Junit3TestCase extends TestCase {
- //...
- public static Test suite() {
- System.out.println("create suite");
- TestSuite suite = new TestSuite();
- suite.addTest(new Junit3TestCase("testMathPow"));
- return suite;
- }
- }
允许上述方法,控制台输出
Set up
Test Math.pow
Tear down
并且只运行了testMathPow
测试方法,而没有运行testMathMin
测试方法。通过显式指定测试方法,可以控制测试执行的顺序。
也可以通过Java的方式创建TestSuite,然后调用TestCase,如
- // 先创建TestSuite,再添加测试方法
- TestSuite testSuite = new TestSuite();
- testSuite.addTest(new Junit3TestCase("testMathPow"));
- // 或者 传入Class,TestSuite会扫描其中的测试方法。
- TestSuite testSuite = new TestSuite(Junit3TestCase.class,Junit3TestCase2.class,Junit3TestCase3.class);
- // 运行testSuite
- TestResult testResult = new TestResult();
- testSuite.run(testResult);
testResult中保存了很多测试数据,包括运行测试方法数目(runCount
)等。
JUnit4
与JUnit3不同,JUnit4通过注解的方式来识别测试方法。目前支持的主要注解有:
@BeforeClass
全局只会执行一次,而且是第一个运行@Before
在测试方法运行之前运行@Test
测试方法@After
在测试方法运行之后允许@AfterClass
全局只会执行一次,而且是最后一个运行@Ignore
忽略此方法
下面举一个样例:
- import org.junit.After;
- import org.junit.AfterClass;
- import org.junit.Assert;
- import org.junit.Before;
- import org.junit.BeforeClass;
- import org.junit.Ignore;
- import org.junit.Test;
- public class Junit4TestCase {
- @BeforeClass
- public static void setUpBeforeClass() {
- System.out.println("Set up before class");
- }
- @Before
- public void setUp() throws Exception {
- System.out.println("Set up");
- }
- @Test
- public void testMathPow() {
- System.out.println("Test Math.pow");
- Assert.assertEquals(4.0, Math.pow(2.0, 2.0), 0.0);
- }
- @Test
- public void testMathMin() {
- System.out.println("Test Math.min");
- Assert.assertEquals(2.0, Math.min(2.0, 4.0), 0.0);
- }
- // 期望此方法抛出NullPointerException异常
- @Test(expected = NullPointerException.class)
- public void testException() {
- System.out.println("Test exception");
- Object obj = null;
- obj.toString();
- }
- // 忽略此测试方法
- @Ignore
- @Test
- public void testMathMax() {
- Assert.fail("没有实现");
- }
- // 使用“假设”来忽略测试方法
- @Test
- public void testAssume(){
- System.out.println("Test assume");
- // 当假设失败时,则会停止运行,但这并不会意味测试方法失败。
- Assume.assumeTrue(false);
- Assert.fail("没有实现");
- }
- @After
- public void tearDown() throws Exception {
- System.out.println("Tear down");
- }
- @AfterClass
- public static void tearDownAfterClass() {
- System.out.println("Tear down After class");
- }
- }
如果细心的话,会发现Junit3的package是junit.framework
,而Junit4是org.junit
。
执行此用例后,控制台会输出
Set up
Test Math.pow
Tear down
Set up
Test Math.min
Tear down
Set up
Test exception
Tear down
Set up
Test assume
Tear down
Tear down After class
可以看到,执行次序是@BeforeClass
-> @Before
-> @Test
-> @After
-> @Before
-> @Test
-> @After
-> @AfterClass
。@Ignore
会被忽略。
运行测试方法
与Junit3类似,可以在Eclipse中运行,也可以通过mvn test
命令运行。
Assert
Junit3和Junit4都提供了一个Assert类(虽然package不同,但是大致差不多)。Assert类中定义了很多静态方法来进行断言。列表如下:
- assertTrue(String message, boolean condition) 要求condition == true
- assertFalse(String message, boolean condition) 要求condition == false
- fail(String message) 必然失败,同样要求代码不可达
- assertEquals(String message, XXX expected,XXX actual) 要求expected.equals(actual)
- assertArrayEquals(String message, XXX[] expecteds,XXX [] actuals) 要求expected.equalsArray(actual)
- assertNotNull(String message, Object object) 要求object!=null
- assertNull(String message, Object object) 要求object==null
- assertSame(String message, Object expected, Object actual) 要求expected == actual
- assertNotSame(String message, Object unexpected,Object actual) 要求expected != actual
- assertThat(String reason, T actual, Matcher matcher) 要求matcher.matches(actual) == true
Mock/Stub
Mock和Stub是两种测试代码功能的方法。Mock测重于对功能的模拟。Stub测重于对功能的测试重现。比如对于List接口,Mock会直接对List进行模拟,而Stub会新建一个实现了List的TestList,在其中编写测试的代码。
强烈建议优先选择Mock方式,因为Mock方式下,模拟代码与测试代码放在一起,易读性好,而且扩展性、灵活性都比Stub好。
比较流行的Mock有:
其中EasyMock和Mockito对于Java接口使用接口代理的方式来模拟,对于Java类使用继承的方式来模拟(也即会创建一个新的Class类)。Mockito支持spy方式,可以对实例进行模拟。但它们都不能对静态方法和final类进行模拟,powermock通过修改字节码来支持了此功能。
EasyMock
IBM上有几篇介绍EasyMock使用方法和原理的文章:EasyMock 使用方法与原理剖析,使用 EasyMock 更轻松地进行测试。
EasyMock把测试过程分为三步:录制、运行测试代码、验证期望。
录制过程大概就是:期望method(params)执行times次(默认一次),返回result(可选),抛出exception异常(可选)。
验证期望过程将会检查方法的调用次数。
一个简单的样例是:
- @Test
- public void testListInEasyMock() {
- List list = EasyMock.createMock(List.class);
- // 录制过程
- // 期望方法list.set(0,1)执行2次,返回null,不抛出异常
- expect1: EasyMock.expect(list.set(0, 1)).andReturn(null).times(2);
- // 期望方法list.set(0,1)执行1次,返回null,不抛出异常
- expect2: EasyMock.expect(list.set(0, 1)).andReturn(1);
- // 执行测试代码
- EasyMock.replay(list);
- // 执行list.set(0,1),匹配expect1期望,会返回null
- Assert.assertNull(list.set(0, 1));
- // 执行list.set(0,1),匹配expect1(因为expect1期望执行此方法2次),会返回null
- Assert.assertNull(list.set(0, 1));
- // 执行list.set(0,1),匹配expect2,会返回1
- Assert.assertEquals(1, list.set(0, 1));
- // 验证期望
- EasyMock.verify(list);
- }
EasyMock还支持严格的检查,要求执行的方法次序与期望的完全一致。
Mockito
Mockito是Google Code上的一个开源项目,Api相对于EasyMock更好友好。与EasyMock不同的是,Mockito没有录制过程,只需要在“运行测试代码”之前对接口进行Stub,也即设置方法的返回值或抛出的异常,然后直接运行测试代码,运行期间调用Mock的方法,会返回预先设置的返回值或抛出异常,最后再对测试代码进行验证。可以查看此文章了解两者的不同。
官方提供了很多样例,基本上包括了所有功能,可以去看看。
这里从官方样例中摘录几个典型的:
- 验证调用行为
- import static org.mockito.Mockito.*;
- //创建Mock
- List mockedList = mock(List.class);
- //使用Mock对象
- mockedList.add("one");
- mockedList.clear();
- //验证行为
- verify(mockedList).add("one");
- verify(mockedList).clear();
- 对Mock对象进行Stub
- //也可以Mock具体的类,而不仅仅是接口
- LinkedList mockedList = mock(LinkedList.class);
- //Stub
- when(mockedList.get(0)).thenReturn("first"); // 设置返回值
- when(mockedList.get(1)).thenThrow(new RuntimeException()); // 抛出异常
- //第一个会打印 "first"
- System.out.println(mockedList.get(0));
- //接下来会抛出runtime异常
- System.out.println(mockedList.get(1));
- //接下来会打印"null",这是因为没有stub get(999)
- System.out.println(mockedList.get(999));
- // 可以选择性地验证行为,比如只关心是否调用过get(0),而不关心是否调用过get(1)
- verify(mockedList).get(0);
代码覆盖率
比较流行的工具是Emma和Jacoco,Ecliplse插件有eclemma。eclemma2.0之前采用的是Emma,之后采用的是Jacoco。这里主要介绍一下Jacoco。Eclmama由于是Eclipse插件,所以非常易用,就不多做介绍了。
Jacoco
Jacoco可以嵌入到Ant、Maven中,也可以使用Java Agent技术监控任意Java程序,也可以使用Java Api来定制功能。
Jacoco会监控JVM中的调用,生成监控结果(默认保存在jacoco.exec文件中),然后分析此结果,配合源代码生成覆盖率报告。需要注意的是:监控和分析这两步,必须使用相同的Class文件,否则由于Class不同,而无法定位到具体的方法,导致覆盖率均为0%。
Java Agent嵌入
首先,需要下载jacocoagent.jar文件,然后在Java程序启动参数后面加上 -javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2]
,具体的options可以在此页面找到。默认会在JVM关闭时(注意不能是kill -9
),输出监控结果到jacoco.exec文件中,也可以通过socket来实时地输出监控报告(可以在Example代码中找到简单实现)。
Java Report
可以使用Ant、Mvn或Eclipse来分析jacoco.exec文件,也可以通过API来分析。
- public void createReport() throws Exception {
- // 读取监控结果
- final FileInputStream fis = new FileInputStream(new File("jacoco.exec"));
- final ExecutionDataReader executionDataReader = new ExecutionDataReader(fis);
- // 执行数据信息
- ExecutionDataStore executionDataStore = new ExecutionDataStore();
- // 会话信息
- SessionInfoStore sessionInfoStore = new SessionInfoStore();
- executionDataReader.setExecutionDataVisitor(executionDataStore);
- executionDataReader.setSessionInfoVisitor(sessionInfoStore);
- while (executionDataReader.read()) {
- }
- fis.close();
- // 分析结构
- final CoverageBuilder coverageBuilder = new CoverageBuilder();
- final Analyzer analyzer = new Analyzer(executionDataStore, coverageBuilder);
- // 传入监控时的Class文件目录,注意必须与监控时的一样
- File classesDirectory = new File("classes");
- analyzer.analyzeAll(classesDirectory);
- IBundleCoverage bundleCoverage = coverageBuilder.getBundle("Title");
- // 输出报告
- File reportDirectory = new File("report"); // 报告所在的目录
- final HTMLFormatter htmlFormatter = new HTMLFormatter(); // HTML格式
- final IReportVisitor visitor = htmlFormatter.createVisitor(new FileMultiReportOutput(reportDirectory));
- // 必须先调用visitInfo
- visitor.visitInfo(sessionInfoStore.getInfos(), executionDataStore.getContents());
- File sourceDirectory = new File("src"); // 源代码目录
- // 遍历所有的源代码
- // 如果不执行此过程,则在报告中只能看到方法名,但是无法查看具体的覆盖(因为没有源代码页面)
- visitor.visitBundle(bundleCoverage, new DirectorySourceFileLocator(sourceDirectory, "utf-8", 4));
- // 执行完毕
- visitor.visitEnd();
- }