还是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脚本,就基本成型了

原文地址:https://www.cnblogs.com/cologne/p/3911398.html