我们可以看到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帮助:
  • 支持三个子命令:runtest/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脚本,我们看最后面的关键几行:
  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:
  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:
  1. /* 129 */   private static Command[] COMMANDS = { HELP_COMMAND, new RunTestCommand(), new DumpCommand(), new EventsCommand() };

这些命令,如我们的RunTestCommand类都是继承与Command这个Launcher的静态抽象内部类:

  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这个类的构造函数就清楚了:

  1. /*     */   public RunTestCommand() {
  2. /*  62 */     super("runtest");
  3. /*     */   }

然后Command类还定义了一个run的方法,注意这个方法非常重要,这个就是我们刚才分析main函数看到的第二点,是开始运行测试的地方。

好,我们返回之前的main方法,看是怎么根据‘runtest'这个我们输入的字串找到对应的RunTestCommand这个command的,我们打开findCommand这个方法:
  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:
  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是如何根据命令行参数来设置相应的变量的:
  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需要用逗号分开:
    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的:
  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方法来指导测试用例的执行:
  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对象到真正执行测试都在这个方法完成,所以也比较长,我们挑重要的部分进行分析,首先我们看以下代码:

  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的方法,从这个类的名字我们可以猜测到它就是专门收集测试用例用的,那么我们往下跟踪下看它是怎么收集测试用例的:

  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:

  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方法:

  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对象的:
  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运行模式:

  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对象的建立,那么往下:
  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这个类的对象拥有的,而我们的测试脚本并没有继承或者拥有这个类的变量。请看以下的测试脚本:
  1. package majcit.com.UIAutomatorDemo;
  2. import com.android.uiautomator.core.UiDevice;
  3. import com.android.uiautomator.core.UiObject;
  4. import com.android.uiautomator.core.UiObjectNotFoundException;
  5. import com.android.uiautomator.core.UiScrollable;
  6. import com.android.uiautomator.core.UiSelector;
  7. import com.android.uiautomator.testrunner.UiAutomatorTestCase;
  8. public class UISelectorFindElementTest extends UiAutomatorTestCase {
  9. public void testDemo() throws UiObjectNotFoundException {
  10. UiDevice device = getUiDevice();
  11. device.pressHome();

既然测试脚本中的getUiDevice方法不是直接从UiAutomatorTestRunner获得,那么是不是从它继承下来的UiAutomatorTestCase中获得呢?答案是肯定的,我们继续看那个UiAutomatorTestRunner中很重要的start方法:

  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:

  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脚本对象里面

  1. /*     */   void setUiDevice(UiDevice uiDevice)
  2. /*     */   {
  3. /* 100 */     this.mUiDevice = uiDevice;
  4. /*     */   }
这样我们测试脚本每次执行getUiDevice的时候就能直接取得该对象了:
  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方法
  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. try {
  17. runMethod.invoke(this, (Object[])new Class[0]);
  18. }
  19. catch (InvocationTargetException e) {
  20. e.fillInStackTrace();
  21. throw e.getTargetException();
  22. }
  23. catch (IllegalAccessException e) {
  24. e.fillInStackTrace();
  25. throw e;
  26. }
  27. }

从中可以看到它会尝试通过getClass().getMethod方法获得这个junit.framework.TestCase所代表的测试脚本的于我们设置的fName一致的方法,然后才会去执行。

【转】UIAutomator源码分析之启动和运行的更多相关文章

  1. UIAutomator源码分析之启动和运行

    通过上一篇<Android4.3引入的UiAutomation新框架官方简介>我们可以看到UiAutomator其实就是使用了UiAutomation这个新框架,通过调用Accessibi ...

  2. UiAutomator源码分析之UiAutomatorBridge框架

    上一篇文章<UIAutomator源码分析之启动和运行>我们描述了uitautomator从命令行运行到加载测试用例运行测试的整个流程,过程中我们也描述了UiAutomatorBridge ...

  3. UiAutomator源码分析之注入事件

    上一篇文章<UiAutomator源码分析之UiAutomatorBridge框架>中我们把UiAutomatorBridge以及它相关的类进行的描述,往下我们会尝试根据两个实例将这些类给 ...

  4. Appium Android Bootstrap源码分析之启动运行

    通过前面的两篇文章<Appium Android Bootstrap源码分析之控件AndroidElement>和<Appium Android Bootstrap源码分析之命令解析 ...

  5. UiAutomator源码分析之获取控件信息

    根据上一篇文章<UiAutomator源码分析之注入事件>开始时提到的计划,这一篇文章我们要分析的是第二点: 如何获取控件信息 我们在测试脚本中初始化一个UiObject的时候通常是像以下 ...

  6. Appium Server 源码分析之启动运行Express http服务器

    通过上一个系列Appium Android Bootstrap源码分析我们了解到了appium在安卓目标机器上是如何通过bootstrap这个服务来接收appium从pc端发送过来的命令,并最终使用u ...

  7. Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3.0 ARMv7)

    http://blog.chinaunix.net/uid-20543672-id-3157283.html Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3 ...

  8. Linux内核源码分析--内核启动之(6)Image内核启动(do_basic_setup函数)(Linux-3.0 ARMv7)【转】

    原文地址:Linux内核源码分析--内核启动之(6)Image内核启动(do_basic_setup函数)(Linux-3.0 ARMv7) 作者:tekkamanninja 转自:http://bl ...

  9. Linux内核源码分析--内核启动之(4)Image内核启动(setup_arch函数)(Linux-3.0 ARMv7)【转】

    原文地址:Linux内核源码分析--内核启动之(4)Image内核启动(setup_arch函数)(Linux-3.0 ARMv7) 作者:tekkamanninja 转自:http://blog.c ...

随机推荐

  1. ARCH模型

    ARCH模型的基本思想 ARCH模型的基本思想是指在以前信息集下,某一时刻一个噪声的发生是服从正态分布.该正态分布的均值为零,方差是一个随时间变化的量(即为条件异方差).并且这个随时间变化的方差是过去 ...

  2. Servlet与JSP的区别(转)

    原文链接:Servlet与JSP的区别 两者之间的联系和区别 [1]JSP第一次运行的时候会编译成Servlet,驻留在内存中以供调用. [2]JSP是web开发技术,Servlet是服务器端运用的小 ...

  3. C/C++ 分支预测(likely unlikely)

    看一些代码时,会遇到likely unlikely, 查了查网上的资料,结合自己的理解记录一下. 1. 一些概念 指令周期是指执行一条指令所需要的时间,一般由若干个机器周期组成,是从取指令.分析指令到 ...

  4. webpack打包器简单入门

    概念 webpack是一个现代javascript应用程序的模块打包器. 当webpack处理你的应用程序时,它会递归构建一个依赖图(包含了你的应用程序所需要每个模块),然后把这些模块打包到少数几个b ...

  5. 插件化 VirtualAPK 简介 体验 MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  6. 10款CSS3按钮 - 程序员再也不用为按钮设计而发愁了...

    这次主要给大家分享10款风格各异的CSS3按钮,如果你希望你的页面也能有很炫的样式,那么我相信这10款CSS3按钮就非常适合你,而且每一款都整理了源代码供参考,一起来看看吧. 1.绚丽的CSS3发光按 ...

  7. Oracle数据库将varchar类型的字段改为clob类型

    alter table pwlp_accuse_info modify INSTRUCTION_STYLE long; alter table pwlp_accuse_info modify INST ...

  8. GridViewColumn.CellTemplate

    <GridViewColumn Header="Sig" Width="210"> <GridViewColumn.CellTemplate& ...

  9. 怎样将 MySQL 迁移到 MariaDB 上

    自从甲骨文收购 MySQL 后,由于甲骨文对 MySQL 的开发和维护更多倾向于闭门的立场,很多 MySQL 的开发者和用户放弃了它.在社区驱动下,促使更多人移到 MySQL 的另一个叫 MariaD ...

  10. 向windows添加环境变量

    以NASM为例,软件安装完毕后,启动Windows操作系统的命令窗口,在安装目录(比如C:\Program Files\NASM)下运行nasm是ok的,但是在其他任意目录下运行nasm就会报错. 这 ...