随笔记录:基于robotium安卓UI自动化测试框架

去年到今年年初 我们组做了安卓原生应用的自动化测试,是基于robotium编写测试用例。

但是直接写的robotium用例不太方便,而且执行很多用例的时候时长会出现进程冲突,而不能继续后续的执行。而且robotium本身没有报告统计,截图等功能。

基于上述原因我们写了一个含有调度功能的自动化测试框架:是我们自己开发的运行在PC端的(只支持windows),用于组织测试用例、自动重签名apk文件、以及操作模拟器、启动运行测试用例、用 例crash以及失败重跑、测试结果收集等功能的一个工具,通过简单的配置,既可以全自动全SDK版本回归运行robotium编写的测试用例。

1. 框架结构:

技术分享

2.  其中核心调度功能代码

package com.efunds.framework;

import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.IDevice;
import com.efunds.emulator.EmulatorHelper;
import com.efunds.emulator.JavaWindowsCommandUtil;
import com.efunds.testSuites.TestCase;
import com.efunds.testSuites.TestSuite;
import com.efunds.xml.XmlCom;


import org.apache.log4j.*;

public class ScheduleCenter {    
    //true-截图    false-不截图
    public static final boolean btakephoto = true; 
    
    // 全局变量 
    ExcelHelper excelHelp = new ExcelHelper();
    
    // 递归调用
    @SuppressWarnings("null")
    public void RunCenter() throws InterruptedException
    {
        // 1. 读配置文件, 循环取得案例
        InputStream inputStream = ReadConfig.class.getClassLoader()  
                .getResourceAsStream("config.properties");  
        Properties p = new Properties();  
        try {  
            p.load(inputStream);  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  

        String modelFilePath = p.get("modelFilePath").toString();
        String reportPath = p.get("reportPath").toString();
        String testSuiteXml = p.get("testSuiteXml").toString();
        XmlCom xmlCom = new XmlCom(testSuiteXml);
        ArrayList<TestSuite> tsList = xmlCom.GetTestSuiteesValue();
        System.out.println("包的总数量:" + tsList.size());
        //startEmulator();
        int writeLine = 1;
        
        //截图代码
        IDevice device = null;
        String sztakephotopath = null;                    
        if(btakephoto){
            AndroidDebugBridge.init(false); // 很重要
            device = getDevice(0);
            System.out.println("设备内存的地址:"+device.getFileListingService().getRoot());
            File file1 = new File("D:\\takesnapshot");
            if(!file1.exists() && !file1.isDirectory()){
//                System.out.println("文件夹不存在,创建文件夹");
                file1.mkdir();
            }
            
            sztakephotopath = "D:\\takesnapshot\\" + getcurtime(2)+"\\";
            File file2 = new File(sztakephotopath);
            if(!file2.exists() && !file2.isDirectory()){
//                System.out.println("文件夹不存在,创建文件夹");
                file2.mkdir();
            }                
        }
        
        Getlogcatinfo getinfothread;
        if(ScheduleCenter.btakephoto){
            getinfothread = new Getlogcatinfo(); 
            getinfothread.start();
        }
        
        // 测试suite
        for(int i =0 ; i <tsList.size(); i++ )
        {
            TestSuite ts = tsList.get(i);
            String pckName = ts.getPackageName();
            System.out.println("类中包含的用例数量:" + ts.getCaseCount());
//            startEmulator();
            ArrayList<TestCase> caseList = ts.getCaseList();
            // 执行suite 第一行  
            excelHelp.SetDataToExcel(pckName+"——suite名称", i * (caseList.size())+2, 1);
            
            for(int j =0 ; j <caseList.size(); j++ )
            {
                writeLine++;
                TestCase tc = caseList.get(j);   //测试用例列表
                RunCase rc = new RunCase(tc,pckName); //  执行用例
                System.out.println("当前用例名称:"+tc.getMethodName());
                System.out.println("当前执行类中第几个用例:"+String.valueOf(j+1));
                // 执行用例  用例名称    
//                excelHelp.SetDataToExcel(tc.getMethodName()+"——case名称", writeLine, 2);
                excelHelp.SetDataToExcel(tc.getMethodName(), writeLine, 2);
                excelHelp.SetDataToExcel(getcurtime(1), writeLine, 3);
                ArrayList<String> outStringArr = new ArrayList<String>(){};
                HashMap<String,String> mapA = new HashMap<String,String>();
                getinfothread.setthreadstart(device, excelHelp, sztakephotopath, writeLine);
                //添加失败重跑机制
                boolean retFlag = false;
                for(int k =0 ; k<3; k++){
                    retFlag = rc.run(outStringArr,mapA,writeLine,excelHelp,device,sztakephotopath);
                    
                    if(retFlag)
                        break;
                    else{
                        if(0 == k|| 1 == k)
                            outStringArr.clear();
                    }
                }
// 执行用例 结果记录
                if (retFlag)
                {
                    //excelHelp.SetDataToExcel("成功", i * (caseList.size())+j+1, 3);
                    excelHelp.SetDataToExcel("成功", writeLine, 4);
                }
                else
                {
                    excelHelp.SetDataToExcel("失败", writeLine, 4);
                }
                // 返回信息当中的时间和执行结果
                if (mapA.size() > 0)
                {
                    excelHelp.SetDataToExcel(mapA.get("timeStr").toString(), writeLine, 5);
//                    excelHelp.SetDataToExcel(mapA.get("errResult").toString(), writeLine, 6);
                }
                // 写执行结果进行Excel当中
                String szTemp[];
                String szgetstr;
                for(int m = 0 ; m < outStringArr.size() ; m++)
                {
                    szgetstr = outStringArr.get(m);
                    if(szgetstr.contains("java.lang.AssertionError")){
                        szTemp = szgetstr.split("java.lang.AssertionError: ");
                        excelHelp.SetDataToExcel(szTemp[1], writeLine, 6);
                    }
                    else if(szgetstr.contains("junit.framework.AssertionFailedError")){
                        szTemp = szgetstr.split("junit.framework.AssertionFailedError: ");
                        excelHelp.SetDataToExcel(szTemp[1], writeLine, 6);
                    }
                    
                    if (retFlag)
                        excelHelp.SetDataToExcel("", writeLine, 6);
                    
                    // 追加内容
                    excelHelp.SetDataExtendToExcel(outStringArr.get(m), writeLine, 7);
                }
            }
        }
        
        if(ScheduleCenter.btakephoto){
            getinfothread.stopthread();
        }
        AndroidDebugBridge.terminate();
        System.out.println("**************全部用例执行完成**************");
        
    }
    
    private static IDevice getDevice(int index) {
        IDevice device = null;
        AndroidDebugBridge bridge = AndroidDebugBridge.createBridge();
        waitDevicesList(bridge);
        IDevice devices[] = bridge.getDevices();
        if (devices.length < index) {
            // 没有检测到第index个设备
            System.err.print("没有检测到第" + index + "个设备");
        } else {
            device = devices[index];
        }
        return device;
    }

    private static void waitDevicesList(AndroidDebugBridge bridge) {
        int count = 0;
        while (bridge.hasInitialDeviceList() == false) {
            try {
                Thread.sleep(500);
                count++;
            } catch (InterruptedException e) {
            }
            if (count > 60) {
                System.err.print("等待获取设备超时");
                break;
            }
        }
    }
    
    private void startEmulator()
    {
        // 判断模拟器状态,并执行调度(目前就直接做个硬代码开发:  直接杀掉exe  然后直接启动模拟器)
        // 杀掉进程
        JavaWindowsCommandUtil.killExe("genymotion.exe");
        // 重启进程
        JavaWindowsCommandUtil.startTask("\"D:\\Program Files\\Genymobile\\Genymotion\\genymotion.exe");
        // 启动模拟器
        EmulatorHelper.StartEmulator();
        // 等待100秒
        try {
            //java.util.concurrent.TimeUnit.SECONDS.sleep(100);
            java.util.concurrent.TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    
    public String getcurtime(int nNum) {
        Calendar cal1 = Calendar.getInstance();
        TimeZone.setDefault(TimeZone.getTimeZone("GMT+8:00"));
        java.text.SimpleDateFormat sdf;
        if(nNum == 1)
            sdf = new SimpleDateFormat("kk:mm:ss");
        else
            sdf = new SimpleDateFormat("yyyy-MMdd-kkmm");
        
        return sdf.format(cal1.getTime());
    }
    

}

3.  实现原理

    3.1  自动重签名APK文件

    使用Robotium来测试应用的话,需要测试程序和被测程序的签名是一样的,但是一般被测程序都会有自己签名过,所以我们需要实现自动的删除被测程序以及测试程序的签名文件,然后使用统一的key对被测程序的测试程序进行签名。

    实现原理:

    3.1.1、删除apk文件中的META-INF文件夹。

    使用winrar工具的 winrar d ***.apk META-INF 命令可以实现在不解压apk文件的情况下删除压缩包内指定文件夹。

    所以这里需要先下载安装winrar工具,另外在电脑的环境变量中配置winrar的安装目录,保证在cmd命令中能够调用到winrar命令。

    (PS:这里原来是使用的java jar -xvf 命令先把apk解压缩,然后调用删除方法删除掉META-INF,最后使用jar -cvf压缩为apk文件,但是通过jar命令解压缩之后,重签名后的网易阅读apk会出现崩溃的情况,改成了通过winrar工具来实现)。

    3.1.2、使用JARSIGNER命令对apk文件使用统一的key进行重签名操作。

    3.1.3、使用zipalign命令对重签名后的文件进行优化。

    框架如何使用bat脚本:

   3.2.1、bat脚本和重签名的key在Orange的jar包中会保存一份。

    3.2.2、运行Orange时,复制bat文件、key文件以及测试应用和被测试应用到temp目录下。

    3.2.3、通过java方法的Process process = Runtime.getRuntime()。exec( "cmd.exe /c start " + path); 执行bat文件。

    3.2.4、bat文件会自动把当前目录下的apk文件全部都进行重签名,重签名的apk文件保存到指定目录下。

    3.3  自动创建、启动、删除模拟器以及安装apk功能。

    需要在不同sdk版本的模拟器上实现全自动切换运行测试用例,所以需要涉及到创建、启动、删除模拟器以及安装apk文件等功能。

    实现原理:

    所有这些操作都使用了android自带的android命令来实现的。

    3.3.1、创建模拟器 

    通过java中调用以上命令实现创建模拟器的操作,如何判断创建模拟器的是否成功,以及失败的话错误信息呢?

    Process process = Runtime.getRuntime()。exec(cmd);来执行上面的cmd命令行。

    然后通过process.getInputStream()和process.getErrorStream()来获取到相应的返回信息,如果有错误信息的话可以再getErrorStream中获取到。

    说明: 正确情况下getErrorStream能够获取到值的话则抛出异常说明命令执行是时候出错了,但是有些命令的话执行正常,但是getErrorStream也能获取到返回值,所以有些命令需要做特殊的处理。

    3.3.2、启动模拟器 emulator -avd Test -sdcard  。

    通过上面的命令启动模拟器。

    3.3.3、删除模拟器android delete avd -n Test。

    3.4、安装APK adb wait-for-device install -r *.apk。

    3.4.0  Crash以及失败用例重跑功能。

    平时我们使用Robotium编写完一些测试用例的时候经常是直接通过Eclipse运行多个测试用例,或者通过junit编写一个testsuite把需要运行的测试用例加入到testsuite中一次运行多个测试用例。

    但是在运行的过程中,我们会发现有时候应用会Crash,导致的结果是测试暂停,所有测试的结果都收集不到。

    另外在运行的时候因为模拟器的一些不稳定性,可能会存在某一次用例运行失败的情况,但是再次运行用例就恢复正常了。

    为了解决Crash的问题,以及失败用例重跑的问题,我们开发了运行在PC端框架。

    实现原理:

    3.4.1、运行测试用例的时候通过框架控制每次只运行一个测试用例,这样子如果这一个用例运行crash了或者失败的话,我可以再次启动应用重新运行,对其它用例不会有影响。

    首先通过Robotium编写的需要运行的测试用例需要配置在xml文件中,按照类似TestNG配置文件的格式来指定这次运行哪些测试用例。

    <!-- packageName is required-->

<classes name="suiteNames" timeout="300" maxcount="3" packageName="com.efund.jqb.testLogin">

     <class name="com.efund.jqb.testAddcreditcard.AddcreditcardHigh">

-     <methods>
        <include name="test5890" />
     </methods>
    </class>
  </classes>
-     这里要求配置的时候必须配置到具体的方法,我们运行的时候框架会解析这个xml文件,最后组合成以下的一些命令来循环运行测试用例,每次运行一个。

    adb shell am instrument -e class com.netease.mobile.autotest.testing.LoginTest#testLogin -w com.netease.mobile.autotest/com.zutubi.android.junitreport.JUnitReportTestRunner

    adb shell am instrument -e class com.netease.mobile.autotest.testing.LoginTest#testUnLogin -w com.netease.mobile.autotest/com.zutubi.android.junitreport.JUnitReportTestRunner

    3.4.2、如何判断用例是运行Crash的?

    我们还是通过Process process = Runtime.getRuntime()。exec(cmd);方法来运行第一步生成的adb命令,然后通过 process.getInputStream();获取到流的返回值,如果返回值中包含"shortMsg=Process crashed"字符的话,说明这个用例运行crash了,那我们使用重试机制。

    再次运行这个测试用例,如果连续3次都是crash的话,则把crash的信息也写入到返回值中,保证了用例运行Crash的话也能够收集的错误信息。

    3.4.3、如果判断用例运行失败?

    这里使用了JUnitReportTestRunner的一个开源的插件(),是重写了官方的InstrumentTestRunner,能够帮我们收集每个用例的执行情况。

    每次用例执行结束后,会在模拟器或者真机的指定位置下生成一个Junit-report.xml文件,我们通过adb pull 命令把这个文件保存到PC端,然后框架来解析这个文件,如果结果中包含failure或者error的话说明这个用例运行失败了,则重跑用例。

    如果不包含的话说明用例运行成功,则把刚才取出的xml文件存下来,后续所有用例运行完成整合为一个完整的xml结果文件。

    3.4.4、如何实现用例重跑?

    这个只需要实现一个方法的递归调用就可以,每次运行的时候会查看返回值以及解析xml的返回结果文件,如果存在异常则递归调用该方法,同时会定义一个运行次数的参数,如果大于这个次数的时候不管是够通过都会保存当前最后一次的运行结果。   

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。