在分析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源码分析的文章,这里列出来方便大家浏览:
- 《Android4.3引入的UiAutomation新框架官方简介》
- 《UIAutomator源码分析之启动和运行》
- 《UiAutomator源码分析之UiAutomatorBridge框架》
- 《UiAutomator源码分析之注入事件》
- 《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); } } }