这是我入职新公司以来第一个相对来说比较成型的工具,虽然功能是那么的弱智,但是基本上我是抱着认真的态度来看待这个工具的开发
废话不多说,首先阐明一下这个工具的意图:
意图:起因是当时需要测试公司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脚本,就基本成型了