功能测试大揭秘(2)—— Appium基础篇

作者:超级管理员 更新时间:2017-09-13 11:57:28 来源:未知 点击:15642
  上文回顾  上一篇为大家介绍了如何通过appium桌面客户端的方式来快速搭建appium环境,桌面客户端的appium版本目前为1.6.4,更新稍慢于appium项目,但目前已经支持在线更新,大家
   上文回顾
  上一篇为大家介绍了如何通过appium桌面客户端的方式来快速搭建appium环境,桌面客户端的appium版本目前为1.6.4,更新稍慢于appium项目,但目前已经支持在线更新,大家不用再有客户端版本过低的顾虑。
  接下来将介绍如何使用python来开发appium功能 测试脚本,包括启动、控件定位、操作、函数封装、组织用例五个部分。
   启动
  Appium启动时需要指定一些通用配置,统称为Desired Capabilities,具体的一些参数可以参考Appium服务器初始化参数。这里介绍一些通用的参数与一些常见的问题。
   automationName
   自动化测试的引擎,Appium (默认)、Selendroid、Uiautomator2。Appium使用的是UI Automator v1,相比之下UI Automator v2修复了一些v1的bug,在结构上也有一些优化。对于Android7.0以上的系统,UI Automator v1可能在查找控件时出现超时导致appium服务端报错,这时候可以考虑改用Uiautomator2。
   platformName
   手机 操作系统
   platformVersion
  手机操作系统版本。
   deviceName
  手机类型
   app
  待测app的路径
   newCommandTimeout
  两条appium命令间的最长时间间隔,若超过这个时间,appium会自动结束并退出app
   noReset, fullReset
  noReset 不要在会话前重置应用状态。默认值false。 fullReset ( Android) 通过卸载而不是清空数据来重置应用状态。在Android上, 这也会在会话结束后自动清除被测应用。默认值false。
   unicodeKeyboard, resetKeyboard
  在输入的时候,可能出现键盘挡住控件的情况,这时候需要使用 appium 提供的输入法(支持输入多语言,没有键盘 ui ),unicodeKeyboard 为 true 表示使用 appium-ime 输入法。 resetKeyboard 表示在测试结束后切回系统输入法。
   appActivity, appPackage
  appActivity与appPackage指用于启动待测app的activityName与packageName,appium(1.6.4)已经支持activityName与packageName的自动检测,这两个参数已经可以省略了
  appWaitActivity, appWaitPackage
  appium需要等待的activityName与packageName,与appActivity不同的是,对于有启动动画的app来说,appWaitActivity应该是启动activity消失后出现的activity。这两个参数可以指定多个。
  有了以上介绍的这些参数,我们可以启动appium并开始测试app了。将desired capibilities进行封装,python脚本如下:
  from appium import webdriver
  def get_desired_capabilities():
      desired_caps = {
          'platformName': 'Android',
          'platformVersion': '18',
          'deviceName': 'mqcDevice',
          'udid': 'a05aacaf7d53',
          'app': "D:\\appium\\alicrowdtest.apk",
          'newCommandTimeout': 60,
          'automationName': 'appium',
          'unicodeKeyboard': True,
          'resetKeyboard': True,
          'appWaitActivity': 'com.yunos.mqc.view.activity.MainActivity',
      }
      return desired_caps
  def setUp():
      # 获取我们设定的capabilities,通知Appium Server创建相应的会话。
      desired_caps = get_desired_capabilities()
      # 获取server的地址。
      uri = "http://localhost:4723/wd/hub"
      # 创建会话,得到driver对象,driver对象封装了所有的设备操作。下面会具体讲。
      driver = webdriver.Remote(uri, desired_caps)
      return driver
  if __name__ == '__main__':
      driver = setUp()
      // 打印当前activity
      print driver.current_activity
  控件定位
  android sdk的tools目录下自带一个元素查看工具-uiautomatorviewer,通过这个工具可以获取到app的各个元素属性,辅助我们编写相关的脚本,uiautomatorviewer的界面如下:
  如图为了定位 尚未登录 这个控件,我们推荐以下几种方法:
  xpath: xpath定位效率低,但胜在定位准确,在控件没有明显的唯一特征时,xpath的优势就体现出来了。使用xpath时可以选择以控件树的最近公共祖先的节点开始生成查询路径。
  driver.find_element_by_xpath("//android.widget.LinearLayout[1]/android.widget.FrameLayout[1]/android.widget.ScrollView[1]/android.widget.LinearLayout[1]/android.widget.RelativeLayout[1]/android.widget.LinearLayout[1]/1android.widget.TextView[1]")
  id: 为了定位方便,开发一般会为部分控件添加resource-id属性,一般来说可以在一个页面中唯一确定一个控件,对于存在多个相同resource-id控件的页面,可以通过index来定位这些控件中的某一个
  driver.find_element_by_id("com.yunos.mqc:id/user_nickname")
  或者      
  driver.find_elements_by_id("com.yunos.mqc:id/user_nickname")[index]
  text: 定位控件较为简洁清晰地一种方式,就是通过text来查找控件。
  #1.5以上的版本已弃用
  driver.find_element_by_name("尚未登录")
  #新版appium使用xpath来实现这个功能
  driver.find_element_by_xpath("//*[@text='%s']" % (text))  
   操作
  swipe: 很多App都会有引导页需要左滑,这里提供一个左滑的例子,注意,滑动速度不应太快,否则容易导致引导页滑动不成功。
  window_size = driver.get_window_size()
  driver.swipe(start_x=window_size["width"] * 0.9,
               start_y=window_size["height"] * 0.5,
               end_x=window_size["width"] * 0.1,
               end_y=window_size["height"] * 0.5, 500)  
  TouchAction: touchaction可以用来实现点击坐标,滑动等操作,需要注意的是在android系统上,TouchAction的move_to函数是相对坐标。
  #点击操作
  TouchAction(driver).press(None, x, y).release().perform()
  #滑动操作
  TouchAction(driver).press(None, x, y).wait(20).move_to(None, dx, dy).release().perform() 
Scroll: 对于ListView这样的控件,若要滑动到某个控件位置,按照坐标滑动的方式很难适配不同分辨率的手机,这时候需要考虑使用scroll的方式进行滑动。
  #从控件 el1 滑动到 el2
  driver.scroll(el1, el2)  
  Keyevent: 通过系统按键可以实现很多操作,如HOME、POWER、MENU、音量等功能。具体的一些keyevent可以到https://developer.android.google.cn/reference/android/view/KeyEvent.html查看。
  #HOME 键
  driver.keyevent(3) 
  long_press: 利用TouchAction,可以实现长按操作:
  #长按 el 控件 20s
  action = TouchAction(driver)
  action.long_press(el, 20000).perform()
  sleep(20)
  action.release().perform()  
  hidekeyboard: 若 desiredcapbility 没有指定使用 unicodeKeyboard,在输入的时候需要注意键盘 ui 可能会挡住控件的情况,这时候使用 hidekeyboard 函数隐藏键盘
  # python
  driver.hide_keyboard() 
   函数封装
  在写脚本的时候,把一些常用的功能合理封装起来,能够大大提高脚本执行的成功率。
  滑动函数: 通过一组坐标点来进行滑动,从而实现一些曲线形的滑动。我们可以实现一个函数用来支持曲线滑动,输入为形如[[x1, y1],[x2, y2]]的一组坐标点。
  def swipe(points):
      last_x = 0
      last_y = 0
      swipe_action = TouchAction(driver)
      for i in range(0, len(points)):
          x=points[i][0]
          y=points[i][1]
          if i == 0:
              swipe_action = swipe_action.press(None, x, y).wait(20)
          elif i == (len(points) - 1):
              swipe_action = swipe_action.move_to(None, x - last_x, y - last_y).release()
              swipe_action.perform()
          else:
              swipe_action = swipe_action.move_to(None, x - last_x, y - last_y).wait(20)
          last_x = x
          last_y = y  
  查找控件: 对于在调试的时候一切顺利的脚本,真正到了云测平台海量真机上测试的时候,却经常出现控件找不到导致脚本执行失败的问题。实际真机运行的时候,可能会有很多和本地并不相同的环境情况,比如网络延迟导致控件较迟刷新出来,我们应当封装一个较为稳定的控件查找函数。
  appium本身有提供waitUntil的api,现在要找图中的 个人中心 控件,使用显示等待的方法如下:
  from appium import webdriver
  from selenium.webdriver.common.by import By
  from selenium.webdriver.support.ui import WebDriverWait # available since 2.4.0
  from selenium.webdriver.support import expected_conditions as EC 
  #通过xpath的方式搜索
  element1 = WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.XPATH, 
  "//android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.TabHost[1]/android.widget.LinearLayout[1]/android.widget.TabWidget[1]/android.view.View[4]")))
  #通过resource-id的方式搜索,这里底部导航的resource-id是相同,需要通过下标来区分,搜出多个elements后,需要指定需要的下标
  element2 = WebDriverWait(driver, 3).until(EC.presence_of_all_elements_located((By.ID, "com.yunos.mqc:id/id_indicator_discovery")))
  element2[3].click()
  我们也可以封装一个多方式定位控件的函数,这里需要自己把握超时时间
  import time
  from time import sleep
  def wait_for_element(xpath=None, id=None, text=None, index=None, timeout=3):
      startTime = time.time()
      nowTime = time.time()
      while nowTime - startTime < timeout:
          # 通过 xpath 查找控件
          try:
              if xpath is not None:
                  el = driver.find_element_by_xpath(xpath)
                  return el
          except:
              pass
          # 通过 id 查找控件
          try:
              if id is not None:
                  if index is not None:
                      return driver.find_elements_by_id(self.id(id))[index]
                  else:
                      return driver.find_element_by_id(self.id(id))
          except:
              pass
          # 通过 text 查找控件
          try:
              if text is not None:
                  return driver.find_element_by_name(text)
          except:
              pass
          sleep(1)
          nowTime = time.time()
      raise Exception("Element id[%s] text[%s]" % (id, text))  
  组织用例
  unittest是python的一个 单元测试框架,它可以帮助我们有效组织用例,把用例的不同部分区分开来。结合已经封装好的函数,我们写一个登录的测试脚本:
  # -*- coding: UTF-8 -*-
  import unittest
  import time
  import sys
  from appium import webdriver
  from time import sleep
  from unittest import TestCase
  from appium.webdriver.common.touch_action import TouchAction
  from selenium.webdriver.common.touch_actions import TouchActions
  class MqcAppium(TestCase):
      #设备宽高
      global width
      global height
      def get_desired_capabilities(self):
          desired_caps = {
              'platformName': 'Android',
              'platformVersion': '18',
              'deviceName': 'mqcDevice',
              'udid': 'a05aacaf7d53',
              'app': "D:\\appium\\test.apk",
              'newCommandTimeout': 600,
              'automationName': 'appium',
              'unicodeKeyboard': True,
              'resetKeyboard': True,
              'appWaitActivity': 'com.yunos.mqc.view.activity.MainActivity',
          }
          return desired_caps
      #unittest 启动
      def setUp(self):
          desired_caps = self.get_desired_capabilities()
          uri = "http://localhost:4723/wd/hub"
          retry = 0
          while retry < 2:
              try:
                  self.driver = webdriver.Remote(uri, desired_caps)
                  break
              except Exception, e:
                  retry += 1
                  if retry == 2:
                      raise e
          sleep(10)
          # 获取当前设备分辨率
          self.window_size  = self.driver.get_window_size()
          self.width = self.window_size["width"]
          self.height = self.window_size["height"]
      # unittest 用例,用 test_**** 命名
      def test_login(self):
          #大部分app启动后会有动画,启动延迟等,视情况预留启动延迟
          sleep(5)
          #通过 resource-id 与 index 查找 个人中心 控件
          navPerson = self.wait_for_element(id="com.yunos.mqc:id/id_indicator_discovery", index=3);
          navPerson.click()
          #通过 text 查找 尚未登录
          noLogin = self.wait_for_element(xpath=("//*[@text='%s']" % ("尚未登录")));
          noLogin.click()
          #通过 xpath、resource-id 多种方式定位登录控件,避免有些手机上 xpath 失效或者不一致的情况
          inputUsername = self.wait_for_element(xpath="//android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]\
          /android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.LinearLayout[1]/android.widget.EditText[1]", id="com.yunos.mqc:id/custom_account")
          inputUsername.click()
          inputUsername.send_keys("mqc_test")
          inputPassword = self.wait_for_element(xpath="//android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/\
          android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.LinearLayout[1]/android.widget.EditText[2]", id="com.yunos.mqc:id/custom_passwd")
          inputPassword.click()
          inputPassword.send_keys("123456")
          login = self.wait_for_element(id="com.yunos.mqc:id/custom_loginBtn")
          login.click()
          #返回 广场 并且向下滑动
          navGround = self.wait_for_element(id="com.yunos.mqc:id/id_indicator_discovery", index=0);
          navGround.click()
          #与前文 swipe 函数不同的是,为了兼容不同分辨率的手机,滑动操作应当使用 比例 而非 绝对坐标,当然,若是需要精确地滑动操作,建议使用scrollTo
          self.swipe([[0.5, 0.7], [0.5, 0.6], [0.5, 0.5], [0.5, 0.4], [0.5, 0.3]])
      def tearDown(self):
          try:
              self.driver.quit()
          except:
              pass
      def swipe(self, points):
          last_x = 0
          last_y = 0
          swipe_action = TouchAction(self.driver)
          for i in range(0, len(points)):
              x=float(points[i][0]) * self.width
              y=float(points[i][1]) * self.height
              if i == 0:
                  swipe_action = swipe_action.press(None, x, y).wait(20)
              elif i == (len(points) - 1):
                  swipe_action = swipe_action.move_to(None, x - last_x, y - last_y).release()
                  swipe_action.perform()
              else:
                  swipe_action = swipe_action.move_to(None, x - last_x, y - last_y).wait(20)
              last_x = x
              last_y = y
      def wait_for_element(self, xpath=None, id=None, index=None, timeout=3):
          startTime = time.time()
          nowTime = time.time()
          while nowTime - startTime < timeout:
              # 通过 xpath 查找控件
              try:
                  if xpath is not None:
                      el = self.driver.find_element_by_xpath(xpath)
                      return el
              except:
                  pass
              # 通过 id 查找控件
              try:
                  if id is not None:
                      if index is not None:
                          return self.driver.find_elements_by_id(id)[index]
                      else:
                          return self.driver.find_element_by_id(id)
              except:
                  pass
              sleep(1)
              nowTime = time.time()
          raise Exception("Element xpath[%s] id[%s] index[%s] not found" % (xpath, id, index))
  if __name__ == '__main__':
      try: unittest.main()
      except SystemExit: pass

【推荐】Appium直播课早鸟价限时优惠,APP自动化的首选利器>>

推荐阅读

热门内容

功能测试大揭秘(2)—— Appium基

  上文回顾  上一篇为大家介绍了如何通...

iOS单元测试之XCTest详解

  前言:测试是一个好的App不可缺少的...

Android开源数据库 GreenDa

  GreenDao是一款对象关系映射(...

深入理解计算机系统(1.1)——Hell

  1、计算机系统  我们知道计算机系统...

基于.NET CORE微服务框架 -谈谈

  1、前言  对于最近surging更...

Google:防钓鱼工具让全球30亿台设

  Google宣布防钓鱼工具已经累计为...

难以避免的泄漏事故:怎么解?

  世界各地的企业都在担忧网络安全威胁问...

苹果谈Siri隐私:会收集数据,但不保存

  据MacRumors报道,在iOS1...

TestLink 的使用详解

  一、初始配置(设置用户、产品)  1...

selenium知识点小结

  环境:  Python:3.6.1 ...

最新内容

功能测试大揭秘(2)—— Appium基础篇

  上文回顾  上一篇为大家介绍了如何通过appium桌面客户端的方式来快速搭建...

iOS单元测试之XCTest详解

  前言:测试是一个好的App不可缺少的部分。每一个App都是由一个个小的功能组...

Android开源数据库 GreenDao实践

  GreenDao是一款对象关系映射(ORM)的开源数据库框架,通过将Java...

深入理解计算机系统(1.1)——Hello World 是如何运行的

  1、计算机系统  我们知道计算机系统是由硬件和软件组成的。它们共同工作来运行...

基于.NET CORE微服务框架 -谈谈surging API网关

  1、前言  对于最近surging更新的API网关大家也有所关注,也收到了不...

Google:防钓鱼工具让全球30亿台设备更安全

  Google宣布防钓鱼工具已经累计为超过30亿台设备提供安全防护。搜索巨头本...

难以避免的泄漏事故:怎么解?

  世界各地的企业都在担忧网络安全威胁问题,特别是每天看到大量窃取信息和知识产权...

导入jar包,jar包名类名与SDK相同,怎么调用jar中的类或者方法,求大神解疑!

本人是新手,公司给了一个项目,并给了一个classes.jar包,我也在项目中导...

苹果谈Siri隐私:会收集数据,但不保存个人信息

  据MacRumors报道,在iOS11发布之前,苹果营销副总裁格雷格·乔斯维...

请问怎么更改AndroidStudio默认新建工程的时候的gradle版本?

每次新建项目都得自己改回来不然安装上去程序会出错buildscript {   ...

程序员想要不被淘汰 终身学习才是关键

  在IT行业,技术迅速转变、业务要求不断变化,这使得重塑职业道路成为一个关乎生...

出不来结猴子吃桃问题。猴子第一天摘下若干个桃子,当即吃了一半,好不过瘾,又多吃了一个. 第二天早上又吃了剩下的桃子的一半,又多吃了一个。以后每天都吃了前一天剩

#include#includeint f...

关于对文件名使用 string.Replace() 没有作用 请教

这里我用replace()想要让文件名变成E:\CloudMusic\薛之谦 -...

以下代码实现的单例模式有什么缺陷?如果有,请做出你的修改!

public sealed class Singleton       stat...

wpf的keydown事件

页面只有这句话         

ASP.NET MVC验证表单

代码如下        [Required(ErrorMessage = "标题...

以XX开头的标识符太长,最大长度为128

本帖最后由qz6412525于2017-09-1212:09:14编辑如题,是一...

分部声明包含冲突的可访问性修饰符

代码如下:namespace ConstDefinitionLib{    pu...

关于 查询语句 中select 索引问题

string[] fruits = { "apple", "banana", "...

EasyUi Datagrid+DetailView 如何让他默认全部展开

各位牛人,有没有做过类似的功能效果,EasyUI DataGrid+Detail...