轉載自:https://blog.csdn.net/zhubaitian/article/details/40535579
我們可以看到UiAutomator其實就是使用了UiAutomation這個新框架,通過調用AccessibilitService APIs來獲取窗口界面控件信息已經注入用戶行爲事件,那麼今天開始我們就一起去看下UiAutomator是怎麼運作的。
我們在編寫了測試用例之後,我們需要通過以下幾個步驟把測試腳本build起來並放到測試機器上面:
android create uitest-project -n AutoRunner.jar -t 5 -p D:\\Projects\UiAutomatorDemo
adb push e:\workspace\AutoRunner\bin\AutoRunner.jar data/local/tmp
然後通過以下命令把測試運行起來:
adb shell uiautomator runtest AutoRunner.jar -c majcit.com.UIAutomatorDemo.SettingsSample
那麼我們就圍繞以上這個命令,從uiautomator這個命令作爲突破口,看它是怎麼跑起來的。開始之前我們先看下uiautomator的help幫助:
- 支持三個子命令:rutest/dump/events
- runtest命令-c指定要測試的class文件,用逗號分開,沒有指定的話默認執行測試腳本jar包的所有測試類.注意用戶可以以格式$class/$method來指定只是測試該class的某一個指定的方法
- runtest命令-e參數可以指定是否開啓debug模式
- runtest命令-e參數可以指定test runner,不指定就使用系統默認。我自己從來沒有指定過
- runtest命令-e參數還可以通過鍵值對來指定傳遞給測試類的參數
同時我們這裏會涉及到幾個重要的類,我們這裏先列出來給大家有一個初步的印象:
Class | Package | Description | |
Launcher | com.android.commands.uiautomator | uiautomator命令的入口方法main所在的類 | |
RunTestCommand | com.android.commands | 代表了命令行中‘uiautomator runtest'這個子命令 | |
EventsCommand | com.android.commands | 代表了命令行中‘uiautomator events’這個子命令 | |
DumpCommand |
|
代表了命令行中‘uiautomator dump’這個子命令 | |
UIAutomatorTestRunner | com.android.uiautomator.testrunner | 默認的TestRunner,用來知道測試用例如何執行 | |
TestCaseCollector | com.android.uiautomator.testrunner | 用來從命令行和我們的測試腳本.class文件收集每個測試方法然後建立對應的junit.framework.TestCase測試用例的一個類,它維護着一個List<TestCase> mTestCases列表來存儲所有測試方法(用例) | |
UiAutomationShellWrapper | com.android.uiautomator.core | 一個UiAutomation的wrapper類,簡單的做了封裝,其中提供了一個setRunAsMonkey的方法來通過ActivityManagerNativeProxy來設置系統的運行模式 | |
UiAutomatorBridge |
|
相當於UiAutomation的代理,基本上所有和UiAutomation打交道的方法都是通過它來分發的 | |
ShellUiAutomatorBridge | com.android.uiautomator.core | UiAutomatorBridge的子類,額外增加了幾個不需要用到UiAutomation的方法,如getRotation |
1.環境變量配置
和monkey以及monkeyrunner一樣,uiautomator其實也是一個shell腳本,我們看最後面的關鍵幾行:
CLASSPATH=${CLASSPATH}:${jars}
export CLASSPATH
exec app_process ${base}/bin com.android.commands.uiautomator.Launcher ${args}
我們先把這些變量打印出來,看都是些什麼值:
- CLASSPATH:/system/framework/android.test.runner.jar:/system/framework/uiautomator.jar::/data/local/tmp/AutoRunner.jar
- base:/system
- ${args}:runtest -c majcit.com.UIAutomatorDemo.SettingsSample -e jars :/data/local/tmp/AutoRunner.jar
如monkey一樣,這個shell腳本會:
- 首先export需要的classpath環境變量,讓我們的腳本用到的jar包可以在目標設備上被正常的引用到(畢竟我們在客戶端開發的時候引用到的jar包是本地的,比如uiautomator.jar這個jar包。
- 然後通過app_process來指定命令工作路徑爲'/system/bin/'以啓動指定類com.android.commands.uiautomator.Launcher,啓動該類傳入的參數就是我們指定的測試用例類和我們build好的測試腳本jar包:runtest -c majcit.com.UIAutomatorDemo.SettingsSample -e jars :/data/local/tmp/AutoRunner.jar
那麼現在我們就知道我們的入口就在com.android.commands.uiautomator.Launcher這個class裏面了。
2. 子命令定位
打開com.android.commands.uiautomator.Launcher這個類的原文件,我們首先定位它的入口函數main:
/* */ public static void main(String[] args)
/* */ {
/* 74 */ Process.setArgV0("uiautomator");
/* 75 */ if (args.length >= 1) {
/* 76 */ Command command = findCommand(args[0]);
/* 77 */ if (command != null) {
/* 78 */ String[] args2 = new String[0];
/* 79 */ if (args.length > 1)
/* */ {
/* 81 */ args2 = (String[])Arrays.copyOfRange(args, 1, args.length);
/* */ }
/* 83 */ command.run(args2);
/* 84 */ return;
/* */ }
/* */ }
/* 87 */ HELP_COMMAND.run(args);
/* */ }
裏面主要做兩件事情:
- 76行:根據輸入的第一個參數查找到Command,在我們的例子中第一個參數是runtest,所以要找到的就是runtest這個命令對應的Command
- 83行:執行查找到的command的run方法開始執行測試
那麼到了這裏我們首先要搞清楚Command是怎麼一回事。其實說白了一個Command就代表了我們命令行調用uiautomator輸入的第一個參數,也就是subcommand,比如我們這裏就是runtest這一個命令,如果用戶輸入的是'uiautomator dump'去嘗試dump一個當前窗口界面的所有空間信息,那麼該command就代表了dump這一個命令。uiautomator總共支持3種command(不連help):
- runtest :對應RunTestCommand這個類,代表運行相應測試的命令
- dump : 對應DumpCommand這個類,dump當前窗口控件信息,你在命令行運行‘uiautomator dump’就會把當前ui的hierarchy信息dump成一個文件默認放到sdcard上
- events : 對應EventsCommand這個類,獲取accessibility events,你在命令行運行'uiautomator events'然後在鏈接設備上操作一下就會看到相應的事件打印出來
在Launcher裏面有一個靜態預定義列表COMMANDS定義了這些Command:
/* 129 */ private static Command[] COMMANDS = { HELP_COMMAND, new RunTestCommand(), new DumpCommand(), new EventsCommand() };
這些命令,如我們的RunTestCommand類都是繼承與Command這個Launcher的靜態抽象內部類:
/* */ public static abstract class Command
/* */ {
/* */ private String mName;
/* */
/* */ public Command(String name)
/* */ {
/* 40 */ this.mName = name;
/* */ }
/* */ public String name()
/* */ {
/* 48 */ return this.mName;
/* */ }
/* */
/* */ public abstract String shortHelp();
/* */ public abstract String detailedOptions();
/* */
/* */ public abstract void run(String[] paramArrayOfString);
/* */ }
裏面定義了一個mName的字串成員,其實對應的就是我們命令行傳進來的第一個參數,大家看下子類RunTestCommand這個類的構造函數就清楚了:
/* */ public RunTestCommand() {
/* 62 */ super("runtest");
/* */ }
然後Command類還定義了一個run的方法,注意這個方法非常重要,這個就是我們剛纔分析main函數看到的第二點,是開始運行測試的地方。
好,我們返回之前的main方法,看是怎麼根據‘runtest'這個我們輸入的字串找到對應的RunTestCommand這個command的,我們打開findCommand這個方法:
/* */ private static Command findCommand(String name) {
/* 91 */ for (Command command : COMMANDS) {
/* 92 */ if (command.name().equals(name)) {
/* 93 */ return command;
/* */ }
/* */ }
/* 96 */ return null;
/* */ }
跟我們預期一樣,該方法就是循壞COMMANDS這個預定義的靜態command列表,把上面提到的它們的nName取出來比較,然後找到對應的command對象的。
3. 準備運行
在獲取到我們對應的命令之後,下一步我們就需要根據命令行傳進來的參數來設置我們對應的command對象,以RunTestCommand爲例,從main方法進入到run:
/* */ public void run(String[] args)
/* */ {
/* 67 */ int ret = parseArgs(args);
...
/* 84 */ if (this.mTestClasses.isEmpty()) {
/* 85 */ addTestClassesFromJars();
/* 86 */ if (this.mTestClasses.isEmpty()) {
/* 87 */ System.err.println("No test classes found.");
/* 88 */ System.exit(-3);
/* */ }
/* */ }
/* 91 */ getRunner().run(this.mTestClasses, this.mParams, this.mDebug, this.mMonkey);
/* */ }
這裏做了幾個事情:
- 67行:根據命令行參數設置RunTestCommand的命令屬性
- 84-85行:如果沒有-c參數指定測試類或者指定-e class,那麼默認從指定的jar包裏面獲取所有的測試class進行測試
- 91行:獲取testrunner並執行run方法
3.1 設置命令運行參數
我們進入parseArgs裏面看RunTestCommand是如何根據命令行參數來設置相應的變量的:
/* */ private int parseArgs(String[] args)
/* */ {
/* 105 */ for (int i = 0; i < args.length; i++) {
/* 106 */ if (args[i].equals("-e")) {
/* 107 */ if (i + 2 < args.length) {
/* 108 */ String key = args[(++i)];
/* 109 */ String value = args[(++i)];
/* 110 */ if ("class".equals(key)) {
/* 111 */ addTestClasses(value);
/* 112 */ } else if ("debug".equals(key)) {
/* 113 */ this.mDebug = (("true".equals(value)) || ("1".equals(value)));
/* 114 */ } else if ("runner".equals(key)) {
/* 115 */ this.mRunnerClassName = value;
/* */ } else {
/* 117 */ this.mParams.putString(key, value);
/* */ }
/* */ } else {
/* 120 */ return -1;
/* */ }
/* 122 */ } else if (args[i].equals("-c")) {
/* 123 */ if (i + 1 < args.length) {
/* 124 */ addTestClasses(args[(++i)]);
/* */ } else {
/* 126 */ return -2;
/* */ }
/* 128 */ } else if (args[i].equals("--monkey")) {
/* 129 */ this.mMonkey = true;
/* 130 */ } else if (args[i].equals("-s")) {
/* 131 */ this.mParams.putString("outputFormat", "simple");
/* */ } else {
/* 133 */ return -99;
/* */ }
/* */ }
/* 136 */ return 0;
/* */ }
- 106-117行:判斷是否有-e參數,有指定debug的話就啓動debug;有指定runner的就設置runner;有指定class的話就通過addTestClasses把該測試腳本類加入到mTestClasses列表;有指定其他鍵值對的就保存起來到mParams這個map裏面,比如我們例子種是沒有指定debug和runner,但shell腳本自動會通過-e加上一個鍵值爲jars的鍵值對,值就是我們的測試腳本jar包存放的路徑
- 122-129行:判斷是否有-c參數,有的話就把對應的class加入到RunTestCommand對象的mTestClasses這個列表裏面,注意每個class需要用逗號分開:
/* */ private void addTestClasses(String classes)
/* */ {
/* 181 */ String[] classArray = classes.split(",");
/* 182 */ for (String clazz : classArray) {
/* 183 */ this.mTestClasses.add(clazz);
/* */ }
/* */ }
- 其他參數處理...
3.2 獲取測試集(類)字串列表
處理好命令行參數後RunTestCommand的run方法下一個做的事情就是檢查mTestClasses這個字串類型列表是空的,根據上面的parseArgs方法的分析,如果命令行沒有指定-c或者沒有指定-e class,那麼這個mTestClasses就爲空,這種情況下就會把我們通過adb push進來的命令腳本jar包中的所有class加入到mTestClasses這個字串列表中,也就是說會執行裏面的所有腳本。
3.3 獲取TestRunner
準備好命令參數和要執行的測試類後,下一步就要獲取對應的TestRunner來指導測試腳本的執行了,我們看下我們是怎麼獲得TestRunner的:
/* */ protected UiAutomatorTestRunner getRunner() {
/* 140 */ if (this.mRunner != null) {
/* 141 */ return this.mRunner;
/* */ }
/* */
/* 144 */ if (this.mRunnerClassName == null) {
/* 145 */ this.mRunner = new UiAutomatorTestRunner();
/* 146 */ return this.mRunner;
/* */ }
/* */
/* 149 */ Object o = null;
/* */ try {
/* 151 */ Class<?> clazz = Class.forName(this.mRunnerClassName);
/* 152 */ o = clazz.newInstance();
/* */ } catch (ClassNotFoundException cnfe) {
/* 154 */ System.err.println("Cannot find runner: " + this.mRunnerClassName);
/* 155 */ System.exit(-4);
/* */ } catch (InstantiationException ie) {
/* 157 */ System.err.println("Cannot instantiate runner: " + this.mRunnerClassName);
/* 158 */ System.exit(-4);
/* */ } catch (IllegalAccessException iae) {
/* 160 */ System.err.println("Constructor of runner " + this.mRunnerClassName + " is not accessibile");
/* 161 */ System.exit(-4);
/* */ }
/* */ try {
/* 164 */ UiAutomatorTestRunner runner = (UiAutomatorTestRunner)o;
/* 165 */ this.mRunner = runner;
/* 166 */ return runner;
/* */ } catch (ClassCastException cce) {
/* 168 */ System.err.println("Specified runner is not subclass of " + UiAutomatorTestRunner.class.getSimpleName());
/* */
/* 170 */ System.exit(-4);
/* */ }
/* */
/* 173 */ return null;
/* */ }
這個類看上去有點長,但其實做的事情重要的就那麼兩點,其他的都是些錯誤處理:
- 用戶有沒有在命令行通過-e runner指定TestRunner,有的話就用該TestRunner
- 用戶沒有指定TestRunner的話就用默認的UiAutomatorTestRunner
3.4 每個方法建立junit.framework.TestCase
確定了UiAutomatorTestRunner這個TestRunner後的下一步就是調用它的run方法來指導測試用例的執行:
/* */ public void run(List<String> testClasses, Bundle params, boolean debug, boolean monkey)
/* */ {
...
/* 92 */ this.mTestClasses = testClasses;
/* 93 */ this.mParams = params;
/* 94 */ this.mDebug = debug;
/* 95 */ this.mMonkey = monkey;
/* 96 */ start();
/* 97 */ System.exit(0);
/* */ }
傳進來的參數就是我們剛纔通過parseArgs方法設置的那些變量,run方法會把這些變量保存起來以便下面使用,緊跟着它就會調用一個start方法,這個方法非常重要,從建立每個測試方法對應的junit.framwork.TestCase對象到真正執行測試都在這個方法完成,所以也比較長,我們挑重要的部分進行分析,首先我們看以下代碼:
/* */ protected void start()
/* */ {
/* 104 */ TestCaseCollector collector = getTestCaseCollector(getClass().getClassLoader());
/* */ try {
/* 106 */ collector.addTestClasses(this.mTestClasses);
/* */ }
...
}
這裏面調用了TestCaseCollector這個類的addTestClasses的方法,從這個類的名字我們可以猜測到它就是專門收集測試用例用的,那麼我們往下跟蹤下看它是怎麼收集測試用例的:
/* */ public void addTestClasses(List<String> classNames)
/* */ throws ClassNotFoundException
/* */ {
/* 52 */ for (String className : classNames) {
/* 53 */ addTestClass(className);
/* */ }
/* */ }
這裏傳進來的就是我們上面保存起來的收集了每個class名字的字串列表。裏面執行了一個for循環來把每一個類的字串拿出來,然後調用addTestClass:
/* */ public void addTestClass(String className)
/* */ throws ClassNotFoundException
/* */ {
/* 66 */ int hashPos = className.indexOf('#');
/* 67 */ String methodName = null;
/* 68 */ if (hashPos != -1) {
/* 69 */ methodName = className.substring(hashPos + 1);
/* 70 */ className = className.substring(0, hashPos);
/* */ }
/* 72 */ addTestClass(className, methodName);
/* */ }
這裏可能你會奇怪爲什麼會查看類名字串裏面是否有#號呢?其實在文章開頭的時候我就有提出來,-c或者-e class指定的類名是可以支持 $className/$methodName來指定執行該className的methodName這個方法的,比如我可以指定-c majcit.com.UIAutomatorDemo.SettingsSample#testSetLanEng來指定只是測試該類裏面的testSetLanEng這個方法。如果用戶沒有指定的話該methodName變量就設置成null,然後調用重載方法addTestClass方法:
/* */ public void addTestClass(String className, String methodName)
/* */ throws ClassNotFoundException
/* */ {
/* 84 */ Class<?> clazz = this.mClassLoader.loadClass(className);
/* 85 */ if (methodName != null) {
/* 86 */ addSingleTestMethod(clazz, methodName);
/* */ } else {
/* 88 */ Method[] methods = clazz.getMethods();
/* 89 */ for (Method method : methods) {
/* 90 */ if (this.mFilter.accept(method)) {
/* 91 */ addSingleTestMethod(clazz, method.getName());
/* */ }
/* */ }
/* */ }
/* */ }
- 84行:最終會調用 java.lang.ClassLoader的loadClass方法,通過指定類的名字來把該測試腳本類裝載進來並賦予給clazz這個Class<?>變量,注意這裏這個測試類還沒有實例化的,真正實例化的地方是在下面的addSingleTestMethod中
- 85-86行:如果用戶用#號指定測試某一個類的某個方法,那麼就直接傳入參數clazz和要測試的methodName來調用addSingleTestMehod來組建我們需要的TestCase
- 88-91行:如果用戶沒用#號指定測試某個類的某個方法,那麼就需要循環取出該類的所有測試方法,然後每個方法調用一次addSingleTestMethod.
好,終於來到的關鍵點,下面我們看addSingleTestMethod是如何根據測試類clazz和它的一個方法創建一個junit.framework.TestCase對象的:
/* */ protected void addSingleTestMethod(Class<?> clazz, String method) {
/* 106 */ if (!this.mFilter.accept(clazz)) {
/* 107 */ throw new RuntimeException("Test class must be derived from UiAutomatorTestCase");
/* */ }
/* */ try {
/* 110 */ TestCase testCase = (TestCase)clazz.newInstance();
/* 111 */ testCase.setName(method);
/* 112 */ this.mTestCases.add(testCase);
/* */ } catch (InstantiationException e) {
/* 114 */ this.mTestCases.add(error(clazz, "InstantiationException: could not instantiate test class. Class: " + clazz.getName()));
/* */ }
/* */ catch (IllegalAccessException e) {
/* 117 */ this.mTestCases.add(error(clazz, "IllegalAccessException: could not instantiate test class. Class: " + clazz.getName()));
/* */ }
/* */ }
- 106-107行:這一個判斷非常的重要,我們的測試腳本必須都是繼承於UiAutomatorTestCase的,否則不支持!
- 110行:把測試用例類進行初始化獲得一個實例對象,然後強制轉換成junit.framework.TestCase類型,這裏要注意我們測試腳本的父類UiAutomationTestCase也是繼承與junit.framework.TestCase的
- 111行:設置junit.framework.TestCase實例對象的方法名字,這個很重要,下一章節可以看到junit框架會通過它來找到我們測試腳本中要執行的那個方法
- 112行:把這個TestCase對象增加到當前TestCaseCollector的mTestCases這個junit.framework.TestCase類型的列表裏面
這個小節代碼稍微多了點,其實簡單來說就是UiAutomatorTestRunner在指導測試用例怎麼跑的時候,會去請求TestCaseController去把用戶傳進來的測試類名字字串列表中的每個類對應的每個方法轉換成junit.framework.TestCase,並把這些TestCase保存在TestCaseCollector對象的mTestCases這個列表裏面。
這裏千萬要注意的一點是;並非一個測試腳本(類)一個TestCase,而是一個方法創建一個TestCase!
3.5 初始化UiAutomationShellWrapper並連接上AccessibilityService來設置Monkey模式
上面UiAutomatorTestRunner的start方法在調用完TestCaseCollector來建立TestCase列表後,會嘗試建立AccessibilityService的連接,來看是否應該把UiAutomation設置成Monkey運行模式:
/* */ protected void start()
/* */ {
...
/* 117 */ UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper();
/* 118 */ automationWrapper.connect();
/* */
...
/* */ try {
/* 132 */ automationWrapper.setRunAsMonkey(this.mMonkey);
...
}
...
}
這裏會初始化一個UiAutomationShellWrapper的類,其實這個類如其名,就是UiAutomation的一個Wrapper,初始化好後最終會調用UiAutomation的connect方法來連接上AccessibilityService服務,然後就可以調用AccessibilityService相應的API來把UiAutomation設置成Monkey模式來運行了。而在我們的例子中我們沒有指定monkey模式的參數,所以是不會設置monkey模式的。
至於什麼是Monkey模式,我說了不算,官方說了算:
Applications can query whether they are executed in a "monkey" mode, i.e. run by a test framework, and avoid doing potentially undesirable actions such as calling 911 or posting on public forums etc.
也就是說設置了這個模式之後,一些應用會調用我們《Android4.3引入的UiAutomation新框架官方簡介》提到的isUserMonkey()這個著名的api來判斷究竟是不是一個測試腳本在要求本應用做事情,那麼判斷如果是的話就不要讓它做一些意想不到的如撥打911的事情。不然你一個測試腳本寫錯了,一個死循環一個晚上在撥打911,保管警察第二天上你公司找你。
3.6 初始化UiDevice和UiAutomationBridge
在所有要運行的基於每個方法的TestCase都準備好之後,我們還不能直接去調用junit.framework.TestCase的run方法來執行該方法,我們還需要做幾個很重要的事情:
- 初始化一個UiDevice對象
- 每執行一個測試方法之前必須給該腳本傳入該UiDevice對象。大家寫過UiAutomator腳本的應該都知道UiDevce不是調用構造函數而是通過getUiDevice獲得的,而getUiDevice其實就是我們的測試腳本的父類UiAutomatorTestCase的方法,往後我們會看到它們是怎麼聯繫起來的
好,我們繼續分析上面UiAutomatorTestRunner的start方法,上面一小節它完成了測試用例每個方法對應的junit.framework.TestCase對象的建立,那麼往下:
/* */ protected void start()
/* */ {
...
/* */ try {
/* 132 */ automationWrapper.setRunAsMonkey(this.mMonkey);
/* 133 */ this.mUiDevice = UiDevice.getInstance();
/* 134 */ this.mUiDevice.initialize(new ShellUiAutomatorBridge(automationWrapper.getUiAutomation()));
/* */
...
}
...
}
在嘗試設置monkey模式之後,UiAutomatorTestRunner會去實例化一個UiDevice,實例化後會通過以下步驟對其進行初始化:
- 首先獲取上一小節提到的UiAutomationShellWrapper這個Wrapper裏面的UiAutomation實例,注意這個實例在上一小節中已經連接上AccessiblityService的了
- 以這個連接好的UiAutomation爲參數構造一個ShellUiAutomatorBridge,注意這裏不是UiAutiomatorBridge。ShellUiAutomatorBridge時繼承於UiAutomatorBridge的一個子類,裏面實現了額外的幾個不需要通過UiAutomation的操作,比如getRotation等是通過WindowManager來實現的
- 最後通過調用UiDevice的initialize這個方法傳入ShellUiAutomatorBridge的實例來初始化我們的UiDevice
- 完成以上的初始化後,我們就擁有了一個已經通過UiAutomation連接上設備的AccessibilityService的UiDevice了,這樣我們就可以隨意調用AccessibilityService API來爲我們服務了
這裏提到的一些類也許對你會有點陌生,本人接下來會另外開文章去進行描述。
4. 啓動junit測試
到現在位置似乎所有東西都準備好了:
- 每個測試用例中的每個測試方法對應的junit.framework.TestCase建立好
- 已經連接上AccessibilityService的UiDevice準備好
那麼我們是不是就可以立刻直接調用junit.framework.TestCase的run開始執行測試方法呢?既然以這種調調來提問,答案可想而知肯定不是的了。那麼爲什麼還不能運行呢?既然這些都準備好了。其實這裏問題是UiDevice,確實,上面的UiDevice實例已經擁有一個UiAutomation對象,且該對象已經連接上AccessibilityService服務,但是你要知道這個UiDevice對象現在是UiAutomatorTestRunner這個類的對象擁有的,而我們的測試腳本並沒有繼承或者擁有這個類的變量。請看以下的測試腳本:
package majcit.com.UIAutomatorDemo;
import com.android.uiautomator.core.UiDevice;
import com.android.uiautomator.core.UiObject;
import com.android.uiautomator.core.UiObjectNotFoundException;
import com.android.uiautomator.core.UiScrollable;
import com.android.uiautomator.core.UiSelector;
import com.android.uiautomator.testrunner.UiAutomatorTestCase;
public class UISelectorFindElementTest extends UiAutomatorTestCase {
public void testDemo() throws UiObjectNotFoundException {
UiDevice device = getUiDevice();
device.pressHome();
既然測試腳本中的getUiDevice方法不是直接從UiAutomatorTestRunner獲得,那麼是不是從它繼承下來的UiAutomatorTestCase中獲得呢?答案是肯定的,我們繼續看那個UiAutomatorTestRunner中很重要的start方法:
/* */
/* */ protected void start()
/* */ {
...
/* 158 */ for (TestCase testCase : testCases) {
/* 159 */ prepareTestCase(testCase);
/* 160 */ testCase.run(testRunResult);
/* */ }
...
}
一個for循環把我們上面創建好的所有junit.framework.TestCase對象做一個遍歷,在執行之前先調用一個prepareTestCase:
/* */ protected void prepareTestCase(TestCase testCase)
/* */ {
/* 427 */ ((UiAutomatorTestCase)testCase).setAutomationSupport(this.mAutomationSupport);
/* 428 */ ((UiAutomatorTestCase)testCase).setUiDevice(this.mUiDevice);
/* 429 */ ((UiAutomatorTestCase)testCase).setParams(this.mParams);
/* */ }
這個方法所做的事情就解決了我們剛纔的疑問:第428行,把當前UiAutomatorTestRunner擁有的這個已經連接到AccessibilityService的UiObject對象,通過我們測試腳本的父類的setUiDevice方法設置到我們的TestCase腳本對象裏面
/* */ void setUiDevice(UiDevice uiDevice)
/* */ {
/* 100 */ this.mUiDevice = uiDevice;
/* */ }
這樣我們測試腳本每次執行getUiDevice的時候就能直接取得該對象了:
/* */ public UiDevice getUiDevice()
/* */ {
/* 72 */ return this.mUiDevice;
/* */ }
從整個過程可以看到,UiObject的對象我們在測試腳本上是不用初始化的,它是在運行時由我們默認的TestuRunner -- UiAutomatorTestRunner 傳遞進來的,這個我們作爲測試人員是不需要知道這一點的。
好了,到了現在就真的可以直接觸發junit.framework.TestCase的run方法來讓測試跑起來了,這裏要注意我們之前的分析,並不是測試腳本的所有方法都同時調用run執行的,而是一個方法調用一次run方法。
5. 擴展閱讀:junit框架如何通過方法名執行測試方法
下面如果有興趣知道juint框架是如何通過3.4節建立junit.framework.TestCase時調用setName方法設置的測試方法名字來調用執行對應方法的可以繼續往下跟蹤run方法,它最終會進入到junit.framework.TestCase的runTest方法
protected void runTest() throws Throwable {
assertNotNull(fName); // Some VMs crash when calling getMethod(null,null);
Method runMethod= null;
try {
// use getMethod to get all public inherited
// methods. getDeclaredMethods returns all
// methods of this class but excludes the
// inherited ones.
runMethod= getClass().getMethod(fName, (Class[])null);
} catch (NoSuchMethodException e) {
fail("Method \""+fName+"\" not found");
}
if (!Modifier.isPublic(runMethod.getModifiers())) {
fail("Method \""+fName+"\" should be public");
}
try {
runMethod.invoke(this, (Object[])new Class[0]);
}
catch (InvocationTargetException e) {
e.fillInStackTrace();
throw e.getTargetException();
}
catch (IllegalAccessException e) {
e.fillInStackTrace();
throw e;
}
}
從中可以看到它會嘗試通過getClass().getMethod方法獲得這個junit.framework.TestCase所代表的測試腳本的於我們設置的fName一致的方法,然後纔會去執行。