腾讯Android自动化测试实战
上QQ阅读APP看书,第一时间看更新

3.2.1 Robotium支持Native原理

1.获取控件原理

我们知道Android会为res目录下的所有资源分配ID,例如在布局xml文件中使用了android:id="@+id/example_id",那么在Android工程编译时就会在R.java中相应地为该布局控件分配一个int型的ID,在Android工程中就可以通过Activity、Context或View等对象调用findViewById(int id)方法引用相应布局中的控件。因此,在测试工程中,如果是在源码的情况下,测试工程可以引用被测工程的代码,也即可以直接获得被测工程中R.java中的ID,因此可以通过这种方式直接根据ID获取控件。Robotium中根据ID获取控件的实现即包含该方式,如代码清单3-5所示。

代码清单3-5 Getter.getView

        public View getView(int id, int index, int timeout){
            final Activity activity = activityUtils.getCurrentActivity(false);
            View viewToReturn = null;
            //如果index小于1,则直接通过Activity的findViewById查找
            if(index < 1){
                index = 0;
                viewToReturn = activity.findViewById(id);
            }
            if (viewToReturn ! = null) {
                return viewToReturn;
            }
            return waiter.waitForView(id, index, timeout);
        }

在getView(int id, int index, int timeout)方法中,先获取当前所在的Activity,然后直接通过findViewById(id)方法尝试获取控件,如果该方法能够正确获取,则直接返回;否则,使用waitForView(id, index, timeout)方法进一步等待控件的出现。

对于测试工程没有关联被测工程的情况,是无法直接通过R.id.example_id的形式获取控件的,此时一般调用getView(String id)方法,即通过String型ID获取。之所以可以通过String型ID获取控件,是因为Robotium中该方法使用了Resources.getIdentifier(String name, String defType, String defPackage)方法动态地将String型ID转换成了int型ID,如代码清单3-6所示。

代码清单3-6 Getter.getView(String id, int index)

        public View getView(String id, int index){
            View viewToReturn = null;
            Context targetContext = instrumentation.getTargetContext();
            String packageName = targetContext.getPackageName();
            //先将String类型的ID转换成int型的ID
              int viewId = targetContext.getResources().getIdentifier(id, "id", packageName);
              if(viewId ! = 0){
                  viewToReturn = getView(viewId, index, TIMEOUT);
              }
              //如果还未找到,则传入的ID可能是Android系统中的ID
              if(viewToReturn == null){
                  int  androidViewId  =  targetContext.getResources().getIdentifier(id,
          "id", "android");
                  if(androidViewId ! = 0){
                      viewToReturn = getView(androidViewId, index, TIMEOUT);
                  }
              }
              if(viewToReturn ! = null){
                  return viewToReturn;
              }
              return getView(viewId, index);
          }

因此,为了简化操作,我们完全可以统一使用getView(String id)方法来获取控件。

以上为根据ID获取控件的一种方式,另一种方式则是通过WindowManager获取所有View后再进行各种过滤封装。如代码清单3-7所示,在ViewFetcher中通过getAllViews方法获取所有的View,其中分别处理DecorView与nonDecorView。

代码清单3-7 ViewFetcher.getAllViews

        public ArrayList<View> getAllViews(boolean onlySufficientlyVisible) {
            //获取所有的DocorViews
            final View[] views = getWindowDecorViews();
            final ArrayList<View> allViews = new ArrayList<View>();
            final View[] nonDecorViews = getNonDecorViews(views);
            View view = null;
            if(nonDecorViews ! = null){
                for(int i = 0; i < nonDecorViews.length; i++){
                    view = nonDecorViews[i];
                    try {
                        addChildren(allViews, (ViewGroup)view, onlySufficientlyVisible);
                    } catch (Exception ignored) {}
                    if(view ! = null) allViews.add(view);
                }
            }
            if (views ! = null && views.length > 0) {
                view = getRecentDecorView(views);
                try {
                        addChildren(allViews, (ViewGroup)view, onlySufficientlyVisible);
                    } catch (Exception ignored) {}
                    if(view ! = null) allViews.add(view);
                }
                return allViews;
            }

如代码清单3-8所示,在getWindowDecorViews方法中通过使用反射获取Window-Manager中的mViews对象来获取所有DecorView,其中也可以看到对于Android系统版本大于19的处理是不同的。

代码清单3-8 ViewFetcher.getWindowDecorViews

        @SuppressWarnings("unchecked")
        public View[] getWindowDecorViews()
        {
            Field viewsField;
            Field instanceField;
            try {
        //通过反射获取WindowManagerGlobal或WindowManagerImpl中的mViews变量
                viewsField = windowManager.getDeclaredField("mViews");
        //通过反射获取WindowManagerGlobal或WindowManagerImpl中的WindowManager实例的变量
                instanceField = windowManager.getDeclaredField(windowManagerString);
                viewsField.setAccessible(true);
                instanceField.setAccessible(true);
                Object instance = instanceField.get(null);
                View[] result;
                if (android.os.Build.VERSION.SDK_INT >= 19) {
                    result = ((ArrayList<View>) viewsField.get(instance)).toArray(new View[0]);
                } else {
                    result = (View[]) viewsField.get(instance);
                }
                return result;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }

再看代码清单3-9中的WindowManagerString变量的来源,如代码清单3-9所示,WindowManagerString也同样地需要根据Android系统版本的不同而分别处理。

代码清单3-9 ViewFetcher.setWindowManagerString

        private void setWindowManagerString(){
                //不同的系统版本,WindowManager的变量名不同
                if (android.os.Build.VERSION.SDK_INT >= 17) {
                    windowManagerString = "sDefaultWindowManager";
                } else if(android.os.Build.VERSION.SDK_INT >= 13) {
                    windowManagerString = "sWindowManager";
                } else {
                    windowManagerString = "mWindowManager";
                }
            }

至此我们知道了Robotium中获取所有Views是通过反射机制实现的,而源码中的变量很可能根据版本的不同而改变,因此通过反射则往往需根据系统版本的不同而分别处理。所以,使用Robotium时最好使用开源项目中的最新版本,因为当有新的Android系统版本发布时,很可能Robotium也需要与时俱进地完善获取控件方式。

2.控件操作原理

Robotium获取控件后,调用clickOnView(View view)方法就可以完成点击操作,这个方法可以实现两大功能:

❑ 根据View获取了控件在屏幕中的坐标。

❑ 根据坐标发送了模拟的点击操作。

如代码清单3-10所示,由于View本身可以获取到在屏幕中的起始坐标与控件长宽,因此通过getLocationOnScreen获取起始坐标后,再加上1/2的长与宽,即可计算出控件的中心点在屏幕中的位置。

代码清单3-10 Clicker.getClickCoordinates

            private float[] getClickCoordinates(View view){
                sleeper.sleep(200);
                int[] xyLocation = new int[2];
                float[] xyToClick = new float[2];
                //获取view的坐标,xyLocation[0]为x坐标的值,xyLocation[1]为y坐标的值
                view.getLocationOnScreen(xyLocation);
 
                final int viewWidth = view.getWidth();
                final int viewHeight = view.getHeight();
                //xyLocation中的值为控件左上角的坐标,因此xyLocation[0]+宽长除2即为该控件在x轴的中
            心点,同样地计算在y轴的中心点
                final float x = xyLocation[0] + (viewWidth / 2.0f);
                float y = xyLocation[1] + (viewHeight / 2.0f);
                  xyToClick[0] = x;
                  xyToClick[1] = y;
                  return xyToClick;
              }

知道了需要点击的位置后,那么接下来发送模拟点击就可以了。Android中的模拟操作可以通过MotionEvent来实现,而MotionEvent主要有以下三种形式:

❑ MotionEvent.ACTION_DOWN:模拟对屏幕发送下按事件。

❑ MotionEvent.ACTION_UP:模拟对屏幕发送上抬事件。

❑ MotionEvent.ACTION_MOVE:模拟对屏幕发送移动事件。

Robotium中的点击屏幕方法即是通过MotionEvent实现的,如代码清单3-11所示,通过MotionEvent.obtain(long downTime, long eventTime, int action, float x, float y, int metaState)方法获取相应的event事件后,再通过Instrumentation的sendPointerSync(MotionEvent event)方法将event事件实际地在手机上模拟执行。

代码清单3-11 Clicker.clickOnScreen

            public void clickOnScreen(float x, float y, View view) {
                boolean successfull = false;
                int retry = 0;
                SecurityException ex = null;
 
                while(! successfull && retry < 20) {
                    long downTime = SystemClock.uptimeMillis();
                    long eventTime = SystemClock.uptimeMillis();
                    MotionEvent event = MotionEvent.obtain(downTime, eventTime,
                        MotionEvent.ACTION_DOWN, x, y, 0);
                    MotionEvent event2 = MotionEvent.obtain(downTime, eventTime,
                        MotionEvent.ACTION_UP, x, y, 0);
                    try{
                        //通过Instrumentation模拟发送下按操作
                        inst.sendPointerSync(event);
                        //通过Instrumentation模拟发送上抬操作,与下按操作结合,模拟完成了一个点击过程
                        inst.sendPointerSync(event2);
                        successfull = true;
                    }catch(SecurityException e){
                        ex = e;
                        dialogUtils.hideSoftKeyboard(null, false, true);
                        sleeper.sleep(MINI_WAIT);
                        retry++;
                        View identicalView = viewFetcher.getIdenticalView(view);
                        if(identicalView ! = null){
                            float[] xyToClick = getClickCoordinates(identicalView);
                            x = xyToClick[0];
                            y = xyToClick[1];
                        }
                    }
                }
            //如果点击失败,将抛出异常
                if(! successfull) {
                    Assert.fail("Click  at  ("+x+",  "+y+")  can  not  be  completed!  ("+(ex  ! =
            null ? ex.getClass().getName()+": "+ex.getMessage() : "null")+")");
                }
            }

结合getClickCoordinates(View view)与clickOnScreen(float x, float y, View view)方法就完成了clickOnView(View view)方法的核心实现。通过控制不同手势操作的时间顺序还可以模拟各种手势操作,例如先发送MotionEvent.ACTION_DOWN,一段时间后,再发送MotionEvent.ACTION_UP就模拟了长按操作。先发送MotionEvent.ACTION_DOWN,然后发送MotionEvent.ACTION_MOVE,最后发送MotionEvent.ACTION_UP就是滑动操作了。因此,结合MotionEvent的各种模拟事件也可以自行实现自定义的手势操作。