JUnit结果重跑失败用例(支持Mvn和Ant)
来源:互联网 发布:用jsp简单游戏编程 编辑:程序博客网 时间:2024/06/05 02:23
1.背景
之前一篇文章介绍过
JUnit4—实践三:支持多线程,失败重试执行测试case的方法, 是ant 执行用例结束后,根据输出日志(类似:TEST-com.weibo.cases.suite.HugangTestSuite.txt),正则匹配找出错误用例,执行用例(JUnitCore),写结果(Result.txt),并将本次结果失败的用例回写到输出日志(TEST-com.weibo.cases.suite.HugangTestSuite.txt),达到重复执行失败用例。当然JUnit执行过程中,也可以支持重试机制,通过自定义注解(淘测试的一篇文章:AutomanX框架原理-JUnit4如何支持测试方法断言失败后重试),本文关注的是用例执行结束后,怎么根据本次的结果,进行重试。之前的代码写的比较糙,打算重构代码,进行代码解耦,提高可扩展性。使用简单工厂模式和策略模式,同时支持MVN(执行mvn test -Dtest=类名时,stdout的日志,为了支持TOAST框架使用(该框架根据stdout解析执行结果)) 和 ANT 日志,运行RunFailedCases.java文件即可(1.扫日志,2.运行失败用例, 3.结果展示, 4.将当次结果回写日志);可以重复运行,只执行上一次失败的用例。
2.实现
主函数,只需要修改日志名:LOGNAME和对应的日志类型:LOGTYPE。
package com.weibo.runfail;import java.io.IOException;import java.util.List;import java.util.Map;import java.util.concurrent.ExecutionException;/* * @author hugang * */public class RunFailedCases { // 失败日志名// public static final String LOGNAME = "319.log";// // 日志类型,支持MVN, ANT等2种// public static final String LOGTYPE = "MVN"; // 失败日志名 public static final String LOGNAME = "TEST-com.weibo.cases.suite.HugangTestSuite.txt"; // 日志类型,支持MVN, ANT等2种 public static final String LOGTYPE = "ANT"; // 运行结果日志名,无需修改 public static final String RESULTLOG = "Result.txt"; public static void main(String[] args) throws ExecutionException, IOException { // 记录失败用例,key:Class , value:List of methods Map<Class<?>, List<String>> failMap; // 放到当前包下 String logPath = System.getProperty("user.dir") + System.getProperty("file.separator") + "src" + System.getProperty("file.separator") + "com" + System.getProperty("file.separator") + "weibo" + System.getProperty("file.separator") + "runfail" + System.getProperty("file.separator") + LOGNAME; String resultPath = System.getProperty("user.dir") + System.getProperty("file.separator") + "src" + System.getProperty("file.separator") + "com" + System.getProperty("file.separator") + "weibo" + System.getProperty("file.separator") + "runfail" + System.getProperty("file.separator") + RESULTLOG; System.out.println(logPath); System.out.println(resultPath); // "\"的转义字符 logPath.replace("\\", "\\\\"); // 简单工厂模式和策略模式, 根据不同的LOGTYPE创建不同实例 FailCasesContext fcc = new FailCasesContext(LOGTYPE); // 通过扫日志,拿到对应失败case的Map failMap = fcc.getFailCases(logPath); // 执行失败用例 ExecuteCases ec = new ExecuteCases(); ec.executorCases(failMap, logPath, resultPath, LOGTYPE); }}
根据LOGTYPE实例化不同对象实例:
package com.weibo.runfail;import java.util.List;import java.util.Map;/* * @author hugang */public class FailCasesContext { FailCases fc = null; // 简单工厂模式和策略模式, 根据type声明,实例化不同实例 public FailCasesContext(String type) { // 为了支持JDK6 , case后用数字; JDK7可以直接String类型 int typeNum =2; if("MVN" == type){ typeNum = 1; }else if("ANT" == type){ typeNum = 2; } switch (typeNum) { case 1: FailCasesMvn fcm = new FailCasesMvn(); fc = fcm; break; case 2: FailCasesAnt fca = new FailCasesAnt(); fc = fca; break; } } public Map<Class<?>,List<String>> getFailCases(String logPath){ return fc.findFailedCases(logPath); }}
Ant 方式查找失败用例:
package com.weibo.runfail;import java.io.BufferedReader;import java.io.File;import java.io.FileNotFoundException;import java.io.FileReader;import java.io.IOException;import java.io.Reader;import java.util.ArrayList;import java.util.Iterator;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;import java.util.Set;import java.util.TreeSet;import java.util.regex.Matcher;import java.util.regex.Pattern;/* * @atuhor hugang */public class FailCasesAnt extends FailCases { @Override public Map<Class<?>, List<String>> findFailedCases(String logPath) { // TODO Auto-generated method stub // 文本每一行 List<String> strList = new ArrayList(); // 行号 List<Integer> flags = new ArrayList(); // 记录FAILED和ERROR Set<String> failSet = new TreeSet(); // String regexStr = // "(Testcase:\\s\\w*([\\w]*.{3,}\\w*.):\\sFAILED)|(Testcase:\\s\\w*([\\w]*.{3,}\\w*.):\\sCaused\\san\\sERROR)"; Pattern p = Pattern.compile("Testcase"); Matcher m; int i = 0; try { Reader re = new FileReader(new File(logPath)); BufferedReader bre = new BufferedReader(re); while (bre.ready()) { String str = bre.readLine(); strList.add(str); m = p.matcher(str); // 匹配后,记录匹配的行号 if (m.find()) { flags.add(i); System.out.println("find " + i); } i++; } for (int k = 0; k < flags.size(); k++) { // 去除SKIPPED, 只存 FAILED和ERROR if (!strList.get(flags.get(k)).contains("SKIPPED")) { // 从文本中取满足匹配的那行字符串 failSet.add(strList.get(flags.get(k))); } } bre.close(); re.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } Map<Class<?>, List<String>> myClassMethodMap = new LinkedHashMap(); List<String> className = new ArrayList<String>(); List<String> methodName = new ArrayList<String>(); for (Iterator it = failSet.iterator(); it.hasNext();) { // System.out.println(it.next().toString()); // Testcase: // testAPIRequest(com.weibo.cases.xuelian.FeedWithDarwinTagsForMovieStatusTest): // FAILED // 取出类和方法 String str = it.next().toString(); int classBegin = str.indexOf("("); int classEnd = str.indexOf(")"); // 类名 String classPart = str.substring(classBegin + 1, classEnd); // 方法名 String methodPart = str.substring(10, classBegin); Class<?> failClass; try { failClass = Class.forName(classPart); // 聚合 class-method 一对多 if (myClassMethodMap.containsKey(failClass)) { // 拿到之前的class 对应的list, 并在该list下新增 method, 再放回map List<String> beforeFailMethod = myClassMethodMap .get(failClass); beforeFailMethod.add(methodPart); myClassMethodMap.put(failClass, beforeFailMethod); } else { // 第一次添加该class时 List<String> firstMethod = new ArrayList<String>(); firstMethod.add(methodPart); myClassMethodMap.put(failClass, firstMethod); } } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return myClassMethodMap; }}
Mvn 方式查找失败用例:
package com.weibo.runfail;import java.io.BufferedReader;import java.io.File;import java.io.FileNotFoundException;import java.io.FileReader;import java.io.IOException;import java.io.Reader;import java.util.ArrayList;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;import java.util.Set;import java.util.TreeSet;import java.util.regex.Matcher;import java.util.regex.Pattern;import com.weibo.cases.hugang.*;import com.weibo.cases.xiaoyu.*;import com.weibo.cases.wanglei16.*;import com.weibo.cases.xuelian.*;import com.weibo.cases.lingna.*;import com.weibo.cases.maincase.*;/* * @author hugang */public class FailCasesMvn extends FailCases { @Override public Map<Class<?>, List<String>> findFailedCases(String logPath) { // TODO Auto-generated method stub // e.g. LikeObjectRpcTest.testLikeStatus:122 String failRegex = "\\s{2}(\\w+)\\.(\\w+):\\d{1,}"; Pattern pattern = Pattern.compile(failRegex); Matcher match; // 记录失败用例,key:类名, value:方法列表 Map<Class<?>, List<String>> myClassMethodMap = new LinkedHashMap<Class<?>, List<String>>(); Reader re; BufferedReader bre; try { re = new FileReader(new File(logPath)); bre = new BufferedReader(re); while (bre.ready()) { String str = bre.readLine(); match = pattern.matcher(str); // 匹配后,group(1)为类名,group(2)为方法名 if (match.find()) { String className = match.group(1); String methodName = match.group(2); // mvn执行结果中,失败用例只有单独类名,不是完全类名(包括包名) // 会导致ClassNotFoundException // RealClassMvn找所属的类名,返回完全类名 RealClassMvn rcm = new RealClassMvn(); rcm.findClass(className); String realClassName = rcm.getRealClassName(); Class<?> failClass = Class.forName(realClassName); // 聚合 class-method 一对多 if (myClassMethodMap.containsKey(failClass)) { // 拿到之前的class 对应的list, 并在该list下新增 method, 再放回map List<String> beforeFailMethod = myClassMethodMap.get(failClass); beforeFailMethod.add(methodName); myClassMethodMap.put(failClass, beforeFailMethod); } else { // 第一次添加该class时 List<String> firstMethod = new ArrayList<String>(); firstMethod.add(methodName); myClassMethodMap.put(failClass, firstMethod); } } } bre.close(); re.close(); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (Throwable e) { // TODO Auto-generated catch block e.printStackTrace(); } return myClassMethodMap; }}
Mvn结果中打印出的失败的类名,不是完全类名(包括包名),导致Class.forName(类名)时出现ClassNotFoundException,这是比较坑的地方;使用递归遍历包名,寻找正确的完全类名。
package com.weibo.runfail;/* * @author hugang */public class RealClassMvn { String[] packageName = { "com.weibo.cases.hugang", "com.weibo.cases.lingna", "com.weibo.cases.maincase", "com.weibo.cases.qiaoli", "com.weibo.cases.wanglei16", "com.weibo.cases.xiaoyu", "com.weibo.cases.xuelian" }; int now = 0; int retryNum = packageName.length; String realClassName; public String getRealClassName() { return realClassName; } public void setRealClassName(String realClassName) { this.realClassName = realClassName; } // 由于, mvn执行结果中失败的用例只返回类名(ActivitiesTimelineSpActivitiesTest), // 而不是完全类名 // (包括包名,e.g.com.weibo.cases.xuelian.ActivitiesTimelineSpActivitiesTest) // 导致Class.forName(类名)抛异常 // 使用递归加上不同包名,进行判断,找到正确完全类名 public void findClass(String className) throws Throwable{ try{ realClassName = packageName[now++] + "." + className; Class.forName(realClassName); setRealClassName(realClassName); }catch(ClassNotFoundException e){ if(now < retryNum){ findClass(className); }else{ throw e; } } }}
以上就是根据输出日志,找出错误用例信息,放到 Map<Class<?>, List<String>> myClassMethodMap = new LinkedHashMap();
中,key是类名,value是类对应的方法列表。
根据获得的myClassMethodMap执行用例,写结果日志:
package com.weibo.runfail;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;import java.text.SimpleDateFormat;import java.util.ArrayList;import java.util.Date;import java.util.List;import java.util.Map;import java.util.concurrent.ExecutionException;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future;import org.junit.runner.Result;import com.weibo.failmethods.WriteLogTest;/* * @author hugang */public class ExecuteCases { // 线程池大小 final static int THREADCOUNT = 50; public void writeResult(String resultPath, List<Result>methodsResult, int failNum, int successNum, int casesNum, long runTime, String logPath, String logType) throws IOException{ String filePath = resultPath; File file = new File(filePath); if (!file.exists()) { file.createNewFile(); } FileOutputStream fop = new FileOutputStream(file); SimpleDateFormat sdf = new SimpleDateFormat( "yyyy.MM.dd HH:mm:ss,SSS"); fop.write("(一).Time's Result generated on: ".getBytes()); fop.write(sdf.format(new Date()).getBytes()); fop.write("\n".getBytes()); StringBuffer sb = new StringBuffer(); sb.append("(二).日志类型:" + logType); sb.append("\n"); sb.append("(三).日志名:" + logPath); sb.append("\n"); sb.append("===================== 结果集 ====================="); sb.append("\n"); sb.append("用例总数:" + casesNum); sb.append(", 成功数:" + successNum); sb.append(", 失败数:" + failNum); sb.append(", 运行时间:" + (runTime / 1000) / 60 + " 分钟 " + (runTime / 1000) % 60 + " 秒"); sb.append("\n"); sb.append("================================================="); sb.append("\n"); sb.append("\n"); fop.write(sb.toString().getBytes()); for (int j = 0; j < methodsResult.size(); j++) { byte[] fail = methodsResult.get(j).getFailures().toString() .getBytes(); fop.write(fail); fop.write("\n".getBytes()); fop.write("\n".getBytes()); } fop.flush(); fop.close(); } public void executorCases(Map<Class<?>,List<String>> failcasesMap, String logPath, String resultPath, String logType) throws ExecutionException, IOException{ // 失败cases, key:Class, value:List of methods Map<Class<?>, List<String>> runcaseMap; runcaseMap = failcasesMap; int failNum = 0; int successNum = 0; int casesNum = 0; long runTime = 0L; List<Result> methodsResult = new ArrayList<Result>(); // 线程池 ExecutorService executorService = Executors .newFixedThreadPool(THREADCOUNT); // 存运行结果 List<Future<Result>> listFr = new ArrayList<Future<Result>>(); long startTime = System.currentTimeMillis(); // 多线程执行用例 for (Map.Entry<Class<?>, List<String>> entry : runcaseMap .entrySet()) { Class<?> testClass = entry.getKey(); List<String> failMethod = entry.getValue(); casesNum += failMethod.size(); for (int i = 0; i < failMethod.size(); i++) { Future<Result> fr = executorService.submit(new ThreadRunTest( testClass, failMethod.get(i))); listFr.add(fr); } } // 记录结果 for (Future<Result> fr : listFr) { try { while (!fr.isDone()) ; Result result = fr.get(); if (result.wasSuccessful()) { successNum++; } else { failNum++; methodsResult.add(result); } } catch (InterruptedException e) { e.printStackTrace(); } finally { executorService.shutdown(); } } long endTime = System.currentTimeMillis(); runTime = endTime - startTime; // 写结果日志 writeResult(resultPath, methodsResult, failNum, successNum, casesNum, runTime, logPath, logType); // 回写日志, 根据logType回写不同格式的运行失败用例回日志文件, 简单工厂模式 WriteLogFactory wlf = new WriteLogFactory(); wlf.writeLog(logPath, logType, resultPath); }}
使用多线程执行每个用例,线程执行体:
package com.weibo.runfail;import java.util.concurrent.Callable;import org.junit.runner.JUnitCore;import org.junit.runner.Request;import org.junit.runner.Result;/* * @author hugang *///Callable<Result>实现类,一个线程执行一个case, 返回结果Resultclass ThreadRunTest implements Callable<Result>{ private Class oneFailClass; private String oneFailMethod; public ThreadRunTest(Class oneFailClass, String oneFailMethod){ this.oneFailClass = oneFailClass; this.oneFailMethod = oneFailMethod; } @Override public Result call() throws Exception { // TODO Auto-generated method stub // JUnitCore执行JUnit用例 JUnitCore junitRunner = new JUnitCore(); Request request = Request.method(oneFailClass, oneFailMethod); Result result = junitRunner.run(request); return result; }}
用例执行结束后,写结果日志, 使用ExecuteCases类中writeResult方法:
public void writeResult(String resultPath, List<Result>methodsResult, int failNum, int successNum, int casesNum, long runTime, String logPath, String logType) throws IOException{...}
根据本次运行结果,再将失败的回写到输出日志,为下一次提供依据; 使用简单工厂模式,会根据不同日志类型,使用对应的格式将本次运行失败的结果回写到输出日志。
package com.weibo.runfail;import java.io.IOException;/* * @author hugang */public class WriteLogFactory { public void writeLog(String logPath, String logType, String resultPath) throws IOException { // 为了支持JDK6 , case后用数字; JDK7可以直接String类型 int typeNum = 2; if ("MVN" == logType) { typeNum = 1; } else if ("ANT" == logType) { typeNum = 2; } switch (typeNum) { case 1: new WriteLogMvn().writeLogMvn(logPath, resultPath); break; case 2: new WriteLogAnt().writeLogAnt(logPath, resultPath); } }}
Mvn格式回写输出日志:
package com.weibo.runfail;import java.io.BufferedReader;import java.io.BufferedWriter;import java.io.File;import java.io.FileReader;import java.io.FileWriter;import java.io.IOException;import java.text.SimpleDateFormat;import java.util.ArrayList;import java.util.Date;import java.util.List;import java.util.regex.Matcher;import java.util.regex.Pattern;/* * author hugang */public class WriteLogMvn { public void writeLogMvn(String logPath, String resultPath) throws IOException{ BufferedWriter writer; BufferedReader reader; // 日志文件 File sourceFile = new File(logPath); if (!sourceFile.exists()) { sourceFile.createNewFile(); } writer = new BufferedWriter(new FileWriter(sourceFile)); // 结果日志 File readFile = new File(resultPath); if (!readFile.exists()) { readFile.createNewFile(); System.out.println("read 文件不存在, Result.txt 不存在"); } else { System.out.println("" + readFile.canRead() + " " + readFile.length() + " " + readFile.getAbsolutePath() + " " + readFile.getCanonicalPath()); } reader = new BufferedReader(new FileReader(readFile)); // 正则表达式找失败用例, result日志格式 String pattern = "\\[(\\w+)\\((.*)\\)"; Pattern pt = Pattern.compile(pattern); Matcher mt; List<String> strList = new ArrayList<String>(); // 行号// List<Integer> flags = new ArrayList();// int i = 0; SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); writer.write(sdf.format(new Date())); writer.newLine(); try { while (reader.ready()) { String str = reader.readLine(); strList.add(str); mt = pt.matcher(str); // 匹配后,记录匹配的行号 if (mt.find()) { String[] className = mt.group(2).split("\\."); int size = className.length; String methodName = mt.group(1); // \\s{2}(\\w+)\\.(\\w+):\\d{1,} // 模拟MVN 日志,回写 \s\sclassname.methodname:\d{1,} // 为了与MVN日志统一(正则匹配一致, 循环跑),完全类名去掉包名 // com.weibo.cases.wanglei16.LikesByMeBatchTest String failStr = " " + className[size-1] + "." + methodName + ":1"; writer.write(failStr); writer.newLine(); }// i++; }// for (int k = 0; k < flags.size(); k++) {// // 模拟MVN 日志,回写 \s\sclassname.methodname:\d{1,}// String failStr = "Testcase:" + strList.get(flags.get(k));// writer.write(failStr);// writer.newLine();// } } finally { writer.close(); reader.close(); } }}
Ant格式回写输出日志:
package com.weibo.runfail;import java.io.BufferedReader;import java.io.BufferedWriter;import java.io.File;import java.io.FileReader;import java.io.FileWriter;import java.io.IOException;import java.text.SimpleDateFormat;import java.util.ArrayList;import java.util.Date;import java.util.List;import java.util.regex.Matcher;import java.util.regex.Pattern;/* * @author hugang */public class WriteLogAnt { public void writeLogAnt(String logPath, String resultPath) throws IOException{ BufferedWriter writer; BufferedReader reader; // 日志文件 File sourceFile = new File(logPath); if (!sourceFile.exists()) { sourceFile.createNewFile(); } writer = new BufferedWriter(new FileWriter(sourceFile)); // 结果日志 File readFile = new File(resultPath); if (!readFile.exists()) { readFile.createNewFile(); System.out.println("read 文件不存在, Result.txt 不存在"); } else { System.out.println("" + readFile.canRead() + " " + readFile.length() + " " + readFile.getAbsolutePath() + " " + readFile.getCanonicalPath()); } reader = new BufferedReader(new FileReader(readFile)); // 正则表达式找失败用例, result日志格式 String pattern = "\\[(\\w+)\\((.*)\\)"; Pattern pt = Pattern.compile(pattern); Matcher mt; List<String> strList = new ArrayList<String>(); // 行号 List<Integer> flags = new ArrayList(); int i = 0; SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 写时间戳 writer.write(sdf.format(new Date())); writer.newLine(); try { while (reader.ready()) { String str = reader.readLine(); strList.add(str); mt = pt.matcher(str); // 匹配后,记录匹配的行号 if (mt.find()) { flags.add(i); System.out.println("find " + i); } i++; } for (int k = 0; k < flags.size(); k++) { // 模拟 FindFailTest.java 截取的规则 String failStr = "Testcase:" + strList.get(flags.get(k)); writer.write(failStr); writer.newLine(); } } finally { writer.close(); reader.close(); } }}
三. 结果展示
Ant执行用例后,输出日志:
第一次根据Ant输出日志,执行失败用例,结果如下:
根据第一次执行结果,回写输出日志:
第二次,根据被回写的输出日志,执行失败用例(只跑上一次失败的用例):
- JUnit结果重跑失败用例(支持Mvn和Ant)
- Junit和Ant入门(一) JUnit
- Junit和Ant入门(二) Ant
- Ant 和 Junit 的复用
- 用Ant和JUnit构建自动化测试
- JUnit和ant结合
- JUnit 和 Ant
- Junit和Ant入门(三) Junit和Ant的混合使用
- JUnit(二) 测试结果和注解
- ant脚步执行JUnit测试用例
- ant脚本执行junit测试用例
- 对基于JUnit和Ant的测试用例执行过程使用Kieker(AspectJ)进行监控的方法
- JUnit和Ant整合案例
- Junit和Ant入门(四) 问题详解
- 失败用例的重跑,实践
- 用ANT执行JUNIT测试
- 用Ant执行Junit测试
- 扩展Robot Framework,实现失败用例自动再执行(失败重跑)
- Facebook POP 进阶指南
- Android nfc模块读写MifareClassic卡50
- listctrl 插入多条数据时避免闪烁
- PHP的静态变量介绍
- C++ 版 Longest Common Prefix leetcode提交正确。
- JUnit结果重跑失败用例(支持Mvn和Ant)
- make编译报错 "Makefile", line 10: make: Dependency line needs colon or double colon operator.
- 三消游戏的一个重要帮助函数
- Axure RP快速入门
- python11:函数
- Android如何引用其他工程
- Cordova 开发属于自己的插件(plugin)
- MakeFile 固定格式编写
- Jfinal框架学习系列之图片上传