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, ANT2
//  public static final String LOGTYPE = "MVN";

    // 失败日志名
    public static final String LOGNAME = "TEST-com.weibo.cases.suite.HugangTestSuite.txt";
    // 日志类型,支持MVN, ANT2
    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();
        // 记录FAILEDERROR
        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, 只存 FAILEDERROR
                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 输出日志,执行失败用例,结果如下:
这里写图片描述

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

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


↙↙↙阅读原文可查看相关链接,并与作者交流