企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
通过上一篇《[Android4.3引入的UiAutomation新框架官方简介](http://blog.csdn.net/zhubaitian/article/details/40504827)》我们可以看到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帮助: ![](https://box.kancloud.cn/2016-08-12_57ad6e297176f.jpg) - 支持三个子命令:rutest/dump/events - runtest命令-c指定要测试的class文件,用逗号分开,没有指定的话默认执行测试脚本jar包的所有测试类.注意用户可以以格式$class/$method来指定只是测试该class的某一个指定的方法 - runtest命令-e参数可以指定是否开启debug模式 - runtest命令-e参数可以指定test runner,不指定就使用系统默认。我自己从来没有指定过 - runtest命令-e参数还可以通过键值对来指定传递给测试类的参数 同时我们这里会涉及到几个重要的类,我们这里先列出来给大家有一个初步的印象: <table cellspacing="0" cellpadding="0" class=" " style="border-collapse:collapse"><tbody><tr><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:13px; padding:4px; background-color:rgb(100,179,223)"><p align="center" style="margin-top:0px; margin-bottom:0px; text-align:center"><span style="font-size:10px; font-family:Helvetica; letter-spacing:0px"><strong>Class<span style="white-space:pre"/></strong></span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:151px; height:13px; padding:4px; background-color:rgb(100,179,223)"><p align="center" style="margin-top:0px; margin-bottom:0px; text-align:center"><span style="font-size:10px; font-family:Helvetica; letter-spacing:0px"><strong>Package<span style="white-space:pre"/></strong></span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:13px; padding:4px; background-color:rgb(100,179,223)"><p align="center" style="margin-top:0px; margin-bottom:0px; text-align:center"><span style="font-size:10px; font-family:Helvetica; letter-spacing:0px"><strong>Description</strong></span></p></td></tr><tr><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:39px; padding:4px; background-color:rgb(239,239,239)"><p style="margin-top:0px; margin-bottom:0px"><span style="font-size:10px; font-family:Helvetica; letter-spacing:0px">Launcher</span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:151px; height:39px; padding:4px; background-color:rgb(239,239,239)"><p style="margin-top:0px; margin-bottom:0px; line-height:26px; background-color:rgb(255,255,255)"><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(51,51,51)">com.android.commands.uiautomator</span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:39px; padding:4px; background-color:rgb(239,239,239)"><p style="margin-top:0px; margin-bottom:0px"><span style="font-size:10px; font-family:Helvetica; letter-spacing:0px">uiautomator</span><span style="font-size:10px; font-family:'Heiti SC Light'; letter-spacing:0px">命令的入口方法</span><span style="font-size:10px; font-family:Helvetica; letter-spacing:0px">main</span><span style="font-size:10px; font-family:'Heiti SC Light'; letter-spacing:0px">所在的类</span></p></td></tr><tr><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:39px; padding:4px"><p style="margin-top:0px; margin-bottom:0px"><span style="font-size:10px; font-family:Helvetica; letter-spacing:0px">RunTestCommand</span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:151px; height:39px; padding:4px"><p style="margin-top:0px; margin-bottom:0px; line-height:26px"><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(51,51,51)">com.android.commands</span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:39px; padding:4px"><p style="margin-top:0px; margin-bottom:0px; line-height:26px"><span style="line-height:normal; font-family:'Heiti SC Light'; letter-spacing:0px; color:rgb(51,51,51)">代表了命令行中</span><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(51,51,51)">‘uiautomator runtest'</span><span style="line-height:normal; font-family:'Heiti SC Light'; letter-spacing:0px; color:rgb(51,51,51)">这个子命令</span></p></td></tr><tr><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:39px; padding:4px; background-color:rgb(239,239,239)"><p style="margin-top:0px; margin-bottom:0px"><span style="font-size:10px; font-family:Helvetica; letter-spacing:0px">EventsCommand</span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:151px; height:39px; padding:4px; background-color:rgb(239,239,239)"><p style="margin-top:0px; margin-bottom:0px; line-height:26px; background-color:rgb(255,255,255)"><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(51,51,51)">com.android.commands</span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:39px; padding:4px; background-color:rgb(239,239,239)"><p style="margin-top:0px; margin-bottom:0px; line-height:26px; background-color:rgb(255,255,255)"><span style="line-height:normal; font-family:'Heiti SC Light'; letter-spacing:0px; color:rgb(51,51,51)">代表了命令行中</span><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(51,51,51)">‘uiautomator events’</span><span style="line-height:normal; font-family:'Heiti SC Light'; letter-spacing:0px; color:rgb(51,51,51)">这个子命令</span></p></td></tr><tr><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:38px; padding:4px"><p style="margin-top:0px; margin-bottom:0px"><span style="font-size:10px; font-family:Helvetica; letter-spacing:0px">DumpCommand</span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:151px; height:38px; padding:4px"><p style="margin-top:0px; margin-bottom:0px; line-height:26px"><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(51,51,51)">com.android.commands</span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:38px; padding:4px"><p style="margin-top:0px; margin-bottom:0px; line-height:26px"><span style="line-height:normal; font-family:'Heiti SC Light'; letter-spacing:0px; color:rgb(51,51,51)">代表了命令行中</span><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(51,51,51)">‘uiautomator dump’</span><span style="line-height:normal; font-family:'Heiti SC Light'; letter-spacing:0px; color:rgb(51,51,51)">这个子命令</span></p></td></tr><tr><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:39px; padding:4px; background-color:rgb(239,239,239)"><p style="margin-top:0px; margin-bottom:0px"><span style="font-size:10px; font-family:Helvetica; letter-spacing:0px">UIAutomatorTestRunner</span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:151px; height:39px; padding:4px; background-color:rgb(239,239,239)"><p style="margin-top:0px; margin-bottom:0px; line-height:26px; background-color:rgb(255,255,255)"><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(51,51,51)">com.android.uiautomator.testrunner</span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:39px; padding:4px; background-color:rgb(239,239,239)"><p style="margin-top:0px; margin-bottom:0px; line-height:26px; background-color:rgb(255,255,255)"><span style="line-height:normal; font-family:'Heiti SC Light'; letter-spacing:0px; color:rgb(51,51,51)">默认的</span><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(51,51,51)">TestRunner</span><span style="line-height:normal; font-family:'Heiti SC Light'; letter-spacing:0px; color:rgb(51,51,51)">,用来知道测试用例如何执行</span></p></td></tr><tr><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:78px; padding:4px"><p style="margin-top:0px; margin-bottom:0px"><span style="font-size:10px; font-family:Helvetica; letter-spacing:0px">TestCaseCollector</span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:151px; height:78px; padding:4px"><p style="margin-top:0px; margin-bottom:0px; line-height:26px"><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(51,51,51)">com.android.uiautomator.testrunner</span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:78px; padding:4px"><p style="margin-top:0px; margin-bottom:0px"><span style="font-size:10px; font-family:'Heiti SC Light'; letter-spacing:0px">用来从命令行和我们的测试脚本</span><span style="font-size:10px; font-family:Helvetica; letter-spacing:0px">.class</span><span style="font-size:10px; font-family:'Heiti SC Light'; letter-spacing:0px">文件收集每个测试方法然后建立对应的</span><span style="font-size:10px; font-family:Helvetica; letter-spacing:0px">junit.framework.TestCase</span><span style="font-size:10px; font-family:'Heiti SC Light'; letter-spacing:0px">测试用例的一个类,它维护着一个</span><span style="font-size:10px; font-family:Helvetica; letter-spacing:0px">List&lt;TestCase&gt; mTestCases</span><span style="font-size:10px; font-family:'Heiti SC Light'; letter-spacing:0px">列表来存储所有测试方法</span><span style="font-size:10px; font-family:Helvetica; letter-spacing:0px">(</span><span style="font-size:10px; font-family:'Heiti SC Light'; letter-spacing:0px">用例</span><span style="font-size:10px; font-family:Helvetica; letter-spacing:0px">)</span></p></td></tr><tr><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:167px; padding:4px; background-color:rgb(239,239,239)"><p style="margin-top:0px; margin-bottom:0px; line-height:26px; background-color:rgb(255,255,255)"><span style="line-height:normal; font-family:Helvetica; letter-spacing:0px; color:rgb(54,46,43)">UiAutomationShellWrapper</span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:151px; height:167px; padding:4px; background-color:rgb(239,239,239)"><p style="margin-top:0px; margin-bottom:0px; line-height:26px; background-color:rgb(255,255,255)"><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(51,51,51)">com.android.uiautomator.core</span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:167px; padding:4px; background-color:rgb(239,239,239)"><p style="margin-top:0px; margin-bottom:0px; line-height:26px; background-color:rgb(255,255,255)"><span style="line-height:normal; font-family:'Heiti SC Light'; letter-spacing:0px; color:rgb(54,46,43)">一个</span><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(54,46,43)">UiAutomation</span><span style="line-height:normal; font-family:'Heiti SC Light'; letter-spacing:0px; color:rgb(54,46,43)">的</span><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(54,46,43)">wrapper</span><span style="line-height:normal; font-family:'Heiti SC Light'; letter-spacing:0px; color:rgb(54,46,43)">类,简单的做了封装,其中提供了一个</span><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(54,46,43)">setRunAsMonkey</span><span style="line-height:normal; font-family:'Heiti SC Light'; letter-spacing:0px; color:rgb(54,46,43)">的方法来通过</span><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(54,46,43)">ActivityManagerNativeProxy</span><span style="line-height:normal; font-family:'Heiti SC Light'; letter-spacing:0px; color:rgb(54,46,43)">来设置系统的运行模式</span></p></td></tr><tr><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:39px; padding:4px"><p style="margin-top:0px; margin-bottom:0px; line-height:26px"><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(54,46,43)">UiAutomatorBridge</span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:151px; height:39px; padding:4px"><p style="margin-top:0px; margin-bottom:0px; line-height:26px"><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(54,46,43)">com.android.uiautomator.core</span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:39px; padding:4px"><p style="margin-top:0px; margin-bottom:0px"><span style="font-size:10px; font-family:'Heiti SC Light'; letter-spacing:0px">相当于</span><span style="font-size:10px; font-family:Helvetica; letter-spacing:0px">UiAutomation</span><span style="font-size:10px; font-family:'Heiti SC Light'; letter-spacing:0px">的代理,基本上所有和</span><span style="font-size:10px; font-family:Helvetica; letter-spacing:0px">UiAutomation</span><span style="font-size:10px; font-family:'Heiti SC Light'; letter-spacing:0px">打交道的方法都是通过它来分发的</span></p></td></tr><tr><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:91px; padding:4px; background-color:rgb(239,239,239)"><p style="margin-top:0px; margin-bottom:0px; line-height:26px; background-color:rgb(255,255,255)"><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(54,46,43)">ShellUiAutomatorBridge</span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:151px; height:91px; padding:4px; background-color:rgb(239,239,239)"><p style="margin-top:0px; margin-bottom:0px; line-height:26px; background-color:rgb(255,255,255)"><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(54,46,43)">com.android.uiautomator.core</span></p></td><td valign="top" style="border-style:solid; border-color:rgb(0,0,0); width:152px; height:91px; padding:4px; background-color:rgb(239,239,239)"><p style="margin-top:0px; margin-bottom:0px; line-height:26px; background-color:rgb(255,255,255)"><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(54,46,43)">UiAutomatorBridge</span><span style="line-height:normal; font-family:'Heiti SC Light'; letter-spacing:0px; color:rgb(54,46,43)">的子类,额外增加了几个不需要用到</span><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(54,46,43)">UiAutomation</span><span style="line-height:normal; font-family:'Heiti SC Light'; letter-spacing:0px; color:rgb(54,46,43)">的方法</span><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(54,46,43)">,</span><span style="line-height:normal; font-family:'Heiti SC Light'; letter-spacing:0px; color:rgb(54,46,43)">如</span><span style="line-height:normal; font-family:Arial; letter-spacing:0px; color:rgb(54,46,43)">getRotation</span></p></td></tr></tbody></table> # 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新框架官方简介](http://blog.csdn.net/zhubaitian/article/details/40504827)》提到的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一致的方法,然后才会去执行。 <table cellspacing="0" cellpadding="0" width="539" class=" " style="margin:0px 0px 10px; padding:0px; border-collapse:collapse; width:668px; max-width:100%; word-wrap:break-word!important"><tbody style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important"><tr style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important"><td valign="top" width="112" height="39" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important"> </td></tr><tr style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important"><td valign="top" width="111" height="13" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important; background-color:rgb(190,192,191)"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important">作者</span></p></td><td valign="top" width="112" height="13" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important; background-color:rgb(190,192,191)"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important">自主博客</span></p></td><td valign="top" width="111" height="13" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important; background-color:rgb(190,192,191)"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important">微信</span></p></td><td valign="top" width="112" height="13" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important; background-color:rgb(190,192,191)"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; font-family:Helvetica; letter-spacing:0px; word-wrap:break-word!important">CSDN</span></p></td></tr><tr style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important"><td valign="top" width="111" height="39" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important; background-color:rgb(227,228,228)"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important">天地会珠海分舵</span></p></td><td valign="top" width="112" height="39" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; font-size:11px; font-family:Helvetica; letter-spacing:0px; word-wrap:break-word!important"><a target="_blank" href="http://techgogogo.com/">http://techgogogo.com</a></span><span style="margin:0px; padding:0px; max-width:100%; font-family:Helvetica; font-size:11px; letter-spacing:0px; word-wrap:break-word!important"/></p><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:14px; white-space:pre-wrap; font-family:Helvetica; word-wrap:break-word!important"><br style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important"/></p></td><td valign="top" width="111" height="39" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important">服务号</span><span style="margin:0px; padding:0px; max-width:100%; font-size:10px; font-family:Helvetica; letter-spacing:0px; word-wrap:break-word!important">:TechGoGoGo</span></p><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important">扫描码</span><span style="margin:0px; padding:0px; max-width:100%; font-size:10px; font-family:Helvetica; letter-spacing:0px; word-wrap:break-word!important">:</span></p><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:14px; white-space:pre-wrap; font-family:Helvetica; word-wrap:break-word!important"><img src="image/47cf4f9ec59b0ef1f807a6c33ab5ce5f.jpg" alt="" style="max-width:100%; margin:0px; padding:0px; height:auto!important; word-wrap:break-word!important; width:auto!important; visibility:visible!important"/></p></td><td valign="top" width="112" height="39" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; color:rgb(62,62,62); font-family:'Helvetica Neue',Helvetica,'Hiragino Sans GB','Microsoft YaHei',΢ÈíÑźÚ,Arial,sans-serif; font-size:18px; line-height:28.7999992370605px; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; color:rgb(0,0,0); font-size:11px; font-family:Helvetica; letter-spacing:0px; word-wrap:break-word!important"><a target="_blank" href="http://blog.csdn.net/zhubaitian">http://blog.csdn.net/zhubaitian</a></span><span style="margin:0px; padding:0px; max-width:100%; color:rgb(0,0,0); font-family:Helvetica; font-size:11px; letter-spacing:0px; line-height:28.7999992370605px; word-wrap:break-word!important"/></p><div><span style="margin:0px; padding:0px; max-width:100%; color:rgb(0,0,0); font-family:Helvetica; font-size:11px; letter-spacing:0px; line-height:28.7999992370605px; word-wrap:break-word!important"><br/></span></div></td></tr></tbody></table>