【Android测试】【第十三节】Uiautomator——如何组织好你的测试代码(项目实战)

 版权声明:本文出自胖喵~的博客,转载必须注明出处。

    转载请注明出处:http://www.cnblogs.com/by-dream/p/4996000.html

前言


  前面我们已经了解Uiautomator的基本知识,并且学习了API的用法,因此对于我们来说完成一个UI自动化测试脚本并不难,但是如何将UI自动化应用在实际的项目中,帮我们提高测试的效率呢?本节我们就说说,UI自动化应该怎么去完成。

  我们以微信"小视屏"这个功能为例,来完成本次自动化测试的讲解。(鉴于隐私原因,默认在执行脚本前,微信已经是登录状态)


分析


  当我们要完成一个自动化时,需要考虑这个用例需要怎么设计,需要测试哪些项,怎么验证,出现错误时应该如何处理。

  首先需要明确一点,并不是所有需求文档上提到的功能,我们都必须用自动化方式去验证,由于UI自动化本身的局限性,UI自动化的可行度不是100%的准确,因此我们只对“小视屏”的卖点功能进行自动化验证,你也可以理解为对该功能做一个冒烟测试。

  小视屏功能的入口一共是三个,分别是下面这三个地方:

【Android测试】【第十三节】Uiautomator——如何组织好你的测试代码(项目实战)  

  我们除了要验证这地方的入口外,还需要在其中一处完成对小视屏的发送,并且验证小视屏发送成功。因此我们可以按照下面流程来进行测试脚本的编写,流程图如下所示:

 【Android测试】【第十三节】Uiautomator——如何组织好你的测试代码(项目实战)

编码前准备


  有了流程图之后,不要迫不及待的编码。编码之前也需要考虑考虑,是否有一些公共的方法可以提取出来做为一个单独的函数呢?

1、点击操作

  首先,点击的操作是Uiautomator中用的最多的,而根据控件id和text来做为索引则是更多的。因此我们封装如下的内容:

    /* 定义“通过哪种方式来获得uiselector”的int标识,
       如果以后想添加别的方法(例如 通过description 来获取),则可以参考此形式进行扩充 */

    final int CLICK_ID = 2000;
    final int CLICK_TEXT = 2001;

    /* 实现具体的外部可以调用的函数 */

    // 通过id来进行点击操作
    public boolean ClickById(String id)
    {
        return ClickByInfo(CLICK_ID, id);
    }
    // 通过text来进行点击操作
    public boolean ClickByText(String text)
    {
        return ClickByInfo(CLICK_TEXT, text);
    }

    /* 封装出通用的点击方法,供上面的public函数调用
       如果以后想添加别的方法(例如 通过description 来获取),则可以在switch中扩充 */
    private boolean ClickByInfo(int CLICK, String str)
    {
        UiSelector uiselector = null;
        // switch根据不同的CLICK标识,创建出UiSelector的对象
        switch(CLICK)
        {
        case CLICK_ID:   uiselector = new UiSelector().resourceId(str); break;
        case CLICK_TEXT: uiselector = new UiSelector().text(str);       break;
        default: return false;
        }
        // 根据UiSelector对象构造出UiObject的对象
        UiObject uiobject = new UiObject(uiselector);
        // 判断该控件是否存在
        if(!uiobject.exists())
        {
            return false;
        }
        // 点击
        try
        {
            uiobject.click();
        } catch (UiObjectNotFoundException e)
        {
            e.printStackTrace();
        }
        return true;
    }

  使用上面我的方法封装之后,你只需要调用  ClickByText("通讯录");  即可完成对"通信录" 这个控件的点击,并且在因为异常情况获取不到该控件的时候,也不会报出异常。

  然而,我们去点击一个控件的时候,当它出现找不到的情况的时候,这有可能就是bug了,我们需要将其记录下来,并且记录下当时的现场,一般采用截图的方法,以便我们查问题时候能更直观的了解到当时机器一个运行情况。因此接下来,我要说说截图和异常处理。

2、截屏和异常处理

  上面的代码中,当UiObject对象找不到的时候,我们只是返回了一个false,告诉调用者这次调用失败了,但是为什么失败,怎么避免这样的失败,并没有记录下来。因此在这段代码中,我们需要加以下的内容:

    private boolean ClickByInfo(int CLICK, String str)
    {
        ....
        // 判断该控件是否存在
        if(!uiobject.exists())
        {
            TakeScreen(getUiDevice(), str+"-not-find");
            return false;
        }
       ....
    }
    
    /* 保存屏幕截图
       参数descrip 为 描述该截图的内容 */
    public void TakeScreen(UiDevice device, String descrip) 
    {
        // 取得当前时间
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(System.currentTimeMillis());
        String datestr = calendar.get(Calendar.HOUR_OF_DAY) + "_" + calendar.get(Calendar.MINUTE) + "_" + calendar.get(Calendar.SECOND);
        
        // 保存文件
        File files = new File("/mnt/sdcard/"+datestr+"_"+descrip+".jpg");    
        device.takeScreenshot(files);        
    }

  这样当我们在调用 ClickByText("通讯录"); 找不到控件的时候,我们的脚本就会自动截取当时屏幕的图像保存在我们的手机中(如下图),这样我们只需打开图片,就知道当时发生了什么,为什么没有找到该控件:

   【Android测试】【第十三节】Uiautomator——如何组织好你的测试代码(项目实战)

   看似完美的方案,其实在实际运行中只是帮我们记录了这个控件这一时刻点击失败的原因,而我们想要的是,脚本在调用了这个方法后,尽最大的可能帮我们点击成功。举一个简单的例子:

【Android测试】【第十三节】Uiautomator——如何组织好你的测试代码(项目实战)

  这是我们写脚本中经常遇到的一个问题,我们需要 ‘在A页面上点击“进入”按钮,跳转到B页面,然后点击B页面上的“保存”按钮’ 完成我们的操作。

  一般我们的写法是:

ClickByText("进入");
ClickByText("保存");

  然而当我们的手机特别卡,或者是页面承载太多东西的时候,当你调用了点击“进入”按钮后,B页面没有及时的跳转出来,这个时候调用B页面上的“保存”按钮,就会出现异常,而如果你没有按照我上面的方案去实现的话,系统就会抛出异常,而使用了我上面的方案之后,系统虽然不会抛出异常,而且会在你找不到B页面的“保存”按钮时截取当前的屏幕,你完全可以根据截图来判断出来:当是没有找到“保存”按钮的原因是,当时的B页面还没有跳转出来。然而在这个时候,我最希望的并不是看到日志告诉我说哪里哪里失败了,而是想让这次的点击效果生效。

  那么怎么解决这个问题呢?相信很多亲手写过Uiautomator脚本的朋友都知道,在两个操作直接加如sleep,没错,这是解决方案,那么究竟应该slepp多久呢?因为不同的手机响应时间是不一样的,如果sleep太短就依然存在上述问题;如果sleep太长的话,无疑使得脚本的运行变的缓慢,多出写无用的sleep。因此我们需要下面的方案解决:

    private boolean ClickByInfo(int CLICK, String str)
    {
        ....
        // 判断该控件是否存在
        if(!uiobject.exists())
        {
            TakeScreen(getUiDevice(), str+"-not-find");
            return false;
        }
        int i = 0;
        while (!uiobject.exists() && i<5)
        {                    
            sleep(500);
            if (i== 4)
            {
                TakeScreen(getUiDevice(), str+"-not-find");
        return false;  } i
++; } .... }

  我们去掉了if判断的代码,改为在while循环中等待这个控件的出现,一共等待5次,如果到了第五次,它还没有出现的话,那么我们就认为它真的不会出现了,这个时候去截屏比第一次就没有找到更加的有意义。当然如果你还想提高你的UI自动化的健壮性,那么这里还可以加一个类似这样的函数:

    /* 封装出通用的点击方法,供上面的public函数调用
       如果以后想添加别的方法(例如 通过description 来获取),则可以在switch中扩充 */
    private boolean ClickByInfo(int CLICK, String str)
    {
        ....
        // 判断该控件是否存在      
        int i = 0;
        while (!uiobject.exists() && i<5)
        {
            SolveProblems();
            sleep(500);
            if (i== 4)
            {
                TakeScreen(getUiDevice(), str+"-not-find");
                return false;
            }
            i++;        
        }
        ....
    }
    
    /**
     * 当进不下去的时候,使用该方法,例如可能是出现了一些对话框遮挡,该方法会把对话框干掉*/
    private void SolveProblems()
    {        
        ....
    }

  这个 SolveProblems() 函数主要是用来解决一些“麻烦”的,例如我们在操作地图的时候,当gps信号不好的时候,就会弹出下面的对话框: 

【Android测试】【第十三节】Uiautomator——如何组织好你的测试代码(项目实战)

  由于出现的对话框,遮挡住了我们的Activity,影响我们对界面上ui元素的获取,这个时候,我们就可以在SolveProblems() 加入这样一断逻辑:当出现“开启gps”对话框的时候,就点击“残忍的拒绝”,将此对话框给关掉,这样while的判断条件再次执行的时候,就可以成功获取到你想要的元素。下面这段对话主要为了加深你对SolveProblems() 这个函数的理解:

【Android测试】【第十三节】Uiautomator——如何组织好你的测试代码(项目实战)

  所以说这个SolveProblems()才是提高UI自动化成功率的关键,因为每个App都有自己的特征,因此这部分的内容,需要你们在平时的日积月累中才能总结出来,当你有了一个足够多的经验库之后,你的App几乎不会再因为外界因素而导致失败了。经过我自己在我项目上的尝试,效果非常的显著。

3、日志

  日志的重要性不言而喻,当我们在自动化执行的过程中,肯定不会一直盯着屏幕观察,因此日志使我们最依靠的东西。关于日志的记录方法多种多样,我这里提供下我是怎么在Uiautomator中打印日志的:

  public String m_logpathString = "/mnt/sdcard/PerformanceLog.txt";

  public
void UiAutomationLog(String str) { // 取得当前时间 Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(System.currentTimeMillis()); String datestr = calendar.get(Calendar.HOUR_OF_DAY) + ":" + calendar.get(Calendar.MINUTE) + ":" + calendar.get(Calendar.SECOND) + calendar.get(Calendar.MILLISECOND) + ":"; FileWriter fwlog = null; try { fwlog = new FileWriter(m_logpathString, true); fwlog.write(datestr + str + " "); System.out.println(datestr + str); fwlog.flush(); } catch (IOException e) { e.printStackTrace(); } finally { try { fwlog.close(); } catch (IOException e) { e.printStackTrace(); } } }

  接下来就是把这个函数加在一些关键的地方,当出错的时候,方便我们排查问题即可。下面是脚本打出来的日志格式:

【Android测试】【第十三节】Uiautomator——如何组织好你的测试代码(项目实战)

实现


  总结上面的所有代码,我们把这些放到一个公共的方法中,这样我们的脚本就可以直接引入这个类,然后直接的进行调用这些函数了。公共方法和测试的脚本我们单独分开,像下面一样:

【Android测试】【第十三节】Uiautomator——如何组织好你的测试代码(项目实战)

  首先附上公共方法的完整源代码:

  1 package QQ;
  2 
  3 import java.io.File;
  4 import java.io.FileWriter;
  5 import java.io.IOException;
  6 import java.util.Calendar;
  7 
  8 import com.android.uiautomator.core.UiDevice;
  9 import com.android.uiautomator.core.UiObject;
 10 import com.android.uiautomator.core.UiObjectNotFoundException;
 11 import com.android.uiautomator.core.UiSelector;
 12 import com.android.uiautomator.testrunner.UiAutomatorTestCase;
 13 
 14 public class UiautomatorAssistant extends UiAutomatorTestCase
 15 {
 16     /* UiDevice对象*/
 17     UiDevice mdevice;
 18     
 19     /* log地址 */
 20     public String m_logpathString = "/mnt/sdcard/PerformanceLog.txt";
 21     
 22     /* 定义“通过哪种方式来获得uiselector”的int标识,
 23        如果以后想添加别的方法(例如 通过description 来获取),则可以参考此形式进行扩充 */
 24 
 25     final int CLICK_ID = 2000;
 26     final int CLICK_TEXT = 2001;
 27 
 28     /*构造传入UiDevice对象*/
 29     UiautomatorAssistant(UiDevice device)
 30     {
 31         mdevice =device;
 32     }
 33     
 34     
 35     /* 实现具体的外部可以调用的函数 */
 36 
 37     // 通过id来进行点击操作
 38     public boolean ClickById(String id)
 39     {
 40         return ClickByInfo(CLICK_ID, id);
 41     }
 42     // 通过text来进行点击操作
 43     public boolean ClickByText(String text)
 44     {
 45         return ClickByInfo(CLICK_TEXT, text);
 46     }
 47 
 48     /* 封装出通用的点击方法,供上面的public函数调用
 49        如果以后想添加别的方法(例如 通过description 来获取),则可以在switch中扩充 */
 50     private boolean ClickByInfo(int CLICK, String str)
 51     {
 52         UiSelector uiselector = null;
 53         // switch根据不同的CLICK标识,创建出UiSelector的对象
 54         switch(CLICK)
 55         {
 56         case CLICK_ID:      uiselector = new UiSelector().resourceId(str); break;
 57         case CLICK_TEXT: uiselector = new UiSelector().text(str); break;
 58         default: return false;
 59         }
 60         // 根据UiSelector对象构造出UiObject的对象
 61         UiObject uiobject = new UiObject(uiselector);
 62         // 判断该控件是否存在
 63         int i = 0;
 64         while (!uiobject.exists() && i<5)
 65         {
 66             SolveProblems();
 67             sleep(500);
 68             if (i== 4)
 69             {
 70                 TakeScreen(str+"-not-find");
 71                 return false;
 72             }
 73             i++;        
 74         }
 75         // 点击
 76         try
 77         {
 78             UiAutomationLog("click type:"+CLICK+" content:"+str );
 79             uiobject.click();
 80         } catch (UiObjectNotFoundException e)
 81         {
 82             e.printStackTrace();
 83         }
 84         return true;
 85     }
 86     
 87     /* 当进不下去的时候,使用该方法,例如可能是出现了一些对话框遮挡,该方法会把对话框干掉 */
 88     private void SolveProblems()
 89     {        
 90         
 91     }
 92     
 93     /* 保存屏幕截图
 94          参数descrip 为 描述该截图的内容 */
 95     public void TakeScreen(String descrip) 
 96     {
 97         // 取得当前时间
 98         Calendar calendar = Calendar.getInstance();
 99         calendar.setTimeInMillis(System.currentTimeMillis());
100         String datestr = calendar.get(Calendar.HOUR_OF_DAY) + "_" + calendar.get(Calendar.MINUTE) + "_" + calendar.get(Calendar.SECOND);
101         
102         // 保存文件
103         File files = new File("/mnt/sdcard/"+datestr+"_"+descrip+".jpg");    
104         UiAutomationLog("TakeScreen: " + datestr+"_"+descrip+".jpg");
105         mdevice.takeScreenshot(files);        
106     }
107     
108     /* 打log记录在手机中 */
109     public void UiAutomationLog(String str) 
110     {
111         // 取得当前时间
112         Calendar calendar = Calendar.getInstance();
113         calendar.setTimeInMillis(System.currentTimeMillis());
114         String datestr = calendar.get(Calendar.HOUR_OF_DAY) + ":" + calendar.get(Calendar.MINUTE) + ":" + calendar.get(Calendar.SECOND) + calendar.get(Calendar.MILLISECOND) + ":";
115 
116         FileWriter fwlog = null;
117         try
118         {
119             fwlog = new FileWriter(m_logpathString, true);
120             fwlog.write(datestr + str + "
");
121             System.out.println(datestr + str);
122             fwlog.flush();
123 
124         } catch (IOException e)
125         {
126             e.printStackTrace();
127         } finally
128         {
129             try
130             {
131                 fwlog.close();
132             } catch (IOException e)
133             {
134                 e.printStackTrace();
135             }
136         }
137     }
138 }

  这个类需要注意的就是31行这里的UiDevice对象需要从测试类中传递过来。否则无法得到UiDevice对象。

  好,接下来我们看看最终实现的脚本的源码:

 1 package QQ;
 2 
 3 import java.io.IOException;
 4 
 5 import com.android.uiautomator.core.UiObject;
 6 import com.android.uiautomator.core.UiObjectNotFoundException;
 7 import com.android.uiautomator.core.UiSelector;
 8 import com.android.uiautomator.testrunner.UiAutomatorTestCase;
 9 
10 public class Test_wechat extends UiAutomatorTestCase
11 {
12     
13     UiautomatorAssistant uiautomatorAssistant ;
14     
15     public void testDemo() throws IOException, UiObjectNotFoundException {
16         
17         uiautomatorAssistant = new UiautomatorAssistant(getUiDevice());
18         
19         // 启动App
20         Runtime.getRuntime().exec("am start com.tencent.mm/com.tencent.mm.ui.LauncherUI");
21         sleep(3000);
22         
23         /*----------------------- 验证第一种小视频的打开方式------------------------------------*/
24         uiautomatorAssistant.ClickByText("通讯录");
25         // 点击一个好友
26         uiautomatorAssistant.ClickById("com.tencent.mm:id/gx");
27         // 点击发消息
28         uiautomatorAssistant.ClickByText("发消息");
29         // 点击发送栏的“+”
30         uiautomatorAssistant.ClickById("com.tencent.mm:id/wm");
31         // 点击小视频
32         uiautomatorAssistant.ClickByText("小视频");
33         // 验证第一种小视频打开方式
34         UiObject obj_anzhupaiObject = new UiObject(new UiSelector().text("按住拍"));
35         if (obj_anzhupaiObject.exists())
36         {
37             uiautomatorAssistant.UiAutomationLog( "第一次进入小视频的方法测试通过");
38         }
39         else {
40             uiautomatorAssistant.TakeScreen("第一次进入小视频的方法测试不通过");
41         }
42         
43         
44         //  第二种、第三种类似第一种的方法,这里省略。
45         //  在我代码还没完成的时候,微信发出了新版本,屏蔽了第三种下拉的方式,打开小视频。
46     }
47         
48 }

  很明显可以看到,使用了封装函数之后,代码的可读性大大的增强,而且很好的维护,要完成一个其他的case也可以轻而易举。后续如果读者朋友有需要完成Uiautomator脚本的不妨可以使用我的公共类来辅助你完成你的脚本,这样你就可以更加高效,快速的完成一个失败率降到最低的UI自动化测试脚本。