zoukankan      html  css  js  c++  java
  • 还是Robotium娱乐小工具,取名LikeMonkey(持续更新成果,纯属娱乐,请勿吐槽)

    这是我入职新公司以来第一个相对来说比较成型的工具,虽然功能是那么的弱智,但是基本上我是抱着认真的态度来看待这个工具的开发

    废话不多说,首先阐明一下这个工具的意图:

    意图:起因是当时需要测试公司APK的稳定性,开发建议使用Monkey,但是Monkey是有很多弊病的,比如加-p参数即使加了指定包名,也还是会有时跳出被测程序,跑到OS里去执行;还比如测试中经常会有需要模拟按键的操作,比如音量,HOME之类的,这些是我所不需要的,而恰恰公司4个APK中都有的左滑右滑貌似没有支持,所以萌生出了一个自己用robotium写一个类似于Monkey操作的脚本

    解释一下为什么我会选择使用坐标点击,而不是使用控件集来进行随机点击,我公司有一个业务逻辑很复杂,界面很乱的手机助手APK,起初我使用getCurrentViews()方法尝试过对控件进行筛选,然后随机点击,但是由于各种空指针,而且由于界面布局上控件过于繁杂,在获取上的效率非常之慢;但是这个方法在我公司中另一个界面比较规范简洁的APK上测试,确实会比坐标点击的有效率高很多,综合考虑通用性以及稳定性,操作性各个方面,最终我还是敲定使用坐标随机这种方式进行实现

    这篇博文我会持续更新,按照我当时开发工具的顺序进行讲解,其中涉及到一些android开发相关的东西,所以我会一点点把整个工具的开发思路,代码都顺序写下来,也让大伙方便理解和思考

    一、让Monkey跑起来

    原理:要实现Monkey操作其实特别简单,但是这里有一个可以扩展的地方,就是,我们怎么让脚本,可以适配各种屏幕尺寸呢,所以具体思路就是:我们要在点击之前,使用一个方法去获取到当前屏幕的宽和高,然后分别使用这个宽和高利用随机数函数生成随机值,然后进行随机坐标点击;还有一个问题,取得屏幕的宽高,是会将上方状态栏,也就是信号栏那一条的坐标算进去,点击那里可是会弹出通知中心的,那样我们的脚本不就挂了吗,所以我们还需要一个方法去计算状态栏的宽度,然后去计算,代码如下:

    public class BaihMonkey extends ActivityInstrumentationTestCase2 {
        public static String LAUNCHER_ACTIVITY_FULL_CLASSNAME="com.android.haoyouduo.StartupActivity" ;
        private static Class launcherActivityClass;
          DisplayMetrics ty=new DisplayMetrics();
        //静态加载将获取到的点击的MainActivity字符串读出来
    //    static{
    //        File file=new File("/mnt/sdcard", "activityName.txt");
    //        
    //        try {           
    //            BufferedReader  fileReader = new BufferedReader (new FileReader(file));
    //            String activityName = fileReader.readLine();
    //            System.out.println(activityName);
    //            LAUNCHER_ACTIVITY_FULL_CLASSNAME=activityName;            
    //            fileReader.close();
    //        } catch (FileNotFoundException e) {
    //            // TODO Auto-generated catch block
    //            e.printStackTrace();
    //        } catch (IOException e) {
    //            // TODO Auto-generated catch block
    //            e.printStackTrace();
    //        }
    //        
    //    }
    
      public BaihMonkey() throws ClassNotFoundException {
              super(Class.forName(LAUNCHER_ACTIVITY_FULL_CLASSNAME));
             }
    
      private Solo solo;
      String logtag="LikeMonkey_log";
    
      @Override
      protected void setUp() throws Exception {
              solo = new Solo(getInstrumentation(), getActivity());
             }
      
      public void testMonkey() throws InterruptedException{
              Thread.sleep(6000);
               while(true)
              {
                  Thread.sleep(2000);
                  //特殊操作随机触发机制
                  Random setindex=new Random();
                  int setId=setindex.nextInt(20);
                  Log.e(logtag, "特殊操作值:"+setId);
                  switch (setId) {
                case 2:
                    Log.e(logtag, "操作左滑动");
                    solo.scrollToSide(solo.LEFT, (float) 0.8);     //左滑动
                    break;                
                case 5:
                    Log.e(logtag, "操作右滑动");
                    solo.scrollToSide(solo.RIGHT, (float) 0.8);   //右滑动
                    break;                
                case 10:
                    Log.e(logtag, "操作返回");
                    solo.goBack();       //返回
                    break;
                }
                  int ClickX=createX();
                  int ClickY=createY();
                  Log.e("baih", "x="+ClickX);
                  Log.e("baih", "y="+ClickY);
                  //随机屏幕坐标点击机制(去除信号栏高度)
                  if(ClickX>=ty.widthPixels || ClickY>=ty.heightPixels)
                  {
                      continue;
                  }
                  else
                  {
                      Log.e(logtag, "点击坐标为:x="+ClickX+"  y="+ClickY);
                      solo.clickOnScreen(ClickX, ClickY);
                  }
              }
     }
    
    //获取屏幕X轴长度并计算X轴随机点
      public int createX(){
          solo.getCurrentActivity().getWindowManager().getDefaultDisplay().getMetrics(ty);
          int x1=ty.widthPixels;
          Random x=new Random();
          int Rxindex=x.nextInt(x1);
          int xIndex=Rxindex+10;
          return xIndex;      
    }
    
    //获取屏幕Y轴长度(去除信号栏高度)并计算Y轴随机点
      public int createY(){
          Rect frame=new Rect();
          solo.getCurrentActivity().getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);
          int statusBarHeight=frame.top;//计算顶部信号栏高度
          solo.getCurrentActivity().getWindowManager().getDefaultDisplay().getMetrics(ty);
          int y1=ty.heightPixels;
          Random y=new Random();
          int Ryindex=y.nextInt(y1);
          int yIndex=Ryindex+statusBarHeight+5;
          return yIndex;     
    } 
    
    @Override
      public void tearDown() throws Exception {
              solo.finishOpenedActivities();
             }
    }

    注释已经将各功能的实现写的很明白了,通过使用DisplayMetrics对象的widthPixels和heightPixels方法,我们可以得到当前设备的宽高(设备分辨率还需要考虑DPI的值,此处我没有考虑进去,因为还不知道分辨率和颗粒密度之间如何计算,这个后期准备研究下)

    二、让脚本封装成APK

    这个在我前一篇随笔里面有比较详细的记录,这里不再多说,各位可以自行去研究,或者在基础上改良

    三、Activity跳转了怎么办?

    在实际测试中发现,我们公司的一款手机应用市场APK,在下载完成一个应用后,会自动弹出系统的程序安装界面,在点击一个已安装的应用时,也会自动弹出系统的程序卸载界面,这样的Acticity切换会导致我的脚本因为活动进程不在被测程序中而挂掉,也就又回归到了2个月前我用appium写LikeMonkey的问题:怎么可以启动一个线程去实时监听Activity的切换,并且还不影响主线程(即操作线程)的执行,这个时候,我想到了android四大基本组件里的Service,关于service的概念,各位可以自行百度

    我在测试工具启动时,在界面onCreate中启动一个service,这个Service的onCreate中去另启一个线程循环去监听当前的Activity栈的最顶部Activity,如果检测到当前最顶部的Activity是系统的安装界面或者卸载界面,就startActivity唤醒我的被测程序,代码如下:

    //该类继承Service,实现实时监听
    public
    class StartService extends Service { public static String activityName; public boolean setWhile=true; @Override public IBinder onBind(Intent intent) { // TODO Auto-generated method stub return null; } public void onCreate(){ Log.e("baih", "进入了onCreate里面"); IntentFilter intent =new IntentFilter("android.intent.action.VIEW"); //intent.addAction(Intent.ACTION_VIEW); intent.setPriority(Integer.MAX_VALUE); Toast.makeText(getApplicationContext(), "service已启动", 3000).show(); Log.e("baih", "===================service已启动"); //从Activity栈中获取当前系统Activity列表 final ActivityManager ActivityList=(ActivityManager)getApplicationContext().getSystemService(ACTIVITY_SERVICE); //另启线程,完成监听安装界面弹出工作 new Thread(){ public void run(){ while(setWhile) { //从Activity列表中读取一个RunningTaskInfo List<RunningTaskInfo> acList=ActivityList.getRunningTasks(1); //得到第一个RunningTaskInfo RunningTaskInfo mTaskInfo; mTaskInfo=acList.get(0); //获取该RunningTaskInfo的ActivityName String name=mTaskInfo.topActivity.getClassName(); String setup="com.android.packageinstaller.PackageInstallerActivity"; String uninstall="com.android.packageinstaller.UninstallerActivity"; //判断获取到的ActivityName是否为系统的安装界面或者卸载界面 if(name.equals(setup) || name.equals(uninstall) ) { Log.e("baih", "已经跳转到安装/卸载界面,准备操作返回随乐游"); //从Acitivity列表中读取两个RunningTaskInfo List<RunningTaskInfo> ac1=ActivityList.getRunningTasks(2); //得到第二个RunningTaskInfo RunningTaskInfo ra1; ra1=ac1.get(1); //获取该RunningTaskInfo的ActivityName String name1=ra1.topActivity.getClassName(); ComponentName componentName = ra1.topActivity; //启动新Activity指向到被测程序 Intent intent = new Intent(); //intent.setComponent(componentName); intent.setClassName("com.stnts.suileyoo.gamecenter", "com.android.haoyouduo.StartupActivity"); intent.setAction(Intent.ACTION_MAIN); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); Log.e("baih", "===========操作返回"); Log.e("baih", "==========="+name1); } try { Thread.sleep(3000); } catch (InterruptedException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } } } }.start(); } public void onStart(){ Log.e("baih", "进入了onStart里面"); } public void onDestroy(){ setWhile=false; } }
    //这个类实现测试工具启动的Activity
    package test.Monkey;
    
    import java.io.IOException;
    
    import test.Monkey.R;
    import test.Monkey.*;
    import android.content.Intent;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.View;
    import android.view.View.OnClickListener;
    import android.widget.Button;
    import android.widget.Toast;
    
    public class Start  extends android.app.Activity{
    
    @Override
    
      public void onCreate(Bundle savedInstanceState)
     {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Button btn1=(Button) findViewById(R.id.startTest);
        btn1.setOnClickListener(my);
        //启动Service
        Intent serviceIntent =new Intent(this,StartService.class);
        startService(serviceIntent);
        LogOutput log=LogOutput.getInstance();
        log.startLog();
     }
    
    public void onDestroy(){
        //在关闭测试工具的时候关闭Service
        super.onDestroy();
        LogOutput log=LogOutput.getInstance();
        log.stopLog();
        Intent intent1=new Intent(this,StartService.class);
        Toast.makeText(getApplicationContext(), "service已关闭", 3000).show();
        stopService(intent1);
    }
    
     private OnClickListener my=new OnClickListener() {
        
        @Override
        public void onClick(View v) {
            // TODO Auto-generated method stub
            Log.e("baih", "====================================");
            //使用命令行启动测试脚本
            Runtime run=Runtime.getRuntime();
            try {
                run.exec("am instrument -w test.Monkey/test.Monkey.InstrumentationTestRunner");
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    };
    }

    四、怎么输出Log,怎么让开发人员debug

    monkey测试这类工作,基本都是不需要人员看护,自己进行脚本执行的,所以我们就面对一个问题,出了问题,没人看见怎么办,所以我们需要一个功能,可以在脚本运行的过程中,把程序执行的logcat输出到本地,这个位置的功能不多说,直接上代码:

    //这个类的作用是输出Log到手机存储空间根目录下
    public class LogOutput {
    
        private static final String TAG="Log";
        private String LOG_PATH;
        
        private SimpleDateFormat time=new SimpleDateFormat("yyyy-mm-dd-HH-mm-ss");
        
        private Process pro;
        private static LogOutput Logfile=null;
        
        private LogOutput(){
            init();
        }
        
        public static LogOutput getInstance(){
            if(Logfile==null)
            {
                Logfile=new LogOutput();
            }
            return Logfile;
        }
        
        public void startLog(){
            createLog();
        }
        
        public void stopLog(){
            if(pro!=null)
            {
                pro.destroy();
            }
        }
        
        private void init(){
            LOG_PATH=Environment.getExternalStorageDirectory().getAbsolutePath();
            createLogDir();
            Log.e(TAG, "Log onCreate");
            
        }
        
        public void createLog(){
            List<String> commandlist=new ArrayList<String>();
            commandlist.add("logcat");
            commandlist.add("-f");
            commandlist.add(getLogPath());
            commandlist.add("-s");
            commandlist.add("*:E");
            commandlist.add("-v");
            commandlist.add("time");
            
            try {
                pro=Runtime.getRuntime().exec(commandlist.toArray(new String[commandlist.size()]));
            } catch (Exception e) {
                // TODO: handle exception
                Log.e(TAG,e.getMessage(), e); 
            }
    
            
        }
        
        public String getLogPath(){
            createLogDir();
            String logFileName=time.format(new Date())+"_suileyoo_LikeMonkey.log";
            return LOG_PATH+File.separator+logFileName;
        }
        
        private void createLogDir(){
            File file;
            boolean OK;
            if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
            {
                file=new File(LOG_PATH);
                if(!file.isDirectory()){
                    OK=file.mkdirs();
                    if(!OK)
                    {
                        return;
                    }
                }
            }
        }
    }

    编写好Log输出类时,我们只需要在程序启动时调用开始打印log的函数

       LogOutput log=LogOutput.getInstance();
        log.startLog();

    在程序关闭时调用停止打印log的函数

        LogOutput log=LogOutput.getInstance();
        log.stopLog();

    并且在工程的manifest文件中添加读取系统log的权限

        <uses-permission android:name="android.permission.READ_LOGS"/>

    如此,一个可以适配各种屏幕尺寸,可以输出log到本地的Monkey脚本,就基本成型了

  • 相关阅读:
    setTimeout 理解
    Git 使用规范流程
    JavaScript异步编程 ( 一 )
    javaScript模块化一
    javascript 知识点坑
    javaScript闭包
    函数式编程
    JavaScript的68个技巧一
    MySql 隐式转换
    MySQL优化
  • 原文地址:https://www.cnblogs.com/cologne/p/3911398.html
Copyright © 2011-2022 走看看