前言:
本篇文章主要介绍基于pytest和Appium框架,支持Android和iOS功能自动化的测试框架。同时该框架支持多设备测试,并利用allure库,生成可视化测试报告。本框架主要涉及的内容包括:python3、pytest、appium、allure等,此处已假设你具备相应的基础知识,同时已有可以随时运行的测试环境(iOS设备的测试只能在Mac系统中执行,没有Mac的朋友们,可以看看不执行)
一、流程图
本部分内容先从自动化测试的整体流程开始介绍,目的是希望大家在开始动手去实现框架之前,对测试过程做到清晰明了,这样在实现过程中,才能帮助我们无论何时,都不会迷茫和不知所措。才能让我们知道从何开始,如何优化以及拓展。
那么我们先来看下面这张流程图:
以上是本文所介绍框架的核心流程图,上图已经展现了框架的核心流程,所以在接下来的讲述中,大家可以参考该图进行理解和优化。
现在我也找了很多测试的朋友,做了一个分享技术的交流群,共享了很多我们收集的技术文档和视频教程。
如果你不想再体验自学时找不到资源,没人解答问题,坚持几天便放弃的感受
可以加入我们一起交流。而且还有很多在自动化,性能,安全,测试开发等等方面有一定建树的技术大牛
分享他们的经验,还会分享很多直播讲座和技术沙龙
可以免费学习!划重点!开源的!!!
qq群号:485187702【暗号:csdn11】
二、appium服务
在开始我们的测试之前,还有很多的工作需要我们去处理,这其中最重要,也是我们开始的第一步,就是开启appium的本地服务。关于appium的实现原理,本文不作过多的讲解,小编会抽空进行补充,届时也希望大家能及时关注。心急的小伙伴也可以自行百度哦~这里仅介绍启动服务的方法。
根据appium官方的介绍,我们可以通过下面的方式来启动appium服务:
1 |
/ usr / local / bin / appium - a ip - p port |
也就是我们在启动appium时,指定ip和端口,一般来说,本地ip使用127.0.0.1即可,官方默认端口为4723,我们也可以修改成自己想要的端口,只要保证使用的端口没有被其他服务占用即可。(小技巧:如果你不知道自己appium安装路径,可通过which appium来帮你找到)
启动服务之后,一般我们可以通过访问这个连接来验证服务是否正常:http://127.0.0.1:4723/wd/hub/status
。可正常访问并返回json格式数据时,则说明服务已正常启动。
但事实上,并不是每次启动都可以顺利进行,总会有一些意外的情况发生。比如说端口被占用。遇到这种情况我们也不必惊慌,做好应对即可。那么今天我们就上述的过程结合python,把它实现出来。
上面的过程,用python来实现,其实很简单,我们这里选择使用python中的subprocess库来执行命令,从而达到我们预期。
代码片段如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
import subprocess import abc import socket class Driver: __metaclass__ = abc.ABCMeta self ._host = '127.0.0.1' @abc .abstractmethod def connect_appium( self , port, n) """ 待实现的连接设备方法 """ return def start_appium( self , port): server = self .get_local_server_path() host = readConfig.ReadConfig().get_commend( "host" ) log_path = root_path + '/result/log' cmd = "%s -a %s -p %s" % (server, host, str (port)) if self .check_port( int (port)): subprocess.Popen(cmd, shell = True , stdout = open ( '%s/AppiumServer%s.log' % (log_path, port), 'w' )) log.logger.info( '%s/AppiumServer%s.log' % (log_path, port)) else : log.logger.info( "关闭被占用的端口号:%s" % str (port)) self .kill_appium() log.logger.info( "端口释放完毕!启动Appium-server,端口号:%s" % str (port)) subprocess.Popen(cmd, shell = True , stdout = open ( '%s/AppiumServer%s.log' % (log_path, port), 'w' )) log.logger.info( "Appium日志信息存储地址: %s/AppiumServer%s.log" % (log_path, port)) def check_port( self , port): """ 检查端口占用情况 :param port: :return: """ try : host = local_read_config.get_commend( "host" ) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) log.logger.info(s.connect((host, port))) s.shutdown( 2 ) except OSError: log.logger.info( "端口:%s 可用" % str (port)) return True else : log.logger.info( "端口:%s 已被占用" % str (port)) return False |
以上代码,会在启动appium服务之前,通过socket检查本地端口是否被占用,若被占用,则先释放端口,然后再启动服务,否则直接启动服务。
至此,服务启动完成,接下来就可以开始连接测试设备。
三、连接测试设备
当我们启动好appium服务后,就可以开始链接测试设备了。因为我们要同时支持Android和iOS的设备,所以我们先来定义一个Driver类,用来封装一些共有属性及方法,然后让Android和iOS分别继承它。
appium对于设备的连接,官方给我们提供了详细的方法事例:
1 2 3 4 5 6 7 8 9 10 11 12 |
# Android environment from appium import webdriver desired_caps = dict ( platformName = 'Android' , platformVersion = '10' , automationName = 'uiautomator2' , deviceName = 'Android Emulator' , app = PATH( '../../../apps/selendroid-test-app.apk' ) ) self .driver = webdriver.Remote( 'http://localhost:4723/wd/hub' , desired_caps) el = self .driver.find_element_by_accessibility_id( 'item' ) el.click() |
1 2 3 4 5 6 7 8 9 10 11 12 |
# iOS environment from appium import webdriver desired_caps = dict ( platformName = 'iOS' , platformVersion = '13.4' , automationName = 'xcuitest' , deviceName = 'iPhone Simulator' , app = PATH( '../../apps/UICatalog.app.zip' ) ) self .driver = webdriver.Remote( 'http://localhost:4723/wd/hub' , desired_caps) el = self .driver.find_element_by_accessibility_id( 'item' ) el.click() |
在以上两个示例中,我们发现,链接设备使用的都是同一个方法,但不同的设备需要传入不同的参数,
下面便是链接的关键:
1 |
driver = webdriver.Remote( 'http://localhost:4723/wd/hub' , desired_caps) |
既然我们找到了共性,那么就可以对该部分内容进行一番改造,让它来自动完成一些它可以完成的事情。那么首先,我们来看一下,再链接设备的过程中,我们到底做了些什么。
从上面的代码不难看出,每台设备连接都可以看成两步:第一步配置连接参数、第二步请求连接。
那么我们就可以封装一些类和方法,来完成我们想要分端操作的想法了。其实并不困难,我们可以分别写两个类AndroidDriver
和IOSDriver,都继承自Driver,然后实现设备连接的方法。
具体实现可参考下面的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
from Driver import Driver class AndroidDriver(Driver): def __init__( self ): self .driver = None def get_desired_caps( self ): """ 实现继承的抽象类方法;获取链接设备的配置信息 返回设备配置信息 :return:desired_caps """ desired_list = [] package = local_read_config.get_value( "ANDROID" , "package" ) activity = local_read_config.get_value( "ANDROID" , "activity" ) devices_info = self .update_devices_info() for i in range ( len (devices_info)): udid = devices_info[i].get( "udid" ) device_name = devices_info[i].get( "devices_name" ) platform_version = devices_info[i].get( "version" ) system_port1 = 8200 + 2 * i desired_caps = { "platformName" : "Android" , "platformVersion" : platform_version, "appPackage" : package, "appActivity" : activity, "deviceName" : device_name, "automationName" : "uiautomator2" , "udid" : udid, "systemPort" : system_port1, "newCommandTimeout" : 3000 , # "adbExecTimeout": 50000 } desired_list.append(desired_caps) return desired_list def connect_appium( self , port, n): """ 根据传入的port,启动appium服务 :param port: :param n: :return: """ set_adb_path() desired_caps = self .get_desired_caps() try : self .driver = webdriver.Remote( "%s:%s/wd/hub" % ( super ()._remote_url, str (port)), desired_caps[n]) return self .driver except WebDriverException: raise WebDriverException except ConnectionError: raise ConnectionError |
上面的方法主要做了两件事情,首先收集连接设备需要的desired_caps
信息,然后是连接设备。需要注意的是,因为我们这个框架是支持多个测试设备同时连接的,所有这里我们把收集到的每台测试设备的desired_caps
信息放到了一个数组中,并且在连接设备的时候,我们通过appium服务的端口号和数组下标两个值,来确定,每台测试设备连接的appium服务。
小提示:一个appium服务无法同时连接多个手机,但是我们希望能同时连接多个测试手机,并且同时在这连接的多个手机上进行测试,所以我们这里启动了多个appium服务,并指定了每个启动的服务端口号。因此我们只需要将端口号和设备信息对应上即可。
至此,启动服务和测试设备连接的实现就结束了,接下来就是对元素的操作了。那么我们一起来看一下,关于Element
的那些事情。
四、元素封装
众所周知,元素的操作依赖于元素查找。
举个常见的例子:我想百度搜索一个关键词,那么我首先要找到搜索框,才能输入关键词,然后找到搜索按钮,并点击搜索。这就是我们要做的。
常见的定位元素的方法有:ID、XPATH、CLASSNAME、NAME、PREDICATE等,selenium提供了对应的方法,我们这里也不做过多的封装,大家可以直接使用,也可以像我这样,把一些常见的定位方式封装成一个统一的方法,实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
def get_element( self , element_id): """ 获取指定页面的元素路径数据 :param element_id: 元素ID :return: 获取的元素对象 """ element_type = self .page.get(element_id).get( "pathType" ) element_value = self .page.get(element_id).get( "pathValue" ) element = None if element_type = = "ID" : element = self .driver.find_element_by_id(element_value) elif element_type = = "CLASSNAME" : element = self .driver.find_element_by_class_name(element_value) elif element_type = = "XPATH" : element = self .driver.find_element_by_xpath(element_value) elif element_type = = "NAME" : element = self .driver.find_element_by_name(element_value) elif element_type = = "ACB_ID" : element = self .driver.find_element_by_accessibility_id(element_value) elif element_type = = "PREDICATE" : element = self .driver.find_element_by_ios_predicate(element_value) return element |
大家自己选择是否进行封装,正常调用selenium的方法也是OK哒。
同样的道理,我们还可以封装一些常用的操作,比如滑动屏幕,键盘操作等。
分端元素操作
因为我们分别接入了Android和iOS,那么它们的操作,各有不同之处,我们可以将各自的特色操作分别集中到一个单独的AndroidElement类和iOSElement类中,这样在后面使用的时候,我们直接继承这两个类就可以,并且从结构上看,也比较清晰。
比如同样是滑动屏幕,swipe在Android和iOS系统上的表现就不一致,因此我们就选择了其他方法:
AndroidElement:
1 2 3 4 5 6 7 8 |
def swipe_to_up( self ): """ 向上划,页面滚动到最下方 :return: """ width = self .driver.get_window_size()[ "width" ] height = self .driver.get_window_size()[ "height" ] self .driver.swipe(width / 2 , height * 3 / 5 , width / 2 , height / 5 , duration = 500 ) |
iOSElement:
1 2 3 4 5 6 |
def swipe_to_up( self ): """ 向上滑动 :return: """ self .driver.execute_script( 'mobile: swipe' , { 'direction' : 'up' }) |
以上只是一个小例子,只是想说明,如果有这样的操作差异,我们可以将它们分开处理,这样会显得逻辑更清晰。
有了上面的实现,我们就只需要写测试的脚步就可以。写脚本部分的内容就先略过,不做详细描述,毕竟不同的业务需求场景,都有其独特的脚本逻辑。凡事万变不离其宗,元素还是那个元素,操作还是那些操作,就让大家自己去尽情发挥吧。
那么,一切准备就绪,就差让我们的程序跑起来了。接下来就让我们来看看,如何让我们的测试同时在多个连接的测试设备上进行测试。
五、运行
因为我们的测试是通过pytest来执行的,所以pytest的所有执行参数都是可以正常使用的。而我们,也只是利用pytest的main函数来完成本次执行。唯一不同的是,为了满足不同设备同时进行测试,我们为每一台设备的测试,都创建了一个进程。每一个进程都包含了上述完整的流程。选择进程而非线程的原因也很简单,相信大家也都知道,进程和线程的关系吧,在同一个进程中的线程资源是共享的。而在我们看来,每一台设备的测试都应该是独立的、互不干扰的,所以我们选择进程而非线程。
具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
from multiprocessing import Process import pytest import time import os, re import subprocess from appiums.common import read_files from appiums.driver.iOSDriver import IOSDriver from driver.androidDriver import AndroidDriver from driver import Driver from elements import Element class Run(Process): def __init__( self , name, args): super (Run, self ).__init__() self .name = name self .args = args self .root_path = os.getcwd() self .device_name = re.sub( '[\']' , '', str (args[ 2 ].get( "deviceName" )).replace( " " , "_" )) def run_test( self ): """ 执行测试用例 :return: """ pytest.main([ '--alluredir' , '%s/result/data/%s' % ( self .root_path, self .device_name)]) time.sleep( 2 ) def generate_report( self ): """ 整合测试报告到项目根目录下的result/report目录下 :return: none """ cmd = "allure generate %s/result/data/%s -o %s/result/report/%s --clean" \ % ( self .root_path, self .device_name, self .root_path, self .device_name) stdout = subprocess.Popen(cmd, shell = True , stdout = subprocess.PIPE, text = True ) log.logger.info( "测试报告查看路径:%s" % str (stdout.stdout.readlines()[ 0 ]).split( " " )[ - 1 ][: - 1 ]) def get_environment_info( self ): """ 获取测试环境的信息 :return: """ env = { "测试平台" : self .args[ 2 ].get( "platformName" ), "设备名称" : self .device_name, "设备系统版本" : self .args[ 2 ].get( "platformVersion" ), "设备udid" : self .args[ 2 ].get( "udid" ), "应用名称" : self .args[ 2 ].get( "bundleId" ) if str ( self .args[ 2 ].get( "platformName" )).lower() = = 'ios' else self .args[ 2 ].get( "appPackage" ), } return env def run( self ): """ 执行线程中的任务 :return: """ Driver.Driver().start_appium( self .args[ 0 ]) time.sleep( 5 ) self .set_driver() time.sleep( 1 ) self .run_test() time.sleep( 1 ) read_files.set_environment( self .device_name, self .get_environment_info()) time.sleep( 1 ) self .generate_report() def main(desired_caps): """ 开启测试进程执行测试 """ list_p = [] process_num = len (desired_caps) if process_num > 0 : for a in range (process_num): port1 = 4723 + 2 * a p = Run( '测试进程-%s' % str (port1), args = (port1, a, desired_caps[a])) p.start() log.logger.info( "设备%s在进程 %s 上进行测试, 进程ID:%s" % (desired_caps[a].get( "deviceName" ), p.name, p.pid)) list_p.append(p) for b in list_p: b.join() Driver.Driver().kill_appium() else : log.logger.error( "没有设备可进行测试,请重新连接设备后尝试!" ) exit( - 1 ) def android_run(): caps = AndroidDriver().get_desired_caps() main(caps) def ios_run(): caps = IOSDriver().get_desired_caps() main(caps) |
最后感谢每一个认真阅读我文章的人,看着粉丝一路的上涨和关注,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走! 希望能帮助到你!【100%无套路免费领取】