python和Appium的移动端多设备自动化测试框架


Posted in Python onApril 26, 2022

前言:

本篇文章主要介绍基于pytest和Appium框架,支持Android和iOS功能自动化的测试框架。同时该框架支持多设备测试,并利用allure库,生成可视化测试报告。本框架主要涉及的内容包括:python3、pytest、appium、allure等,此处已假设你具备相应的基础知识,同时已有可以随时运行的测试环境(iOS设备的测试只能在Mac系统中执行,没有Mac的朋友们,可以看看不执行)

一、流程图

本部分内容先从自动化测试的整体流程开始介绍,目的是希望大家在开始动手去实现框架之前,对测试过程做到清晰明了,这样在实现过程中,才能帮助我们无论何时,都不会迷茫和不知所措。才能让我们知道从何开始,如何优化以及拓展。

那么我们先来看下面这张流程图: 

python和Appium的移动端多设备自动化测试框架

以上是本文所介绍框架的核心流程图,上图已经展现了框架的核心流程,所以在接下来的讲述中,大家可以参考该图进行理解和优化。

二、appium服务

在开始我们的测试之前,还有很多的工作需要我们去处理,这其中最重要,也是我们开始的第一步,就是开启appium的本地服务。关于appium的实现原理,本文不作过多的讲解,小编会抽空进行补充,届时也希望大家能及时关注。心急的小伙伴也可以自行百度哦~这里仅介绍启动服务的方法。

根据appium官方的介绍,我们可以通过下面的方式来启动appium服务:

/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库来执行命令,从而达到我们预期。

代码片段如下:

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对于设备的连接,官方给我们提供了详细的方法事例:

# 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()
# 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()

在以上两个示例中,我们发现,链接设备使用的都是同一个方法,但不同的设备需要传入不同的参数,

下面便是链接的关键: 

driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)

既然我们找到了共性,那么就可以对该部分内容进行一番改造,让它来自动完成一些它可以完成的事情。那么首先,我们来看一下,再链接设备的过程中,我们到底做了些什么。

从上面的代码不难看出,每台设备连接都可以看成两步:第一步配置连接参数、第二步请求连接。

那么我们就可以封装一些类和方法,来完成我们想要分端操作的想法了。其实并不困难,我们可以分别写两个类AndroidDriver和IOSDriver,都继承自Driver,然后实现设备连接的方法。

具体实现可参考下面的内容:

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提供了对应的方法,我们这里也不做过多的封装,大家可以直接使用,也可以像我这样,把一些常见的定位方式封装成一个统一的方法,实现如下:

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:

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:

def swipe_to_up(self):
        """
        向上滑动
        :return:
        """
        self.driver.execute_script('mobile: swipe', {'direction': 'up'})

以上只是一个小例子,只是想说明,如果有这样的操作差异,我们可以将它们分开处理,这样会显得逻辑更清晰。

有了上面的实现,我们就只需要写测试的脚步就可以。写脚本部分的内容就先略过,不做详细描述,毕竟不同的业务需求场景,都有其独特的脚本逻辑。凡事万变不离其宗,元素还是那个元素,操作还是那些操作,就让大家自己去尽情发挥吧。

那么,一切准备就绪,就差让我们的程序跑起来了。接下来就让我们来看看,如何让我们的测试同时在多个连接的测试设备上进行测试。

五、运行

因为我们的测试是通过pytest来执行的,所以pytest的所有执行参数都是可以正常使用的。而我们,也只是利用pytest的main函数来完成本次执行。唯一不同的是,为了满足不同设备同时进行测试,我们为每一台设备的测试,都创建了一个进程。每一个进程都包含了上述完整的流程。选择进程而非线程的原因也很简单,相信大家也都知道,进程和线程的关系吧,在同一个进程中的线程资源是共享的。而在我们看来,每一台设备的测试都应该是独立的、互不干扰的,所以我们选择进程而非线程。

具体实现如下:

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)

到此这篇关于python和Appium移动端多设备自动化测试框架实现的文章就介绍到这了!

Python 相关文章推荐
零基础写python爬虫之神器正则表达式
Nov 06 Python
Python实现爬取逐浪小说的方法
Jul 07 Python
pygame加载中文名mp3文件出现error
Mar 31 Python
python logging日志模块的详解
Oct 29 Python
python中的tcp示例详解
Dec 09 Python
python的schedule定时任务模块二次封装方法
Feb 19 Python
python画双y轴图像的示例代码
Jul 07 Python
python 控制Asterisk AMI接口外呼电话的例子
Aug 08 Python
详解Python并发编程之创建多线程的几种方法
Aug 23 Python
pytorch torch.expand和torch.repeat的区别详解
Nov 05 Python
python 实现单通道转3通道
Dec 03 Python
python编写俄罗斯方块
Mar 13 Python
Python查找算法的实现 (线性、二分,分块、插值查找算法)
Python 装饰器(decorator)常用的创建方式及解析
Apr 24 #Python
解决IDEA翻译插件Translation报错更新TTK失败不能使用
python使用BeautifulSoup 解析HTML
Apr 24 #Python
Python中npy和mat文件的保存与读取
Apr 24 #Python
python小型的音频操作库mp3Play
Apr 24 #Python
5个pandas调用函数的方法让数据处理更加灵活自如
Apr 24 #Python
You might like
php object转数组示例
2014/01/15 PHP
Yii框架form表单用法实例
2014/12/04 PHP
PHP使用ODBC连接数据库的方法
2015/07/18 PHP
Javascript自定义函数判断网站访问类型是PC还是移动终端
2014/01/10 Javascript
jQuery学习笔记之总体架构
2014/06/03 Javascript
setTimeout()递归调用不加引号出错的解决方法
2014/09/05 Javascript
对比分析AngularJS中的$http.post与jQuery.post的区别
2015/02/27 Javascript
鼠标事件的screenY,pageY,clientY,layerY,offsetY属性详解
2015/03/12 Javascript
使用Javascript监控前端相关数据的代码
2016/10/27 Javascript
JS实用的带停顿的逐行文本循环滚动效果实例
2016/11/23 Javascript
快速理解 JavaScript 中的 LHS 和 RHS 查询的用法
2017/08/24 Javascript
JavaScript自执行函数和jQuery扩展方法详解
2017/10/27 jQuery
浅谈webpack devtool里的7种SourceMap模式
2019/01/14 Javascript
javascript中floor使用方法总结
2019/02/02 Javascript
微信小程序实现左侧滑栏过程解析
2019/08/26 Javascript
Vue的属性、方法、生命周期实例代码详解
2019/09/17 Javascript
微信小程序录音实现功能并上传(使用node解析接收)
2020/02/26 Javascript
React实现todolist功能
2020/12/28 Javascript
[05:35]DOTA2英雄梦之声_第13期_拉比克
2014/06/21 DOTA
[00:03]DOTA2新版本PA至宝展示
2014/11/19 DOTA
[07:09]DOTA2-DPC中国联赛 正赛 Ehome vs Elephant 选手采访
2021/03/11 DOTA
删除目录下相同文件的python代码(逐级优化)
2012/05/25 Python
用python实现批量重命名文件的代码
2012/05/25 Python
在Python中处理时间之clock()方法的使用
2015/05/22 Python
python生成圆形图片的方法
2020/03/25 Python
儿童学习python的一些小技巧
2018/05/27 Python
Python 脚本拉取 Docker 镜像问题
2019/11/10 Python
10个python3常用排序算法详细说明与实例(快速排序,冒泡排序,桶排序,基数排序,堆排序,希尔排序,归并排序,计数排序)
2020/03/17 Python
python用Configobj模块读取配置文件
2020/09/26 Python
python 爬虫之selenium可视化爬虫的实现
2020/12/04 Python
英国最大的在线运动补充剂商店:Discount Supplements
2017/06/03 全球购物
2015年司机工作总结
2015/04/23 职场文书
企业安全生产检查制度
2015/08/06 职场文书
聘任通知书
2015/09/21 职场文书
python中的装饰器该如何使用
2021/06/18 Python
在Python 中将类对象序列化为JSON
2022/04/06 Python