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

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

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

0 0
原创粉丝点击