自动化工具 JUnit 结果重跑失败用例 (支持 Mvn 和 Ant)

胡刚 · April 23, 2015 · Last by 阿森 replied at January 29, 2016 · 2223 hits
本帖已被设为精华帖!

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, 返回结果Result
class 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输出日志,执行失败用例,结果如下:
这里写图片描述

根据第一次执行结果,回写输出日志:
这里写图片描述

第二次,根据被回写的输出日志,执行失败用例(只跑上一次失败的用例):
这里写图片描述

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 10 条回复 时间 点赞

大赞!!

“多说无用,放码过来”的典范~

这个功能很想用起来,但是看了代码感觉有点乱,可能是我对这块不熟,
楼主能否整理出一个简单能运行的demo版本供学习下。
万分感谢。

胡刚 #6 · May 12, 2015 作者

#5楼 @springs412 不要只看,自己跟着敲一遍代码就理解了。

将代码取下来看了一遍,大致明白了,有两个问题
1.最初的一次用例执行是手工操作还是也是自动执行的?
public static final String LOGNAME = "TEST-com.weibo.cases.suite.HugangTestSuite.txt";

2.如果是cucumber+junit的模式,也能通过代码执行错误的场景吗?

胡刚 #8 · June 07, 2015 作者

#7楼 @springs412 第一个问题:手工,每次跑完相关模块后(Ant执行),需手工修改不同模块的日志名,根据扫结果日志进行重试;Ant本身有支持失败重试,看过它的源码,它是生成一个单独的JUnit3测试文件,也是类似将失败用例聚合起来,但是JUnit3已经过时,故自己实现了失败用例重试。第二个问题,理论是可以的,可以根据你的结果失败用例的固有特征,修改正则表达式即可。

思路的确很好,收藏慢慢消化

学习了

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up