导航

Android4.3引入的UiAutomation新框架官方简介

Posted on 2017-06-25 00:43  Young哥哥  阅读(818)  评论(0)    收藏  举报

译者序:Google在Android 4.3发布时提供了一套新的UiAutomation框架来支持用户界面自动化测试,该框架通过运用已有的Accessibility APIs来模拟用户跟设备用户界面的交互:比如获取窗口界面控件和注入事件等。如在4.3之前UiAutomator工具是通过InputManager或者更早的WindowManager来注入KeyEvent等,4.3之后用的就是新框架UiAutomation使用的Accessibility APIs来注入事件了。

Class Overview/概览


Class for interacting with the device's UI by simulation user actions and introspection of the screen content. It relies on the platform accessibility APIs to introspect the screen and to perform some actions on the remote view tree. It also allows injecting of arbitrary raw input events simulating user interaction with keyboards and touch devices. One can think of a UiAutomation as a special type of AccessibilityService which does not provide hooks for the service life cycle and exposes other APIs that are useful for UI test automation.

这是一个通过模拟用户操作来与设备用户界面交互以及获取屏幕内容的类。它依赖于平台的辅助功能APIs来在远程的控件树上获取屏幕内容以及执行一些操作。同时它也允许通过注入原生事件(译者注:指的就是InputEvent. KeyEvent也是继承于InputEvent的,所以说它是原生事件)来模拟用户的按键和触屏操作。我们可以认为UiAutomation就是一个特殊类型的AccessibilityService,其既不会为控制服务的生命周期而提供钩子函数,也不会暴露任何其他可以直接用于用户界面测试自动化的APIs.

The APIs exposed by this class are low-level to maximize flexibility when developing UI test automation tools and libraries. Generally, a UiAutomation client should be using a higher-level library or implement high-level functions. For example, performing a tap on the screen requires construction and injecting of a touch down and up events which have to be delivered to the system by a call to injectInputEvent(InputEvent, boolean).

这个类暴露出来的APIs是很低层的,目的就是为了在开发用户界面测试自动化框架和库时提供最大的弹性。总的来说,一个UiAutomation客户端应该使用一些(基于UiAutomation的)更高层次的库或者实现更高层次的方法。比如,模拟一个用户在屏幕上的点击事件需要构造并注入一个按下和一个弹起事件,然后必须调用UiAutomation的一个injectInputEvent(InputEvent, boolean)的调用来发送给操作系统

The APIs exposed by this class operate across applications enabling a client to write tests that cover use cases spanning over multiple applications. For example, going to the settings application to change a setting and then interacting with another application whose behavior depends on that setting.

这个类暴露出来的APIs可以跨应用,这样用户就可以编写可以跨越多个应用的测试用例脚本了。比如,打开系统的设置应用去修改一些设置然后再与另外一个依赖于该设置的应用进行交互(译者注:这个在instrumentation这个框架可以做不到的)。

Testing and Debugging(来自android 4.3的APIs官方改动文档)


Automated UI testing/用户界面测试自动化

The new UiAutomation class provides APIs that allow you to simulate user actions for test automation. By using the platform's AccessibilityService APIs, the UiAutomation APIs allow you to inspect the screen content and inject arbitrary keyboard and touch events.

新的UiAutomation这个类提供了一系列的APIs来允许你在测试自动化时模拟用户的操作。通过封装使用了平台上的AccessibilityService  APIs, UiAutomation APIs允许你获取窗口(控件)内容并且注入按键和触屏事件。

To get an instance of UiAutomation, call Instrumentation.getUiAutomation(). In order for this to work, you must supply the -w option with the instrument command when running your InstrumentationTestCase from adb shell.

你可以通过调用Instrumentation.getUiAutomation()来获得UiAutomation的一个实例。为了让它工作起来,当你在adb shell上运行你的InstrumentationTestCase的时候你还需要为instrument命令提供-w这个选项。

With the UiAutomation instance, you can execute arbitrary events to test your app by callingexecuteAndWaitForEvent(), passing it a Runnable to perform, a timeout period for the operation, and an implementation of the UiAutomation.AccessibilityEventFilter interface. It's within yourUiAutomation.AccessibilityEventFilter implementation that you'll receive a call that allows you to filter the events that you're interested in and determine the success or failure of a given test case.

通过UiAutomation的实例,你可以调用其executeAndWaitForEvent()对你的应用注入不同的事件来进行测试:该函数会接受一个可执行 Runnable线程对象用来执行事件注入操作,一个操作超时,以及一个实现了UiAutomation.AccessibilityEventFilter的类的实例。正是在这个UiAutomation.AccessibilityEventFilter实现类里面你会收到一个回调来让你过滤那些你喜欢的事件并决定你的测试用例是否通过。

To observe all the events during a test, create an implementation of UiAutomation.OnAccessibilityEventListenerand pass it to setOnAccessibilityEventListener(). Your listener interface then receives a call toonAccessibilityEvent() each time an event occurs, receiving an AccessibilityEvent object that describes the event.

如果要在测试时监控所有的事件,你需要创建一个UiAutomation.OnAccessibilityEventListener的实现类然后把它的实例传递给setOnAccessibilityEventListener()。你的监听接口将会在每次有事件触发的时候接收到一个发送给onAccessibilityEvent()的回调,里面的参数就是一个描述该事件的AccessibilityEvent 的对象

There is a variety of other operations that the UiAutomation APIs expose at a very low level to encourage the development of UI test tools such as uiautomator. For instance, UiAutomation can also:

 UiAutomation APIs还暴露了很多其他的低层次的操作来鼓励大家去开发如uiautomator这样的用户界面测试工具。比如UiAutomation还可以做以下事情:

  • Inject input events/注入事件
  • Change the orientation of the screen/改变屏幕的方向
  • Take screenshots/截屏

And most importantly for UI test tools, the UiAutomation APIs work across application boundaries, unlike those inInstrumentation.

最为用户界面自动化测试工具,最重要的一点是,UiAutomation APIs 可以跨应用工作,而不像Instrumentation提供了的那些APIs.

 
  

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

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

 

1.环境变量配置

和monkey以及monkeyrunner一样,uiautomator其实也是一个shell脚本,我们看最后面的关键几行:
[java] view plain copy
 
  1. CLASSPATH=${CLASSPATH}:${jars}  
  2. export CLASSPATH  
  3. 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:
[java] view plain copy
 
  1. /*     */   public static void main(String[] args)  
  2. /*     */   {  
  3. /*  74 */     Process.setArgV0("uiautomator");  
  4. /*  75 */     if (args.length >= 1) {  
  5. /*  76 */       Command command = findCommand(args[0]);  
  6. /*  77 */       if (command != null) {  
  7. /*  78 */         String[] args2 = new String[0];  
  8. /*  79 */         if (args.length > 1)  
  9. /*     */         {  
  10. /*  81 */           args2 = (String[])Arrays.copyOfRange(args, 1, args.length);  
  11. /*     */         }  
  12. /*  83 */         command.run(args2);  
  13. /*  84 */         return;  
  14. /*     */       }  
  15. /*     */     }  
  16. /*  87 */     HELP_COMMAND.run(args);  
  17. /*     */   }  
里面主要做两件事情:
  • 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:
[java] view plain copy
 
  1. /* 129 */   private static Command[] COMMANDS = { HELP_COMMAND, new RunTestCommand(), new DumpCommand(), new EventsCommand() };  
这些命令,如我们的RunTestCommand类都是继承与Command这个Launcher的静态抽象内部类:
[java] view plain copy
 
  1. /*     */   public static abstract class Command  
  2. /*     */   {  
  3. /*     */     private String mName;  
  4. /*     */       
  5. /*     */     public Command(String name)  
  6. /*     */     {  
  7. /*  40 */       this.mName = name;  
  8. /*     */     }  
  9. /*     */     public String name()  
  10. /*     */     {  
  11. /*  48 */       return this.mName;  
  12. /*     */     }  
  13. /*     */       
  14. /*     */     public abstract String shortHelp();  
  15. /*     */     public abstract String detailedOptions();  
  16. /*     */       
  17. /*     */     public abstract void run(String[] paramArrayOfString);  
  18. /*     */   }  
里面定义了一个mName的字串成员,其实对应的就是我们命令行传进来的第一个参数,大家看下子类RunTestCommand这个类的构造函数就清楚了:
[java] view plain copy
 
  1. /*     */   public RunTestCommand() {  
  2. /*  62 */     super("runtest");  
  3. /*     */   }  
然后Command类还定义了一个run的方法,注意这个方法非常重要,这个就是我们刚才分析main函数看到的第二点,是开始运行测试的地方。
好,我们返回之前的main方法,看是怎么根据‘runtest'这个我们输入的字串找到对应的RunTestCommand这个command的,我们打开findCommand这个方法:
[java] view plain copy
 
  1. /*     */   private static Command findCommand(String name) {  
  2. /*  91 */     for (Command command : COMMANDS) {  
  3. /*  92 */       if (command.name().equals(name)) {  
  4. /*  93 */         return command;  
  5. /*     */       }  
  6. /*     */     }  
  7. /*  96 */     return null;  
  8. /*     */   }  
跟我们预期一样,该方法就是循坏COMMANDS这个预定义的静态command列表,把上面提到的它们的nName取出来比较,然后找到对应的command对象的。

3. 准备运行

在获取到我们对应的命令之后,下一步我们就需要根据命令行传进来的参数来设置我们对应的command对象,以RunTestCommand为例,从main方法进入到run:
[java] view plain copy
 
  1. /*     */   public void run(String[] args)  
  2. /*     */   {  
  3. /*  67 */     int ret = parseArgs(args);   
  4.                   ...  
  5. /*  84 */     if (this.mTestClasses.isEmpty()) {  
  6. /*  85 */       addTestClassesFromJars();  
  7. /*  86 */       if (this.mTestClasses.isEmpty()) {  
  8. /*  87 */         System.err.println("No test classes found.");  
  9. /*  88 */         System.exit(-3);  
  10. /*     */       }  
  11. /*     */     }  
  12. /*  91 */     getRunner().run(this.mTestClasses, this.mParams, this.mDebug, this.mMonkey);  
  13. /*     */   }  
这里做了几个事情:
  • 67行:根据命令行参数设置RunTestCommand的命令属性
  • 84-85行:如果没有-c参数指定测试类或者指定-e class,那么默认从指定的jar包里面获取所有的测试class进行测试
  • 91行:获取testrunner并执行run方法
 

3.1 设置命令运行参数

我们进入parseArgs里面看RunTestCommand是如何根据命令行参数来设置相应的变量的:
[java] view plain copy
 
  1. /*     */   private int parseArgs(String[] args)  
  2. /*     */   {  
  3. /* 105 */     for (int i = 0; i < args.length; i++) {  
  4. /* 106 */       if (args[i].equals("-e")) {  
  5. /* 107 */         if (i + 2 < args.length) {  
  6. /* 108 */           String key = args[(++i)];  
  7. /* 109 */           String value = args[(++i)];  
  8. /* 110 */           if ("class".equals(key)) {  
  9. /* 111 */             addTestClasses(value);  
  10. /* 112 */           } else if ("debug".equals(key)) {  
  11. /* 113 */             this.mDebug = (("true".equals(value)) || ("1".equals(value)));  
  12. /* 114 */           } else if ("runner".equals(key)) {  
  13. /* 115 */             this.mRunnerClassName = value;  
  14. /*     */           } else {  
  15. /* 117 */             this.mParams.putString(key, value);  
  16. /*     */           }  
  17. /*     */         } else {  
  18. /* 120 */           return -1;  
  19. /*     */         }  
  20. /* 122 */       } else if (args[i].equals("-c")) {  
  21. /* 123 */         if (i + 1 < args.length) {  
  22. /* 124 */           addTestClasses(args[(++i)]);  
  23. /*     */         } else {  
  24. /* 126 */           return -2;  
  25. /*     */         }  
  26. /* 128 */       } else if (args[i].equals("--monkey")) {  
  27. /* 129 */         this.mMonkey = true;  
  28. /* 130 */       } else if (args[i].equals("-s")) {  
  29. /* 131 */         this.mParams.putString("outputFormat", "simple");  
  30. /*     */       } else {  
  31. /* 133 */         return -99;  
  32. /*     */       }  
  33. /*     */     }  
  34. /* 136 */     return 0;  
  35. /*     */   }  
  • 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需要用逗号分开:
    [java] view plain copy
     
    1. /*     */   private void addTestClasses(String classes)  
    2. /*     */   {  
    3. /* 181 */     String[] classArray = classes.split(",");  
    4. /* 182 */     for (String clazz : classArray) {  
    5. /* 183 */       this.mTestClasses.add(clazz);  
    6. /*     */     }  
    7. /*     */   }  
  • 其他参数处理...
 

3.2 获取测试集(类)字串列表

处理好命令行参数后RunTestCommand的run方法下一个做的事情就是检查mTestClasses这个字串类型列表是空的,根据上面的parseArgs方法的分析,如果命令行没有指定-c或者没有指定-e class,那么这个mTestClasses就为空,这种情况下就会把我们通过adb push进来的命令脚本jar包中的所有class加入到mTestClasses这个字串列表中,也就是说会执行里面的所有脚本。
 

3.3 获取TestRunner

准备好命令参数和要执行的测试类后,下一步就要获取对应的TestRunner来指导测试脚本的执行了,我们看下我们是怎么获得TestRunner的:
[java] view plain copy
 
  1. /*     */   protected UiAutomatorTestRunner getRunner() {  
  2. /* 140 */     if (this.mRunner != null) {  
  3. /* 141 */       return this.mRunner;  
  4. /*     */     }  
  5. /*     */       
  6. /* 144 */     if (this.mRunnerClassName == null) {  
  7. /* 145 */       this.mRunner = new UiAutomatorTestRunner();  
  8. /* 146 */       return this.mRunner;  
  9. /*     */     }  
  10. /*     */       
  11. /* 149 */     Object o = null;  
  12. /*     */     try {  
  13. /* 151 */       Class<?> clazz = Class.forName(this.mRunnerClassName);  
  14. /* 152 */       o = clazz.newInstance();  
  15. /*     */     } catch (ClassNotFoundException cnfe) {  
  16. /* 154 */       System.err.println("Cannot find runner: " + this.mRunnerClassName);  
  17. /* 155 */       System.exit(-4);  
  18. /*     */     } catch (InstantiationException ie) {  
  19. /* 157 */       System.err.println("Cannot instantiate runner: " + this.mRunnerClassName);  
  20. /* 158 */       System.exit(-4);  
  21. /*     */     } catch (IllegalAccessException iae) {  
  22. /* 160 */       System.err.println("Constructor of runner " + this.mRunnerClassName + " is not accessibile");  
  23. /* 161 */       System.exit(-4);  
  24. /*     */     }  
  25. /*     */     try {  
  26. /* 164 */       UiAutomatorTestRunner runner = (UiAutomatorTestRunner)o;  
  27. /* 165 */       this.mRunner = runner;  
  28. /* 166 */       return runner;  
  29. /*     */     } catch (ClassCastException cce) {  
  30. /* 168 */       System.err.println("Specified runner is not subclass of " + UiAutomatorTestRunner.class.getSimpleName());  
  31. /*     */         
  32. /* 170 */       System.exit(-4);  
  33. /*     */     }  
  34. /*     */       
  35. /* 173 */     return null;  
  36. /*     */   }  
这个类看上去有点长,但其实做的事情重要的就那么两点,其他的都是些错误处理:
  • 用户有没有在命令行通过-e runner指定TestRunner,有的话就用该TestRunner
  • 用户没有指定TestRunner的话就用默认的UiAutomatorTestRunner
 

3.4 每个方法建立junit.framework.TestCase

确定了UiAutomatorTestRunner这个TestRunner后的下一步就是调用它的run方法来指导测试用例的执行:
[java] view plain copy
 
  1. /*     */   public void run(List<String> testClasses, Bundle params, boolean debug, boolean monkey)  
  2. /*     */   {  
  3.                 ...  
  4. /*  92 */     this.mTestClasses = testClasses;  
  5. /*  93 */     this.mParams = params;  
  6. /*  94 */     this.mDebug = debug;  
  7. /*  95 */     this.mMonkey = monkey;  
  8. /*  96 */     start();  
  9. /*  97 */     System.exit(0);  
  10. /*     */   }  
传进来的参数就是我们刚才通过parseArgs方法设置的那些变量,run方法会把这些变量保存起来以便下面使用,紧跟着它就会调用一个start方法,这个方法非常重要,从建立每个测试方法对应的junit.framwork.TestCase对象到真正执行测试都在这个方法完成,所以也比较长,我们挑重要的部分进行分析,首先我们看以下代码:
[java] view plain copy
 
  1. /*     */   protected void start()  
  2. /*     */   {  
  3. /* 104 */     TestCaseCollector collector = getTestCaseCollector(getClass().getClassLoader());  
  4. /*     */     try {  
  5. /* 106 */       collector.addTestClasses(this.mTestClasses);  
  6. /*     */     }  
  7.         ...  
  8. }  
这里面调用了TestCaseCollector这个类的addTestClasses的方法,从这个类的名字我们可以猜测到它就是专门收集测试用例用的,那么我们往下跟踪下看它是怎么收集测试用例的:
[java] view plain copy
 
  1. /*     */   public void addTestClasses(List<String> classNames)  
  2. /*     */     throws ClassNotFoundException  
  3. /*     */   {  
  4. /*  52 */     for (String className : classNames) {  
  5. /*  53 */       addTestClass(className);  
  6. /*     */     }  
  7. /*     */   }  
这里传进来的就是我们上面保存起来的收集了每个class名字的字串列表。里面执行了一个for循环来把每一个类的字串拿出来,然后调用addTestClass:
[java] view plain copy
 
  1. /*     */   public void addTestClass(String className)  
  2. /*     */     throws ClassNotFoundException  
  3. /*     */   {  
  4. /*  66 */     int hashPos = className.indexOf('#');  
  5. /*  67 */     String methodName = null;  
  6. /*  68 */     if (hashPos != -1) {  
  7. /*  69 */       methodName = className.substring(hashPos + 1);  
  8. /*  70 */       className = className.substring(0, hashPos);  
  9. /*     */     }  
  10. /*  72 */     addTestClass(className, methodName);  
  11. /*     */   }  
这里可能你会奇怪为什么会查看类名字串里面是否有#号呢?其实在文章开头的时候我就有提出来,-c或者-e class指定的类名是可以支持 $className/$methodName来指定执行该className的methodName这个方法的,比如我可以指定-c majcit.com.UIAutomatorDemo.SettingsSample#testSetLanEng来指定只是测试该类里面的testSetLanEng这个方法。如果用户没有指定的话该methodName变量就设置成null,然后调用重载方法addTestClass方法:
[java] view plain copy
 
  1. /*     */   public void addTestClass(String className, String methodName)  
  2. /*     */     throws ClassNotFoundException  
  3. /*     */   {  
  4. /*  84 */     Class<?> clazz = this.mClassLoader.loadClass(className);  
  5. /*  85 */     if (methodName != null) {  
  6. /*  86 */       addSingleTestMethod(clazz, methodName);  
  7. /*     */     } else {  
  8. /*  88 */       Method[] methods = clazz.getMethods();  
  9. /*  89 */       for (Method method : methods) {  
  10. /*  90 */         if (this.mFilter.accept(method)) {  
  11. /*  91 */           addSingleTestMethod(clazz, method.getName());  
  12. /*     */         }  
  13. /*     */       }  
  14. /*     */     }  
  15. /*     */   }  
  • 84行:最终会调用 java.lang.ClassLoader的loadClass方法,通过指定类的名字来把该测试脚本类装载进来并赋予给clazz这个Class<?>变量,注意这里这个测试类还没有实例化的,真正实例化的地方是在下面的addSingleTestMethod中
  • 85-86行:如果用户用#号指定测试某一个类的某个方法,那么就直接传入参数clazz和要测试的methodName来调用addSingleTestMehod来组建我们需要的TestCase
  • 88-91行:如果用户没用#号指定测试某个类的某个方法,那么就需要循环取出该类的所有测试方法,然后每个方法调用一次addSingleTestMethod.
好,终于来到的关键点,下面我们看addSingleTestMethod是如何根据测试类clazz和它的一个方法创建一个junit.framework.TestCase对象的:
[java] view plain copy
 
  1. /*     */   protected void addSingleTestMethod(Class<?> clazz, String method) {  
  2. /* 106 */     if (!this.mFilter.accept(clazz)) {  
  3. /* 107 */       throw new RuntimeException("Test class must be derived from UiAutomatorTestCase");  
  4. /*     */     }  
  5. /*     */     try {  
  6. /* 110 */       TestCase testCase = (TestCase)clazz.newInstance();  
  7. /* 111 */       testCase.setName(method);  
  8. /* 112 */       this.mTestCases.add(testCase);  
  9. /*     */     } catch (InstantiationException e) {  
  10. /* 114 */       this.mTestCases.add(error(clazz, "InstantiationException: could not instantiate test class. Class: " + clazz.getName()));  
  11. /*     */     }  
  12. /*     */     catch (IllegalAccessException e) {  
  13. /* 117 */       this.mTestCases.add(error(clazz, "IllegalAccessException: could not instantiate test class. Class: " + clazz.getName()));  
  14. /*     */     }  
  15. /*     */   }  
  • 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运行模式:
[java] view plain copy
 
  1. /*     */   protected void start()  
  2. /*     */   {  
  3.         ...  
  4. /* 117 */     UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper();  
  5. /* 118 */     automationWrapper.connect();  
  6. /*     */       
  7.         ...  
  8. /*     */     try {  
  9. /* 132 */       automationWrapper.setRunAsMonkey(this.mMonkey);  
  10.                     ...  
  11.               }  
  12.     ...  
  13. }  
这里会初始化一个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对象的建立,那么往下:
[java] view plain copy
 
  1. /*     */   protected void start()  
  2. /*     */   {  
  3.         ...  
  4. /*     */     try {  
  5. /* 132 */       automationWrapper.setRunAsMonkey(this.mMonkey);  
  6. /* 133 */       this.mUiDevice = UiDevice.getInstance();  
  7. /* 134 */       this.mUiDevice.initialize(new ShellUiAutomatorBridge(automationWrapper.getUiAutomation()));  
  8. /*     */         
  9.            ...  
  10.        }  
  11.     ...  
  12. }  
在尝试设置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这个类的对象拥有的,而我们的测试脚本并没有继承或者拥有这个类的变量。请看以下的测试脚本:
[java] view plain copy
 
  1. package majcit.com.UIAutomatorDemo;  
  2.   
  3. import com.android.uiautomator.core.UiDevice;  
  4. import com.android.uiautomator.core.UiObject;  
  5. import com.android.uiautomator.core.UiObjectNotFoundException;  
  6. import com.android.uiautomator.core.UiScrollable;  
  7. import com.android.uiautomator.core.UiSelector;  
  8. import com.android.uiautomator.testrunner.UiAutomatorTestCase;  
  9.   
  10.   
  11. public class UISelectorFindElementTest extends UiAutomatorTestCase {  
  12.       
  13.      public void testDemo() throws UiObjectNotFoundException {    
  14.             UiDevice device = getUiDevice();  
  15.             device.pressHome();    
既然测试脚本中的getUiDevice方法不是直接从UiAutomatorTestRunner获得,那么是不是从它继承下来的UiAutomatorTestCase中获得呢?答案是肯定的,我们继续看那个UiAutomatorTestRunner中很重要的start方法:
[java] view plain copy
 
  1. /*     */   
  2. /*     */   protected void start()  
  3. /*     */   {  
  4.                 ...  
  5. /* 158 */       for (TestCase testCase : testCases) {  
  6. /* 159 */         prepareTestCase(testCase);  
  7. /* 160 */         testCase.run(testRunResult);  
  8. /*     */       }  
  9.                 ...  
  10. }  
一个for循环把我们上面创建好的所有junit.framework.TestCase对象做一个遍历,在执行之前先调用一个prepareTestCase:
[java] view plain copy
 
  1. /*     */   protected void prepareTestCase(TestCase testCase)  
  2. /*     */   {  
  3. /* 427 */     ((UiAutomatorTestCase)testCase).setAutomationSupport(this.mAutomationSupport);  
  4. /* 428 */     ((UiAutomatorTestCase)testCase).setUiDevice(this.mUiDevice);  
  5. /* 429 */     ((UiAutomatorTestCase)testCase).setParams(this.mParams);  
  6. /*     */   }  
这个方法所做的事情就解决了我们刚才的疑问:第428行,把当前UiAutomatorTestRunner拥有的这个已经连接到AccessibilityService的UiObject对象,通过我们测试脚本的父类的setUiDevice方法设置到我们的TestCase脚本对象里面
[java] view plain copy
 
  1. /*     */   void setUiDevice(UiDevice uiDevice)  
  2. /*     */   {  
  3. /* 100 */     this.mUiDevice = uiDevice;  
  4. /*     */   }  
这样我们测试脚本每次执行getUiDevice的时候就能直接取得该对象了:
[java] view plain copy
 
  1. /*     */   public UiDevice getUiDevice()  
  2. /*     */   {  
  3. /*  72 */     return this.mUiDevice;  
  4. /*     */   }  
从整个过程可以看到,UiObject的对象我们在测试脚本上是不用初始化的,它是在运行时由我们默认的TestuRunner -- UiAutomatorTestRunner 传递进来的,这个我们作为测试人员是不需要知道这一点的。
好了,到了现在就真的可以直接触发junit.framework.TestCase的run方法来让测试跑起来了,这里要注意我们之前的分析,并不是测试脚本的所有方法都同时调用run执行的,而是一个方法调用一次run方法。
 

5. 扩展阅读:junit框架如何通过方法名执行测试方法

下面如果有兴趣知道juint框架是如何通过3.4节建立junit.framework.TestCase时调用setName方法设置的测试方法名字来调用执行对应方法的可以继续往下跟踪run方法,它最终会进入到junit.framework.TestCase的runTest方法
[java] view plain copy
 
  1. protected void runTest() throws Throwable {  
  2.     assertNotNull(fName); // Some VMs crash when calling getMethod(null,null);  
  3.     Method runMethod= null;  
  4.     try {  
  5.         // use getMethod to get all public inherited  
  6.         // methods. getDeclaredMethods returns all  
  7.         // methods of this class but excludes the  
  8.         // inherited ones.  
  9.         runMethod= getClass().getMethod(fName, (Class[])null);  
  10.     } catch (NoSuchMethodException e) {  
  11.         fail("Method \""+fName+"\" not found");  
  12.     }  
  13.     if (!Modifier.isPublic(runMethod.getModifiers())) {  
  14.         fail("Method \""+fName+"\" should be public");  
  15.     }  
  16.   
  17.     try {  
  18.         runMethod.invoke(this, (Object[])new Class[0]);  
  19.     }  
  20.     catch (InvocationTargetException e) {  
  21.         e.fillInStackTrace();  
  22.         throw e.getTargetException();  
  23.     }  
  24.     catch (IllegalAccessException e) {  
  25.         e.fillInStackTrace();  
  26.         throw e;  
  27.     }  
  28. }  
从中可以看到它会尝试通过getClass().getMethod方法获得这个junit.framework.TestCase所代表的测试脚本的于我们设置的fName一致的方法,然后才会去执行。