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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM