您好,登錄后才能下訂單哦!
通過上一篇《Android4.3引入的UiAutomation新框架官方簡介》我們可以看到UiAutomator其實就是使用了UiAutomation這個新框架,通過調用AccessibilitService APIs來獲取窗口界面控件信息已經注入用戶行為事件,那么今天開始我們就一起去看下UiAutomator是怎么運作的。
我們在編寫了測試用例之后,我們需要通過以下幾個步驟把測試腳本build起來并放到測試機器上面:
同時我們這里會涉及到幾個重要的類,我們這里先列出來給大家有一個初步的印象:
Class | Package | Description |
Launcher | com.android.commands.uiautomator | uiautomator命令的入口方法main所在的類 |
RunTestCommand | com.android.commands | 代表了命令行中‘uiautomator runtest'這個子命令 |
EventsCommand | com.android.commands | 代表了命令行中‘uiautomator events’這個子命令 |
DumpCommand | com.android.commands | 代表了命令行中‘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 | com.android.uiautomator.core | 相當于UiAutomation的代理,基本上所有和UiAutomation打交道的方法都是通過它來分發的 |
ShellUiAutomatorBridge | com.android.uiautomator.core | UiAutomatorBridge的子類,額外增加了幾個不需要用到UiAutomation的方法,如getRotation |
CLASSPATH=${CLASSPATH}:${jars} export CLASSPATH exec app_process ${base}/bin com.android.commands.uiautomator.Launcher ${args}
/* */ 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); /* */ }里面主要做兩件事情:
/* 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函數看到的第二點,是開始運行測試的地方。
/* */ 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對象的。
/* */ 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); /* */ }這里做了幾個事情:
/* */ 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; /* */ }
/* */ private void addTestClasses(String classes) /* */ { /* 181 */ String[] classArray = classes.split(","); /* 182 */ for (String clazz : classArray) { /* 183 */ this.mTestClasses.add(clazz); /* */ } /* */ }
/* */ 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; /* */ }這個類看上去有點長,但其實做的事情重要的就那么兩點,其他的都是些錯誤處理:
/* */ 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()); /* */ } /* */ } /* */ } /* */ }
/* */ 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())); /* */ } /* */ }
/* */ 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模式的。
/* */ 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,實例化后會通過以下步驟對其進行初始化:
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; /* */ }
/* */ public UiDevice getUiDevice() /* */ { /* 72 */ return this.mUiDevice; /* */ }從整個過程可以看到,UiObject的對象我們在測試腳本上是不用初始化的,它是在運行時由我們默認的TestuRunner -- UiAutomatorTestRunner 傳遞進來的,這個我們作為測試人員是不需要知道這一點的。
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一致的方法,然后才會去執行。
作者 | 自主博客 | 微信 | CSDN |
天地會珠海分舵 | http://techgogogo.com | 服務號:TechGoGoGo 掃描碼:
| 向AI問一下細節 免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。 猜你喜歡最新資訊相關推薦相關標簽AI
助 手
湖南省|
四平市|
福安市|
嘉黎县|
伊吾县|
额尔古纳市|
桃园县|
轮台县|
白城市|
吉林市|
迭部县|
长汀县|
保定市|
梁平县|
双辽市|
靖西县|
贡嘎县|
黄大仙区|
庆阳市|
庆云县|
冕宁县|
德格县|
淳化县|
滦南县|
静海县|
翼城县|
鄂托克前旗|
乡城县|
巢湖市|
尚志市|
台北市|
德阳市|
通山县|
延吉市|
兴化市|
云林县|
黑龙江省|
滦南县|
应城市|
亚东县|
曲周县|
|