Robotium源码分析之Instrumentation进阶

在分析Robotium的运行原理之前,我们有必要先搞清楚Instrumentation的一些相关知识点,因为Robotium就是基于Instrumentation而开发出来的一套自动化测试框架。鉴于之前本人已经转载和编写了Instrumentation的一些文章,所以建议大家如果没有看过的还是翻看下先对Instrumentation有个基本的理解。然后带着疑问再来看这篇文章看是否能帮上自己。

既然是分析Instrumentation,那么我们必须要先看下Instrumentation 这个类的类图,直接网上截获,就不花时间另外去画了,但请注意网上该图是比较老的,一些新的注入事件的方法是没有加进去的,注意红色部分:

开始分析之前我们要搞清楚Instrumentation的几点

1. Instrumentation测试脚本和目标app在同一个进程中运行

如官方描述的“instrumentation can load both a test package and the application under test into the same process. Since the application components and their tests are in the same process, the tests can invoke methods in the components, and modify and examine fields
in the components.“翻译过来就是“Instrumentation可以把测试包和目标测试应用加载到同一个进程中运行。既然各个控件和测试代码都运行在同一个进程中了,测试代码当然就可以调用这些控件的方法了,同时修改和验证这些控件的一些项也不在话下了。“

这个从ddms的线程输出可以证明,通过"am instrumentation -w com.example.android.notepad/android.test.InstrumentationTestRunner" 运行的输出如下:

我们对比不是通过"am instrumentation"命令启动的notepad应用的线程输出:

可以看到是没有“Instr:android.test.InstrumentationTestRunner这个线程的。这个线程和Instrumentation又是什么关系呢?

/*     */ public class InstrumentationTestRunner
/*     */   extends Instrumentation
/*     */   implements TestSuiteProvider
/*     */ {
                ...
            }

从它的类定义我们可以看到它是从我们的Instrumentation类继承下来的。其实从它的名字我们就大概可以想像到它是扮演什么角色的,参照我们之前对UiAutomator的源码分析《UIAutomator源码分析之启动和运行》,InstrumentationTestRunner扮演的角色类似于当中的UiAutomatorTestRunner类,都是通过解析获取和建立目标测试用例和测试集然后知道测试的运行。

大家先看下它的onCreate方法的官方定义:“Called when the instrumentation is starting, before any application code has been loaded.“。翻译过来就是”这个方法是在Instrumentation启动过程中,在目标测试代码被装载进内存运行之前进行调用“

我们可以看下这个方法的实现,看它是如何启动刚才的那个InstrumetnationTestRunner线程的:

/*     */   public void onCreate(Bundle arguments)
/*     */   {
/* 303 */     super.onCreate(arguments);
                   ...
/* 343 */     TestSuiteBuilder testSuiteBuilder = new TestSuiteBuilder(getClass().getName(), getTargetContext().getClassLoader());
/*     */
/*     */
/* 346 */     if (testSizePredicate != null) {
/* 347 */       testSuiteBuilder.addRequirements(new Predicate[] { testSizePredicate });
/*     */     }
/* 349 */     if (testAnnotationPredicate != null) {
/* 350 */       testSuiteBuilder.addRequirements(new Predicate[] { testAnnotationPredicate });
/*     */     }
/* 352 */     if (testNotAnnotationPredicate != null) {
/* 353 */       testSuiteBuilder.addRequirements(new Predicate[] { testNotAnnotationPredicate });
/*     */     }
/*     */
/* 356 */     if (testClassesArg == null) {
                ...
/*     */     } else {
/* 370 */       parseTestClasses(testClassesArg, testSuiteBuilder);
/*     */     }
/*     */
/* 373 */     testSuiteBuilder.addRequirements(getBuilderRequirements());
/*     */
/* 375 */     this.mTestRunner = getAndroidTestRunner();
/* 376 */     this.mTestRunner.setContext(getTargetContext());
/* 377 */     this.mTestRunner.setInstrumentation(this);
/* 378 */     this.mTestRunner.setSkipExecution(logOnly);
/* 379 */     this.mTestRunner.setTest(testSuiteBuilder.build());
/* 380 */     this.mTestCount = this.mTestRunner.getTestCases().size();
/* 381 */     if (this.mSuiteAssignmentMode) {
/* 382 */       this.mTestRunner.addTestListener(new SuiteAssignmentPrinter());
/*     */     } else {
/* 384 */       WatcherResultPrinter resultPrinter = new WatcherResultPrinter(this.mTestCount);
/* 385 */       this.mTestRunner.addTestListener(new TestPrinter("TestRunner", false));
/* 386 */       this.mTestRunner.addTestListener(resultPrinter);
/* 387 */       this.mTestRunner.setPerformanceResultsWriter(resultPrinter);
/*     */     }
/* 389 */     start();
/*     */   }

从中我们可以看到这个方法开始就是如上面所说的类似UiAutomatorTestRunner一样去获取解析对应测试包里面的测试集和测试用例,这个在这个章节不是重点,重点是最后面的start()这个方法的调用。这个方法最终调用的是父类Instrumentation的start()方法,我们看下这个方法的官方解析"Create and start a new thread in which to run instrumentation.“翻译过来就是”创建一个新的运行Instrumentation(测试用例)的线程":

/*      */   public void start()
/*      */   {
/*  122 */     if (this.mRunner != null) {
/*  123 */       throw new RuntimeException("Instrumentation already started");
/*      */     }
/*  125 */     this.mRunner = new InstrumentationThread("Instr: " + getClass().getName());
/*  126 */     this.mRunner.start();
/*      */   }

在第125行我们很明显知道新的线程名就叫做"Instr:android.test.InstrumentationTestRunner",因为这个方法是从子类android.test.InstrumentationTestRunner中传进来的,所以getClass().getName()方法获得的就是子类的名字。

我们继续看这个线程是如何建立起来的,继续进入InstrumentationThread这个Instrumentation的内部类:

/*      */   private final class InstrumentationThread
/*      */     extends Thread {
/* 1689 */     public InstrumentationThread(String name) { super(); }
/*      */
/*      */     public void run() {
/*      */       try {
/* 1693 */         Process.setThreadPriority(-8);
/*      */       } catch (RuntimeException e) {
/* 1695 */         Log.w("Instrumentation", "Exception setting priority of instrumentation thread " + Process.myTid(), e);
/*      */       }
/*      */
/* 1698 */       if (Instrumentation.this.mAutomaticPerformanceSnapshots) {
/* 1699 */         Instrumentation.this.startPerformanceSnapshot();
/*      */       }
/* 1701 */       Instrumentation.this.onStart();
/*      */     }
/*      */   }

从最前面的定义可看到该类是继承与Thread对象的,所以后面提供个重写的run方法来代表该线程的运行入口完成一个Thread线程的完整实现。

1701行,Instrumentation.this值得就是子类InstrumentationTestRunner的实例,那么它的onStart方法又做了什么事情呢?

    /**
     * Initialize the current thread as a looper.
     * <p/>
     * Exposed for unit testing.
     */
    void prepareLooper() {
        Looper.prepare();
    }

    @Override
    public void onStart() {
        prepareLooper();

        if (mJustCount) {
            mResults.putString(Instrumentation.REPORT_KEY_IDENTIFIER, REPORT_VALUE_ID);
            mResults.putInt(REPORT_KEY_NUM_TOTAL, mTestCount);
            finish(Activity.RESULT_OK, mResults);
        } else {
            if (mDebug) {
                Debug.waitForDebugger();
            }

            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            PrintStream writer = new PrintStream(byteArrayOutputStream);
            try {
                StringResultPrinter resultPrinter = new StringResultPrinter(writer);

                mTestRunner.addTestListener(resultPrinter);

                long startTime = System.currentTimeMillis();
                mTestRunner.runTest();
                long runTime = System.currentTimeMillis() - startTime;

                resultPrinter.printResult(mTestRunner.getTestResult(), runTime);
            } catch (Throwable t) {
                // catch all exceptions so a more verbose error message can be outputted
                writer.println(String.format("Test run aborted due to unexpected exception: %s",
                                t.getMessage()));
                t.printStackTrace(writer);
            } finally {
                mResults.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
                        String.format("\nTest results for %s=%s",
                        mTestRunner.getTestClassName(),
                        byteArrayOutputStream.toString()));

                if (mCoverage) {
                    generateCoverageReport();
                }
                writer.close();

                finish(Activity.RESULT_OK, mResults);
            }
        }
    }

该方法一开始就为InstrumentationTestRunner线程建立一个looper消息队列,至于looper是怎么回事,大家如果不清的请查看网络的解析。Looper是用于给一个线程添加一个消息队列(MessageQueue),并且循环等待,当有消息时会唤起线程来处理消息的一个工具,直到线程结束为止。通常情况下不会用到Looper,因为对于Activity,Service等系统组件,Frameworks已经为我们初始化好了线程(俗称的UI线程或主线程),在其内含有一个Looper,和由Looper创建的消息队列,所以主线程会一直运行,处理用户事件,直到某些事件(BACK)退出。

如果,我们需要新建一个线程,并且这个线程要能够循环处理其他线程发来的消息事件,或者需要长期与其他线程进行复杂的交互,这时就需要用到Looper来给线程建立消息队列。

建立好消息队列后往下的重点就是调用AndroidTestRunner的runTest方法开始测试用例的执行了:

    public void runTest(TestResult testResult) {
        mTestResult = testResult;

        for (TestListener testListener : mTestListeners) {
            mTestResult.addListener(testListener);
        }

        Context testContext = mInstrumentation == null ? mContext : mInstrumentation.getContext();
        for (TestCase testCase : mTestCases) {
            setContextIfAndroidTestCase(testCase, mContext, testContext);
            setInstrumentationIfInstrumentationTestCase(testCase, mInstrumentation);
            setPerformanceWriterIfPerformanceCollectorTestCase(testCase, mPerfWriter);
            testCase.run(mTestResult);
        }
    }

大概做法就是对所有收集到的测试集进行一个for循环然后取出每个测试用例在junit.Framework.Testcase环境下进行运行了。这里就不往下研究junit框架是怎么回事了。

总结以上的分析,android.test.InstrumentationTestRunner会在目标应用代码运行之前调用onCreate方法来建立一个新的线程并准备后消息队列,然后会开始基于Instrumentation的测试集的测试。

2. runOnUiThread和runOnMainSync的区别

既然app的主线程和instrumetnaiton测试用例脚本线程是运行在同一个进程中的,我们脑袋中应该就会立刻闪现以下有关UiThread和子线程的两点限制:

  • 子线程是可以直接获取主线程UiThread的控件以及内容的
  • 子线程是不能直接操作主线程UiThread的控件以及内容的

根据网上的文章《Android中UI线程与后台线程交互设计的5种方法》的描述,android提供了以下几种方法,用于实现后台线程与UI线程的交互。

  • 1、handler
  • 2、Activity.runOnUIThread(Runnable)
  • 3、View.Post(Runnable)
  • 4、View.PostDelayed(Runnabe,long)
  • 5、AsyncTask

而Instrumentation类又提供了一个runOnMainSync的方法,这和上面的Activity提供的runOnUiThread方法从名字上比较接近,那么我们这里就对比下这两种方法有什么区别。

我们先看下Activity的runOnUIThread方法:

    /**
     * Runs the specified action on the UI thread. If the current thread is the UI
     * thread, then the action is executed immediately. If the current thread is
     * not the UI thread, the action is posted to the event queue of the UI thread.
     *
     * @param action the action to run on the UI thread
     */
    public final void runOnUiThread(Runnable action) {
        if (Thread.currentThread() != mUiThread) {
            mHandler.post(action);
        } else {
            action.run();
        }
    }

其代码的功能和对应的描述一致:

  • 如果这个方法不是在运行Activity的主线程UiThread上被调用的,也就是在子线程上调用的,那么把action提交到主线程的Main Looper消息队列中排队然后返回
  • 如果这个方法是在运行Activity的主线程UiThread上被调用的,那么不需要进入Main Looper队列排队,直接调用执行

你看,这多偏心,正室就是权力大,二房就是差点,特别是二房比较多的时候,你慢慢排吧。不过我们的Main Looper还算好了,在我们现实中更多是反过来了,搞了个二房三房后把正室给休掉了。

这里就不往外扯了,这里我们主要关心子线程的情况,既然是把action提交到Main Looper进行排队,那么必然是异步的了,否则消息队列的存在就没有意思了,所以这里我们要在脑袋总记得runOnUiThread是异步的。

我们再看Instrumentation的runOnMainSync方法:

/*      */   public void runOnMainSync(Runnable runner)
/*      */   {
/*  344 */     validateNotAppThread();
/*  345 */     SyncRunnable sr = new SyncRunnable(runner);
/*  346 */     this.mThread.getHandler().post(sr);
/*  347 */     sr.waitForComplete();
/*      */   }

这里也是从再从主线程获得Main Looper的Handler后往Main Looper消息队列中提交action,但人家提交完之后还会等待该action线程的执行完毕才会退出这个函数,所以两个方法的区别就是:Activity的runOnUiThread是异步执行的,Instrumentation的runOnMainSync是同步执行的。runOnMainSync又是怎么实现这一点的呢?这个我们就要看Instrumetnation的内部类SyncRunnable了:

/*      */   private static final class SyncRunnable implements Runnable {
/*      */     private final Runnable mTarget;
/*      */     private boolean mComplete;
/*      */
/* 1715 */     public SyncRunnable(Runnable target) { this.mTarget = target; }
/*      */
/*      */     public void run()
/*      */     {
/* 1719 */       this.mTarget.run();
/* 1720 */       synchronized (this) {
/* 1721 */         this.mComplete = true;
/* 1722 */         notifyAll();
/*      */       }
/*      */     }
/*      */
/*      */     public void waitForComplete() {
/* 1727 */       synchronized (this) {
/* 1728 */         while (!this.mComplete) {
/*      */           try {
/* 1730 */             wait();
/*      */           }
/*      */           catch (InterruptedException e) {}
/*      */         }
/*      */       }
/*      */     }
/*      */   }

它也是从runnable线程类继承下来的。在run方法的1720到1722行我们可以看到,该运行在Main UiThread的方法在跑完后会把Instrumentation实例的mComplete变量设置成true,而runOnMainSync最后调用的运行在子线程中的waitForComplete方法会一直等待这个mComplete变量变成true才会返回,也就是说一直等待主线程的调用完成才会返回,那么到了这里就很清楚runOnMainSync是如何通过SyncRunnable这个内部类实现同步的了。

这里还有必要提一提的是runOnMainSync方法调用的第一个函数validateNotAppThread,其实它做的事情就是去查看下当前线程的Looper是不是主线程的Main Looper,如果是的话就直接抛出异常,代表runOnMainSynce这个方法是不应该在主线程调用的;如果不是的话就什么都不干继续往下跑。

/*      */   private final void validateNotAppThread()
/*      */   {
/* 1650 */     if (Looper.myLooper() == Looper.getMainLooper()) {
/* 1651 */       throw new RuntimeException("This method can not be called from the main application thread");
/*      */     }
/*      */   }

3. Instrumentation注入事件统一方式-- InputManager

从开始的类图,我们可以看到Instrumentation事件相关的方法如下:


Method


Description


Comment


Key Events


sendKeySync


发送一个键盘事件,注意同一时间只有一个action,或者是按下,或者是弹起,所有下面其他key相关的事件注入都是以这个方法为基础的


sendKeyDownUpSync


基于sendKeySync发送一个按键的按下和弹起两个事件


sendCharacterSync


发送键盘上的一个字符,完整的过程包括一个按下和弹起事件


sendStringSync


往应用发送一串字符串


Tackball Event


sendTrackballEventSync


发送轨迹球事件。个人没有用过,应该是像黑莓的那种轨迹球吧


Pointer Event


sendPointerSync


发送点击事件

那么我们根据不同的事件类型看下它们注入事件的方式是如何的,我们先看按键事件类型,因为其他的按键事件都是最终调用sendKeySync,所以我们就看这方法就可以了:

    /**
     * Send a key event to the currently focused window/view and wait for it to
     * be processed.  Finished at some point after the recipient has returned
     * from its event processing, though it may <em>not</em> have completely
     * finished reacting from the event -- for example, if it needs to update
     * its display as a result, it may still be in the process of doing that.
     *
     * @param event The event to send to the current focus.
     */
    public void sendKeySync(KeyEvent event) {
        validateNotAppThread();

        long downTime = event.getDownTime();
        long eventTime = event.getEventTime();
        int action = event.getAction();
        int code = event.getKeyCode();
        int repeatCount = event.getRepeatCount();
        int metaState = event.getMetaState();
        int deviceId = event.getDeviceId();
        int scancode = event.getScanCode();
        int source = event.getSource();
        int flags = event.getFlags();
        if (source == InputDevice.SOURCE_UNKNOWN) {
            source = InputDevice.SOURCE_KEYBOARD;
        }
        if (eventTime == 0) {
            eventTime = SystemClock.uptimeMillis();
        }
        if (downTime == 0) {
            downTime = eventTime;
        }
        KeyEvent newEvent = new KeyEvent(downTime, eventTime, action, code, repeatCount, metaState,
                deviceId, scancode, flags | KeyEvent.FLAG_FROM_SYSTEM, source);
        InputManager.getInstance().injectInputEvent(newEvent,
                InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

这个就很明显了,用的就是InputManager的事件注入方式,如果大家不清楚的请查看本人之前翻译的《Monkey源码分析番外篇之Android注入事件的三种方法比较》。

下一个我们就看下轨迹球相关事件注入,用到的同样是InputManager的事件注入方式:

    /**
     * Dispatch a trackball event. Finished at some point after the recipient has
     * returned from its event processing, though it may <em>not</em> have
     * completely finished reacting from the event -- for example, if it needs
     * to update its display as a result, it may still be in the process of
     * doing that.
     *
     * @param event A motion event describing the trackball action.  (As noted in
     * {@link MotionEvent#obtain(long, long, int, float, float, int)}, be sure to use
     * {@link SystemClock#uptimeMillis()} as the timebase.
     */
    public void sendTrackballEventSync(MotionEvent event) {
        validateNotAppThread();
        if ((event.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) == 0) {
            event.setSource(InputDevice.SOURCE_TRACKBALL);
        }
        InputManager.getInstance().injectInputEvent(event,
                InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

最后我们看下点击事件,同样,使用的也是无一例外的InputManager的事件注入方式:

    /**
     * Dispatch a pointer event. Finished at some point after the recipient has
     * returned from its event processing, though it may <em>not</em> have
     * completely finished reacting from the event -- for example, if it needs
     * to update its display as a result, it may still be in the process of
     * doing that.
     *
     * @param event A motion event describing the pointer action.  (As noted in
     * {@link MotionEvent#obtain(long, long, int, float, float, int)}, be sure to use
     * {@link SystemClock#uptimeMillis()} as the timebase.
     */
    public void sendPointerSync(MotionEvent event) {
        validateNotAppThread();
        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 0) {
            event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
        }
        InputManager.getInstance().injectInputEvent(event,
                InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

4. 文本输入的两种方式

从上面两节我们可以看到,在Instrumetnaiton框架中,我们操作目标应用控件的方式比如输入文本有两种方式,这些都可以从本文最后的实例中看到:

  • 通过runOnMainSync调用直接把文本修改的动作运行在UiThread这个主线程中
  • 通过注入事件模拟用户通过按键输入字符

那么这两种方式有什么区别呢:

  • runOnMainSync: 直接在主线程中修改控件的文本,所以不需要通过键盘驱动,也就是说不需要调出任何的键盘。这样的好处是效率以及不需要担心中英文输入的问题
  • 事件注入方式:模拟用户的输入,所以肯定会调出键盘,这样在中文等非默认英文输入的情况下容易碰到问题,毕竟中文字串也是通过拼音组合而成,那么拼音出来后选择哪个出来的组合就成问题了。比如输入"changan"可能出来的是"长安“,”长按“等组合,那么哪个是我们想要的呢?

5. 跨进程和安全问题

众所周知Instrumentation和基于Instrumentation的Robotium对跨进程跨应用的支持是不支持的(其实Robotium从android 4.3之后开始支持UiAutomation框架,理应可以支持跨应用的,这个往后文章我们会进行分析).

但是从上面第3节的分析我们看到它使用的是InputManager的事件注入方式,大家翻回本人之前的文章:《monkey源码分析之事件注入方法变化》,MonkeyRunner通过Monkey注入事件使用的也是InputManager方式啊。那么为什么基于Monkey的MonkeyRunner就能跨进程跨应用,基于Instrumentation的Robotium就不能跨应用呢?我个人认为有以下几点原因:

  • 首先,一个应用要使用Instrumentation进行测试的话首先必须要在其Manifest.xml做相应的配置,那么一个应用真正发布的时候肯定是把这些配置给去掉的,所以Instrumentation或基于Instrumentation的Robotium肯定是不能对其他应用进行操作的,不然它就可以随意的打开一个流量消耗大户应用来消耗你的流量了。
  • 其次,既然大家里面都用了InputManager进行事件注入,那么为什么Monkey可以跨应用而Robotium不行呢?你Robotium也可以绕开Instrumentation框架直接调用InputManager来做事情啊!这里就要说到INJECT_EVENTS这个系统权限了,大家请参考《Monkey源码分析番外篇之Android注入事件的三种方法比较》。人家Monkey是google亲生的,获取个INJECT_EVENTS系统权限还不容易吗,你Robotium跟我什么关系,我google凭什么给你这些第三方应用开放这个权限呢?鬼知道给你开放这个权限后会不会搞破坏啊!所以你还是待在配置了Mainifest.xml的你的目标测试应用中做事情吧,别到处跑了

6.所谓钩子

都说Android instrumentation是Android系统里面的一套控制方法或者”钩子“。这些钩子可以在正常的生命周期(正常是由操作系统控制的)之外控制Android控件的运行,其实指的就是Instrumentation类提供的各种流程控制方法,下表展示了部分方法的对应关系


Method


Control by User(Instrumentation)


Control by OS


Comment


onCreate


callActivityOnCreate


onCreate


onDestroy


callActivityOnDestroy


onDestroy


onStart


callActivityOnStart


onStarty


默认来说,一个Activity的创建和消亡都是由操作系统来控制调用的,用户是没办法控制的。比如用户是没法直接调用onCreate方法来在activity启动的时候做一些初始化动作。但是Instrumentation提供了对应的callActivityOnCreate方法来允许用户控制对onCreate方法的调用,所以这里本来属于操作系统的控制权就移交给用户了。

    /**
     * Perform calling of an activity's {@link Activity#onCreate}
     * method.  The default implementation simply calls through to that method.
     *
     * @param activity The activity being created.
     * @param icicle The previously frozen state (or null) to pass through to
     *               onCreate().
     */
    public void callActivityOnCreate(Activity activity, Bundle icicle) {
        ...
        activity.performCreate(icicle);
        ...
}

从代码可以看到它做的事情也就是直接调用Activity类的performCreate方法:

   final void performCreate(Bundle icicle) {
        onCreate(icicle);
        mVisibleFromClient = !mWindow.getWindowStyle().getBoolean(
                com.android.internal.R.styleable.Window_windowNoDisplay, false);
        

而performCreate方法最终调用的就是onCreate方法。注意performCreate这个方法是属于Internal API,它不是public出去给外部使用的.

所以这里就好比Instrumentation勾住了本应该系统调用的onCreate方法,然后由用户自己来控制勾住的这个方法什么时候执行。

7. Instrumentation跨应用的考虑

安卓从Android4.3开始引进了UiAutomation框架来支持通过Accessibility API来实现针对用户界面UI层面的功能测试,Instrumentation也提供了相应的接口来获得UiAutomation实例:

    /**
     * Gets the {@link UiAutomation} instance.
     * <p>
     * <strong>Note:</strong> The APIs exposed via the returned {@link UiAutomation}
     * work across application boundaries while the APIs exposed by the instrumentation
     * do not. For example, {@link Instrumentation#sendPointerSync(MotionEvent)} will
     * not allow you to inject the event in an app different from the instrumentation
     * target, while {@link UiAutomation#injectInputEvent(android.view.InputEvent, boolean)}
     * will work regardless of the current application.
     * </p>
     * <p>
     * A typical test case should be using either the {@link UiAutomation} or
     * {@link Instrumentation} APIs. Using both APIs at the same time is not
     * a mistake by itself but a client has to be aware of the APIs limitations.
     * </p>
     * @return The UI automation instance.
     *
     * @see UiAutomation
     */
    public UiAutomation getUiAutomation() {
        if (mUiAutomationConnection != null) {
            if (mUiAutomation == null) {
                mUiAutomation = new UiAutomation(getTargetContext().getMainLooper(),
                        mUiAutomationConnection);
                mUiAutomation.connect();
            }
            return mUiAutomation;
        }
        return null;
    }

关于UiAutomation更多的描述请查看本人上一个系列关于UiAutomator源码分析的文章,这里列出来方便大家浏览:

从上面的一系列文章可以看到UiAutomator运用UiAutomation框架进行UI自动化测试是做了很多工作,进行了很多高层的封装来方便用户使用的。而Robotium仅仅是引入了获取UiAutomation的实例这个api来暴露给用户使用,一个方面,当然没有高层的封装提供了很多自由,但是也是这些自由让你想快速开发脚本无所适从!Robotium现阶段(5.2.1)对比UiAutomator或者Appium在使用UiAutomation来测试UI就好比,Robotium相当于一个原始社会的人自由的披着件兽皮两手空空的在原始森林中自由游猎,碰到猛兽可以自由的选择工具随意组装来进行猎杀,但很有可能工具没有组装好怪兽却先把你给吃了;UiAutomator相当于一个现代的人全副武装AK47的在原始森林根据GPS定位如履薄冰的向目标猎物靠近来猎杀猎物。两者都是使用最终由化学元素组成的工具来猎杀猎物,但早已高层封装好的ak47和你临时抱佛脚去凭空组建个弓弩从效率上又怎么能比呢。

所以这里我怀疑Robotium可能就提供这个接口就算了,不会再做上层的封装,因为UiAutomator已经做了,人家UiAutomator是google自家的,什么时候又改动人家最清楚,你怎么跟得住人家呢?况且它从Instrumentation的不可跨进程到提供了一个跨进程的突破口,也给了确实需要跨进程调用的用户的一个突破口,不提供太多的封装还能美其名曰“自由”了。注意,仅仅是我自己的猜想了,如果Robotium往后真对UiAutomation做高成封装的话就当我发神经得了。

8.Instrumentation使用例子

/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package come.example.android.notepad.test;

import android.test.ActivityInstrumentationTestCase2;

import com.example.android.notepad.NotesList;
import com.example.android.notepad.NoteEditor;
import com.example.android.notepad.NotesList;
import com.example.android.notepad.R;

import android.app.Activity;
import android.app.Instrumentation;
import android.app.Instrumentation.ActivityMonitor;
import android.content.Intent;
import android.os.SystemClock;
import android.test.InstrumentationTestCase;
import android.view.KeyEvent;
import android.widget.TextView;

/**
 * Make sure that the main launcher activity opens up properly, which will be
 * verified by {@link #testActivityTestCaseSetUpProperly}.
 */
public class NotePadTest extends ActivityInstrumentationTestCase2<NotesList> {

	NotesList mActivity = null;

    /**
     * Creates an {@link ActivityInstrumentationTestCase2} for the {@link NotesList} activity.
     */
    public NotePadTest() {
        super(NotesList.class);
    }
	//private static Instrumentation instrumentation = new Instrumentation();

	@Override
	protected void setUp() throws Exception {
		super.setUp();

		//Start the NotesList activity by instrument
		Intent intent = new Intent();
		intent.setClassName("com.example.android.notepad", NotesList.class.getName());
		intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
		Instrumentation inst = getInstrumentation();
		mActivity = (NotesList) inst.startActivitySync(intent);

	}

	 @Override
    protected void tearDown()  {
        mActivity.finish();
        try {
            super.tearDown();
        } catch (Exception e) {
            e.printStackTrace();
        }
	 }

    /**
     * Verifies that the activity under test can be launched.
     */
	 /*
    public void testActivityTestCaseSetUpProperly() {
        assertNotNull("activity should be launched successfully", getActivity());
    }
	*/

	 public void testActivity() throws Exception {

	 	//Add activity monitor to check whether the NoteEditor activity's ready
        ActivityMonitor am = getInstrumentation().addMonitor(NoteEditor.class.getName(), null, false);

        //Evoke the system menu and press on the menu entry "Add note";
        getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_MENU);
        getInstrumentation().invokeMenuActionSync(mActivity, R.id.menu_add, 0);

        //Direct to the NoteEditor activity
        Activity noteEditorActivity = getInstrumentation().waitForMonitorWithTimeout(am, 60000);
        assertEquals(NoteEditor.class,noteEditorActivity.getClass());
        SystemClock.sleep(3000);
        //assertEquals(true, getInstrumentation().checkMonitorHit(am, 1));

        TextView noteEditor = (TextView) noteEditorActivity.findViewById(R.id.note);

        //Get the text directly, DON'T need to runOnMainSync at all!!!
        String text = noteEditor.getText().toString();
        assertEquals(text,"");

        //runOnMainSync to change the text
        getInstrumentation().runOnMainSync(new PerformSetText(noteEditor,"Note1"));

        //inject events to change the text
        getInstrumentation().sendCharacterSync(KeyEvent.KEYCODE_1);
        getInstrumentation().sendCharacterSync(KeyEvent.KEYCODE_2);
        getInstrumentation().sendCharacterSync(KeyEvent.KEYCODE_P);
        getInstrumentation().sendStringSync("gotohell");
        //getInstrumentation().callActivityOnPause(noteEditorActivity);
        Thread.sleep(5000);
        //getInstrumentation().callActivityOnResume(noteEditorActivity);

        //Save the new created note
        getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_MENU);
        getInstrumentation().invokeMenuActionSync(noteEditorActivity, R.id.menu_save, 0);

	 }

	 private class PerformSetText implements Runnable {
        TextView tv;
        String txt;
        public PerformSetText(TextView t,String text) {
            tv = t;
            txt = text;
        }

        public void run() {
            tv.setText(txt);
        }
    }
}
时间: 2024-10-25 22:02:55

Robotium源码分析之Instrumentation进阶的相关文章

Robotium源码分析之运行原理

从上一章<Robotium源码分析之Instrumentation进阶>中我们了解到了Robotium所基于的Instrumentation的一些进阶基础,比如它注入事件的原理等,但Robotium作为一个测试框架,其功能远不止于只是方便我们注入事件,其应该还包含其他高级的功能,参照我们前面其他框架如MonkeyRunner,UiAutomator和Appium的源码分析,我们知道一个移动平台自动化测试框架的基本功能除了事件注入外起码还应该有控件获取的功能.所以,这篇文章我们主要是围绕Robo

Eclipse导入Github上的Robotium源码进行代码分析的步骤

这篇文章应该只是针对像我这样的初级Maven用户的,因为自己花了不少时间来解决这个问题,而网上很多文章描述的也是语焉不详,所以记录下来以便后来如我者可以借鉴一二.文中有几点细节我觉得需要注意的我会高亮出来. 1. 问题描述 今天打算查看一下Robotum(其项目本身基于maven,因为我发现项目中有pom.xml文件)框架的源代码去了解其具体实现以加深理解,但下载后按照认知的方法去Import Maven Project后会发现函数跳转等功能通通不工作,按F3定位一个函数的声明位置时会出现: P

Java进阶之----HashMap源码分析

今天我们接着来看HashMap的源码,对几个常用的方法进行分析.在分析之前,我们还是要先对HashMap的结构有一个了解.看过之前我分析的ArrayList和LinkedList源码的朋友应该清楚,ArrayList内部是以数组实现的,LinkedList内部是以链表实现的.而HashMap则是对数组和链表的结合,虽然看上去复杂了一些,不过仔细分析一下,还是很好理解的.我们来看一张图片,是我根据我的理解画的. 我们在来看看Entry的内部结构是什么: 以上两个图,相信大家对HashMap的结构有

Python 进阶之源码分析:如何将一个类方法变为多个方法?

前一篇文章<Python 中如何实现参数化测试?>中,我提到了在 Python 中实现参数化测试的几个库,并留下一个问题: 它们是如何做到把一个方法变成多个方法,并且将每个方法与相应的参数绑定起来的呢? 我们再提炼一下,原问题等于是:在一个类中,如何使用装饰器把一个类方法变成多个类方法(或者产生类似的效果)? # 带有一个方法的测试类 class TestClass: def test_func(self): pass # 使用装饰器,生成多个类方法 class TestClass: def

Monkey源码分析番外篇之Android注入事件的三种方法比较

原文:http://www.pocketmagic.net/2012/04/injecting-events-programatically-on-android/#.VEoIoIuUcaV 往下分析monkey事件注入源码之前先了解下在android系统下事件注入的方式,翻译一篇国外文章如下. Method 1: Using internal APIs 方法1:使用内部APIs This approach has its risks, like it is always with intern

Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)

1 背景 还记得前一篇<Android触摸屏事件派发机制详解与源码分析一(View篇)>中关于透过源码继续进阶实例验证模块中存在的点击Button却触发了LinearLayout的事件疑惑吗?当时说了,在那一篇咱们只讨论View的触摸事件派发机制,这个疑惑留在了这一篇解释,也就是ViewGroup的事件派发机制. PS:阅读本篇前建议先查看前一篇<Android触摸屏事件派发机制详解与源码分析一(View篇)>,这一篇承接上一篇. 关于View与ViewGroup的区别在前一篇的A

Android ViewGroup触摸屏事件派发机制详解与源码分析

PS一句:最终还是选择CSDN来整理发表这几年的知识点,该文章平行迁移到CSDN.因为CSDN也支持MarkDown语法了,牛逼啊! [工匠若水 http://blog.csdn.net/yanbober] 该篇承接上一篇<Android View触摸屏事件派发机制详解与源码分析>,阅读本篇之前建议先阅读. 1 背景 还记得前一篇<Android View触摸屏事件派发机制详解与源码分析>中关于透过源码继续进阶实例验证模块中存在的点击Button却触发了LinearLayout的事

Android应用Activity、Dialog、PopWindow窗口显示机制及源码分析

[工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重劳动成果] 1 背景 之所以写这一篇博客的原因是因为之前有写过一篇<Android应用setContentView与LayoutInflater加载解析机制源码分析>,然后有人在文章下面评论和微博私信中问我关于Android应用Dialog.PopWindow.Toast加载显示机制是咋回事,所以我就写一篇文章来分析分析吧(本文以Android5.1.1 (API 22)源码为基础分析),以便大家在应

Java源码分析——String的设计

Tip:笔者马上毕业了,准备开始Java的进阶学习计划.于是打算先从String类的源码分析入手,作为后面学习的案例.这篇文章寄托着今后进阶系列产出的愿望,希望能坚持下去,不忘初心,让自己保持那份对技术的热爱. 因为学习分析源码,所以借鉴了HollisChuang成神之路的大部分内容,并在此基础上对源码进行了学习,在此感谢. 问题的引入 关于String字符串,对于Java开发者而言,这无疑是一个非常熟悉的类.也正是因为经常使用,其内部代码的设计才值得被深究.所谓知其然,更得知其所以然. 举个例