依赖Jacob自动生成测试报告

引言

项目上线前,一般都要求测试经理提供测试报告。虽说每个项目实现的需求不尽一样,但测试报告的模板往往是一致的,且大多是word格式的。接下来介绍一个word操作神器—Jacob,用于自动生成测试报告。

Jacob介绍

JACOB is a JAVA-COM Bridge that allows you to call COM Automation components from Java. It uses JNI to make native calls to the COM libraries. JACOB runs on x86 and x64 environments supporting 32 bit and 64 bit JVMs.

注意事项

官网下载地址

  • jdk如果是32位,则需将jacob-1.18-x86.dll放到C:\Windows\System32目录下,64位则将jacob-1.18-x64.dll 放到C:\Windows\SysWOW64目录下。
    ps:java -version可查看jdk位数。
  • 将jacob-XXX.dll放到 jdk安装目录的jre\bin目录下。

实现步骤

1、 定义测试报告模板

测试报告模板
密码:e6iz59

2、分析案例执行结果文件,填充测试报告模板

案例执行结果文件(ps:该文件由工具生成)

根据案例执行结果文件分析缺陷信息及案例执行状态。

public static Map<String,String> countBug(String caseBugFilePath) throws BiffException, IOException{
        
        Map<String,String> testModelMap = new HashMap<String,String>();
        
        InputStream instream = new FileInputStream(caseBugFilePath);
        Workbook rwb = Workbook.getWorkbook(instream);
        
        Sheet sheet = rwb.getSheet(0);
        
        int rsRows = sheet.getRows(); // 获取总行数,案例数为总行数-1
        int caseNum = rsRows -1;
        
        int caseExcute = 0; //已执行的用例(出去执行结果是“未执行”的用例)
        
        String testModel ="";
        if(!sheet.getCell(1, 1).getContents().equals(""))
            testModel = sheet.getCell(1, 1).getContents();  //第2列第2行,初始化功能点描述
        else
            JOptionPane.showMessageDialog(null, "【案例执行结果文件有误,请检查】", "message",JOptionPane.ERROR_MESSAGE);

        int caseNumTemp = 0;
        StringBuffer buffer = new StringBuffer();
        
        for (int i = 1; i < rsRows; i++) {     //第一行为表头
            String excuteResult = sheet.getCell(8, i).getContents(); // 第9列第i行,执行结果
            if(!excuteResult.trim().equals("未执行"))
                caseExcute++;  //统计已执行的用例总数

            if(sheet.getCell(1, i).getContents().equals(testModel)){
                caseNumTemp++;  //模块对应的案例数
                
                String bugMess = sheet.getCell(10, i).getContents();      // 第11列第i行,缺陷信息
                if(!bugMess.equals(""))
                    buffer.append(bugMess);  //提取缺陷信息
                
                testModelMap.put(testModel, caseNumTemp+":"+buffer.toString()); //key为 模块名,value为 案例数,缺陷信息
            }else{
                caseNumTemp = 1;
                buffer.delete(0, buffer.length());  //清空buffer信息
                
                String bugMess = sheet.getCell(10, i).getContents();      // 第11列第i行,缺陷信息
                if(!bugMess.equals(""))
                    buffer.append(bugMess);  //提取缺陷信息
                
                testModel = sheet.getCell(1, i).getContents().trim();  //重新赋值给testModel
                
                testModelMap.put(testModel, caseNumTemp+":"+buffer.toString()); //key为 模块名,value为 案例数,缺陷信息
            }
        }
        
        testModelMap.put("${caseNum}", caseNum +"");      //用例总数
        testModelMap.put("${caseExcute}", caseExcute+""); //已执行的用例
        
        System.out.println("用例总数:" + caseNum);
        System.out.println("已执行的用例数:" + caseExcute);
        System.out.println("模块信息:\n" + testModelMap);
        
        return testModelMap;
    }

替换测试报告模板定义的变量、插入附件、生成目录。

public class TestReport {

    public static String report(String systemName,String author,String projectName,String caseBugFilePath) throws IOException, BiffException{
        
        Map<String,String> map = new HashMap<String,String>();
        
        /*
         * map规则为:key=模块,value=案例数:缺陷信息
         * {模块1=49:GYL-1859_[关闭] GYL-1865_[关闭] GYL-1861_[关闭] GYL-1866_[关闭] , 模块2=188:}
         */
        map = BugCount.countBug(caseBugFilePath);  //统计缺陷及案例信息
        
        JacobFunction m = new JacobFunction(false);
        m.createNewDocument();
        
        try{    
            File file1 = new File("template\\测试报告模板.doc");
            File file1_temp = new File("template\\测试报告模板temp.doc");
            m.copyFile(file1,file1_temp); //拷贝文件,防止生成测试报告失败,测试报告模板.doc被改写
            
            File file2 = new File("testReport\\"+systemName+"_"+projectName+"_测试报告"+".doc");
            
            m.openDocument(file1_temp.getAbsolutePath()); 
            //m.moveStart();
            
            Date dt=new Date();
            SimpleDateFormat matter1=new SimpleDateFormat("yyyy-MM-dd");
            m.replaceAllText("${date}",matter1.format(dt));   //当前日期
            
            m.replaceAllText("${author}",author);        //作者
            m.replaceAllText("${project}",projectName);  //项目名称
            
            System.out.println("用例总数:"+ map.get("${caseNum}"));
            m.replaceAllText("${caseNum}",map.get("${caseNum}")); //用例总数
            
            System.out.println("已执行的用例:"+ map.get("${caseExcute}"));
            m.replaceAllText("${caseExcute}",map.get("${caseExcute}")); //已执行的用例
            
            int modelNum = map.size()-2; //除去 用例总数和已执行额用例,剩下为功能模块数 
            m.replaceAllText("${modelNum}",modelNum+""); //功能模块数
            
            int i = 1;
            int caseNumPerModel = 0; //每个功能模块对应的案例数
            int bugClosePerModel = 0; //每个功能模块 关闭的缺陷数
            int bugHupPerModel = 0; //每个功能模块 挂起的缺陷数
            int bugOtherPerModel = 0; //每个功能模块 其他状态的缺陷数
            int bugCloseTotal = 0; //关闭缺陷数 总数
            int bugHupTotal = 0;   //挂起缺陷数 总数
            int bugOtherTotal = 0; //其他状态缺陷数 总数
            
            for(String key:map.keySet()){  
                if(!key.equals("${caseNum}") && !key.equals("${caseExcute}")){
                    m.addLastTableRow(6); //表格增加一行
                    m.putTxtToCell(6,1+i,1,key);    //填充功能模块名称
                    m.putTxtToCell(6,1+i,4,"100%"); //填充覆盖率
                    
                    String[] array = map.get(key).split(":");
                    caseNumPerModel = Integer.parseInt(array[0]);
                    m.putTxtToCell(6,1+i,2,caseNumPerModel+"");    //填充规则点(模块案例数)
                    m.putTxtToCell(6,1+i,3,caseNumPerModel+"");    //填充已覆盖(模块案例数)
                    
                    m.addLastTableRow(7); //表格增加一行
                    m.putTxtToCell(7,1+i,1,key);    //填充功能模块名称
                    
                    if(array.length == 1){ //表示没有缺陷
                        m.putTxtToCell(7,1+i,2,"0");    //填充每个功能模块 关闭的缺陷数
                        m.putTxtToCell(7,1+i,3,"0");    //填充每个功能模块 挂起的缺陷数
                        m.putTxtToCell(7,1+i,4,"0");    //填充每个功能模块 其他状态的缺陷数
                        
                    }else{
                        String array1[] = array[1].toString().trim().split(" ");
                        
                        for(int j =0;j<array1.length;j++){
                            if(array1[j].indexOf("关闭") >= 0 ){
                                bugClosePerModel++; //每个模块关闭的缺陷数
                                bugCloseTotal++;
                            }
                                
                            else 
                                if(array1[j].indexOf("缺陷挂起") >= 0){
                                    bugHupPerModel++; //每个模块 缺陷挂起数
                                    bugHupTotal++;
                                }
                                else{
                                    bugOtherPerModel++;  //每个模块 其他的状态缺陷数
                                    bugOtherTotal++;
                                }   
                        }
                        m.putTxtToCell(7,1+i,2,bugClosePerModel+"");    //填充每个功能模块 关闭的缺陷数
                        m.putTxtToCell(7,1+i,3,bugHupPerModel+"");    //填充每个功能模块 挂起的缺陷数
                        m.putTxtToCell(7,1+i,4,bugOtherPerModel+"");    //填充每个功能模块 其他状态的缺陷数
                    }
                    
                    i++;
                    bugClosePerModel =0; //下次循环前再初始化
                    bugHupPerModel =0;   //下次循环前再初始化
                    bugOtherPerModel =0; //下次循环前再初始化
                }  
            }  
            
            m.replaceAllText("${bugCloseTotal}",bugCloseTotal+""); //关闭的缺陷总数
            m.replaceAllText("${bugHupTotal}",bugHupTotal+""); //挂起的缺陷总数
            m.replaceAllText("${bugOtherTotal}",bugOtherTotal+""); //其他状态的缺陷总数
            int bugNum = bugCloseTotal + bugHupTotal + bugOtherTotal;
            m.replaceAllText("${bugNum}",bugNum+""); //缺陷总数
            
            System.out.println("功能模块数:"+modelNum);
            System.out.println("关闭的缺陷总数:"+ bugCloseTotal);
            System.out.println("挂起的缺陷总数:"+ bugHupTotal);
            System.out.println("其他状态的缺陷总数:"+ bugOtherTotal);
            System.out.println("缺陷总数:"+ bugNum);
            
            
            File file = new File(caseBugFilePath);  
            String caseBugFileName = file.getName();
            
            m.replaceFile("${insertCaseBugFile}", caseBugFilePath, caseBugFileName);  //插入附件
            
            //m.addLastTableRow(6);
            //m.putTxtToCell(6,1,1,"tomandytomandyddddd");
            
            m.createContents("${contents}");  //生成目录
            
            //String[] a = {"s","dd"};
            //int b = Integer.parseInt(a[0]) ;  //模拟异常
            
            m.save(file2.getAbsolutePath());  //保存测试报告
            m.close();
            
            return "success";
            
        }catch (Exception e){
            m.close();       //如果生成测试报告异常,则关闭文档,防止报错“测试报告模板temp.doc已被占用”
            System.out.println(e.getMessage());
            return e.getMessage();
        }
        
    }
}

Jacob操作word的各类方法。

public class JacobFunction {
    
    // word文档
    private Dispatch doc;

    // word运行程序对象
    private static ActiveXComponent word;

    // 所有word文档集合
    private Dispatch documents;

    // 选定的范围或插入点
    private static Dispatch selection;

    private boolean saveOnExit = true; 
    
    /** *//**
     *    
     * @param visible 为true表示word应用程序可见
     */
    public JacobFunction(boolean visible) {   //是否打开word应用程序
            if (word == null) {
                    word = new ActiveXComponent("Word.Application");
                    word.setProperty("Visible", new Variant(visible));
            }
            if (documents == null)
                    documents = word.getProperty("Documents").toDispatch();
    }
    
    
    /** *//**
     * 从选定内容或插入点开始查找文本
     *    
     * @param toFindText 要查找的文本
     * @return boolean true-查找到并选中该文本,false-未查找到文本
     */
    public static boolean find(String toFindText) {
            if (toFindText == null || toFindText.equals(""))
                    return false;
            // 从selection所在位置开始查询
            Dispatch find = word.call(selection, "Find").toDispatch();
            // 设置要查找的内容
            Dispatch.put(find, "Text", toFindText);
            // 向前查找
            Dispatch.put(find, "Forward", "True");
            // 设置格式
            Dispatch.put(find, "Format", "True");
            // 大小写匹配
            Dispatch.put(find, "MatchCase", "True");
            // 全字匹配
            Dispatch.put(find, "MatchWholeWord", "True");
            // 查找并选中
            //System.out.println("查找");
            return Dispatch.call(find, "Execute").getBoolean();
    }
    
    /** *//**
     * 全局替换文本
     *    
     * @param toFindText 查找字符串
     * @param newText 要替换的内容
     */
    public void replaceAllText(String toFindText, String newText) {
            moveStart();   //移到文件开头
            while (find(toFindText)) {
                    Dispatch.put(selection, "Text", newText);
                    Dispatch.call(selection, "MoveRight");   
                    moveRight(1);
                    //System.out.println("替换");
            }
    } 
    
    public   void  moveRight( int  pos) {
        if  (selection ==  null )
           selection = Dispatch.get(word,  "Selection" ).toDispatch();
        for  ( int  i =  0 ; i < pos; i++)
           Dispatch.call(selection,  "MoveRight" ); 
    }
    
    /** *//**
     * 创建一个新的word文档
     *    
     */
    public  void createNewDocument() {
            doc = Dispatch.call(documents, "Add").toDispatch();
            selection = Dispatch.get(word, "Selection").toDispatch();
    } 
    
    /** *//**
     * 打开一个已存在的文档
     *    
     * @param docPath
     */
    public void openDocument(String docPath) {
            closeDocument();
            doc = Dispatch.call(documents, "Open", docPath).toDispatch();
            selection = Dispatch.get(word, "Selection").toDispatch();
    } 
    
    /** *//**
     * 文件保存或另存为
     *    
     * @param savePath 保存或另存为路径
     * @throws IOException 
     */
    public void save(String savePath) throws IOException {
        
            File file = new File(savePath);
            file.createNewFile();
            Dispatch.call(
                            (Dispatch) Dispatch.call(word, "WordBasic").getDispatch(),
                            "FileSaveAs", savePath);
    } 
    
    /** *//**
     * 关闭当前word文档
     *    
     */
    public void closeDocument() {
            if (doc != null) {
                    //Dispatch.call(doc, "Save");
                    Dispatch.call(doc, "Close", new Variant(saveOnExit));
                    doc = null;
            }
    } 
    
    /** *//**
     * 关闭全部应用
     *    
     */
    public void close() {
            closeDocument();
            if (word != null) {
                    Dispatch.call(word, "Quit");
                    word = null;
            }
            selection = null;
            documents = null;
    } 
    
    /** *//**
     * 把插入点移动到文件首位置
     *    
     */
    public void moveStart() {
            if (selection == null)
                    selection = Dispatch.get(word, "Selection").toDispatch();
            Dispatch.call(selection, "HomeKey", new Variant(6));
    } 
    
    /** *//**
     *    
     * @param toFindText 要查找的字符串
     * @param imagePath 文件路径
     * @return
     */
    public boolean replaceFile(String toFindText, String insertFilePath,String fileName) {
            moveStart();  //从文件首位置开始
            if (!find(toFindText))
                    return false;
            System.out.println("文件路径:"+insertFilePath);
            System.out.println("文件名:"+fileName);
            Dispatch.call(word, "Run", new Variant("InsertCaseMess"),new Variant(insertFilePath),new Variant(fileName));
            return true;
    }
    
    /** *//**
     *    生成目录
     * @param toFindText 要查找的字符串
     * @return
     */
    
    public boolean createContents(String toFindText){
        moveStart();  //从文件首位置开始
        if (!find(toFindText))
                return false;
        
        Dispatch alignment = Dispatch.get(selection, "ParagraphFormat")
                .toDispatch(); // 行列格式化需要的对象
        Dispatch.put(alignment, "Alignment", "1"); // (1:置中 2:靠右 3:靠左)
      //  insertNewParagraph();
        //moveRight(1);
        Dispatch range = Dispatch.get(this.selection, "RANGE").toDispatch();

        Dispatch fields = Dispatch.call(this.selection, "FIELDS").toDispatch();

        Variant call = Dispatch.call(fields,
                "ADD",
                range,
                new Variant(-1),
                new Variant("TOC"),
                new Variant(true));


        Dispatch tablesOfContents = Dispatch.call(doc, "TablesOfContents").toDispatch();// 整个目录区域

        // 整个目录
        Dispatch tableOfContents = Dispatch.call(tablesOfContents, "Item", new Variant(1)).toDispatch();

        // 拿到整个目录的范围
        Dispatch tableOfContentsRange = Dispatch.get(tableOfContents, "Range").toDispatch();
//        // 取消选中,应该就是移动光标
        Dispatch format = Dispatch.get(tableOfContentsRange, "ParagraphFormat").toDispatch();
//        // 设置段落格式为首行缩进2个字符
        Dispatch.put(format, "CharacterUnitLeftIndent", new Variant(1));
        
        return true;
    }
    
    /** *//**
     * 在最后1行前增加一行
     *    
     * @param tableIndex
     *                        word文档中的第N张表(从1开始)
     */
    public void addLastTableRow(int tableIndex) {
            // 所有表格
            Dispatch tables = Dispatch.get(doc, "Tables").toDispatch();
            // 要填充的表格
            Dispatch table = Dispatch.call(tables, "Item", new Variant(tableIndex))
                            .toDispatch();
            // 表格的所有行
            Dispatch rows = Dispatch.get(table, "Rows").toDispatch();
            Dispatch row = Dispatch.get(rows, "Last").toDispatch();
            Dispatch.call(rows, "Add", new Variant(row));
    } 
    
    /** *//**
     * 在指定的单元格里填写数据
     *    
     * @param tableIndex
     * @param cellRowIdx
     * @param cellColIdx
     * @param txt
     */
    public void putTxtToCell(int tableIndex, int cellRowIdx, int cellColIdx,
                    String txt) {
            // 所有表格
            Dispatch tables = Dispatch.get(doc, "Tables").toDispatch();
            // 要填充的表格
            Dispatch table = Dispatch.call(tables, "Item", new Variant(tableIndex))
                            .toDispatch();
            Dispatch cell = Dispatch.call(table, "Cell", new Variant(cellRowIdx),
                            new Variant(cellColIdx)).toDispatch();
            Dispatch.call(cell, "Select");
            Dispatch.put(selection, "Text", txt);
    } 
    
    public  void copyFile(File fromFile,File toFile) throws IOException{
        FileInputStream ins = new FileInputStream(fromFile);
        FileOutputStream out = new FileOutputStream(toFile);
        byte[] b = new byte[1024];
        int n=0;
        while((n=ins.read(b))!=-1){
            out.write(b, 0, n);
        }
        
        ins.close();
        out.close();
    }

}

3、插入附件。

第2点的代码实现了插入附件的功能,但有以下几点需要关注。以wps为例,录制宏,然后再通过java调用宏来实现插入附件。
首先,使用wps录制一个插入附件的宏,命名为“InsertCaseMess”,步骤如下。

录制宏

录制宏

点击“确定”按钮后,再进行“插入附件”操作。
录制宏

随便插入一个附件,然后“停止录制”。
录制宏

点击“VB编辑器”,修改“InsertCaseMess脚本”如下并保存。

Sub InsertCaseMess(filePath As String, fileName As String)
'
' InsertCaseMess Macro
' 宏由 lenovo 录制,时间: 2018/05/21
'

'
    Selection.InlineShapes.AddOLEObject fileName:=filePath, LinkToFile:=0, DisplayAsIcon:=-1, IconFileName:="C:\Users\lenovo\AppData\Local\Kingsoft\WPS Office\10.1.0.7245\office6\et.exe", IconIndex:=3, IconLabel:=fileName
End Sub

其次,在测试报告模板中定义“${insertCaseBugFile}”变量,通过replaceFile方法把变量替换为附件。

测试报告模板

m.replaceFile("${insertCaseBugFile}", caseBugFilePath, caseBugFileName);  //插入附件
    /** *//**
     *    
     * @param toFindText 要查找的字符串
     * @param imagePath 文件路径
     * @return
     */
    public boolean replaceFile(String toFindText, String insertFilePath,String fileName) {
            moveStart();  //从文件首位置开始
            if (!find(toFindText))
                    return false;
            System.out.println("文件路径:"+insertFilePath);
            System.out.println("文件名:"+fileName);
            Dispatch.call(word, "Run", new Variant("InsertCaseMess"),new Variant(insertFilePath),new Variant(fileName));
            return true;
    }

测试报告

生成报告小工具

生成报告路径
密码:ghtw49

参考资料

Java操作Microsoft Word之jacob
Jacob官网

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348

推荐阅读更多精彩内容