Python+Appium+YAML移动端自动化测试框架实现详解
本文详细介绍基于Python、Appium和YAML构建的移动端自动化测试框架。通过将测试数据与执行代码分离,利用YAML管理用例,实现了模块化的测试结构。文章解析了公共模块、页面操作层及用例运行机制,并探讨了弹窗处理、断线重连等优化方向,旨在降低维护成本并提升测试效率。
框架结构概览
框架结构概览
此前曾分享过一篇关于安卓UI测试的文章,但当时未实现数据与代码的分离,导致后期维护成本较高。因此,近期对框架进行了优化升级。若无需阅读详细文章,可直接访问Github仓库查看源码,欢迎提出建议。整体架构大致如下:

testyaml目录用于管理测试用例,核心目标是实现数据与代码的分离,每个功能模块对应独立的文件夹。
public目录存放公共文件,包括读取配置文件、启动Appium服务、解析YAML文件以及定义日志格式等通用功能。
page目录存放最小粒度的测试用例集,同样采用一个模块对应一个文件夹的组织方式。
results目录用于存储生成的测试报告以及测试失败时的截图。

logs目录专门用于存放运行过程中产生的日志文件。


testcase目录存放具体的测试用例,runtest.py脚本用于运行所有测试用例。
YAML格式说明
首先了解YAML文件的格式,此前已有关于YAML语法的介绍文章。testcase部分是配置的重点,具体包含以下字段:
element_info:用于定义需要定位的元素信息。
find_type:指定定位属性,支持id、xpath、text、ids等类型。
operate_type:定义操作类型,目前支持click(点击)、sendkeys(输入)、back(返回)和swipe_up(上滑)四种操作。
上述三个字段为必填项,其中operate_type尤为关键,必须指定。
send_content:在使用sendkeys操作时,需填写具体的输入内容。
index:在使用ids定位时,需指定元素的索引值。
times:用于指定返回或上滑操作的执行次数。
testinfo:
- id: cm001
title: 新增终端门店
execute: 1
testcase:
-
element_info: 客户
find_type: text
operate_type: click
-
element_info: com.fiberhome.waiqin365.client:id/cm_topbar_tv_right
find_type: id
operate_type: click
-
element_info: com.fiberhome.waiqin365.client:id/custview_id_singletv_inputtext
find_type: ids
operate_type: send_keys
send_content: auto0205
index: 0
-
element_info:
find_type:
operate_type: swipe_up
times: 1
-
element_info: 提交
find_type: text
operate_type: click
-
element_info:
find_type:
operate_type: back
times: 1
代码实现部分
代码实现部分
公共模块详解
个人认为框架的核心在于公共模块,这相当于搭建房屋的主体结构。公共模块搭建完毕后,后续工作主要是调用相关接口,即“添砖加瓦”。
读取配置文件readconfig.py、设置日志格式logs.py以及获取设备信息GetDevices.py属于通用基础模块,此处不再赘述。
GetYaml.py主要用于读取YAML格式的测试用例文件。
#coding=utf-8
#author='Shichao-Dong'
import sys
reload(sys)
sys.setdefaultencoding('utf8')
import yaml
import codecs
class getyaml:
def __init__(self,path):
self.path = path
def getYaml(self):
'''
读取yaml文件
:param path: 文件路径
:return:
'''
try:
f = open(self.path)
data =yaml.load(f)
f.close()
return data
except Exception:
print(u"未找到yaml文件")
def alldata(self):
data =self.getYaml()
return data
def caselen(self):
data = self.alldata()
length = len(data['testcase'])
return length
def get_elementinfo(self,i):
data = self.alldata()
# print data['testcase'][i]['element_info']
return data['testcase'][i]['element_info']
def get_findtype(self,i):
data = self.alldata()
# print data['testcase'][i]['find_type']
return data['testcase'][i]['find_type']
def get_operate_type(self,i):
data = self.alldata()
# print data['testcase'][i]['operate_type']
return data['testcase'][i]['operate_type']
def get_index(self,i):
data = self.alldata()
if self.get_findtype(i)=='ids':
return data['testcase'][i]['index']
else:
pass
def get_send_content(self,i):
data = self.alldata()
# print data['testcase'][i]['send_content']
if self.get_operate_type(i) == 'send_keys':
return data['testcase'][i]['send_content']
else:
pass
def get_backtimes(self,i):
data = self.alldata()
if self.get_operate_type(i)=='back' or self.get_operate_type(i)=='swipe_up':
return data['testcase'][i]['times']
else:
pass
def get_title(self):
data = self.alldata()
# print data['testinfo'][0]['title']
return data['testinfo'][0]['title']
StartAppiumServer.py负责启动Appium服务,并返回所需的端口号port,该端口号将在后续的driver初始化中使用。
#coding=utf-8
#author='Shichao-Dong'
from logs import log
import random,time
import platform
import os
from GetDevices import devices
log = log()
dev = devices().get_deviceName()
class Sp:
def __init__(self, device):
self.device = device
def __start_driver(self, aport, bpport):
"""
:return:
"""
if platform.system() == 'Windows':
import subprocess
subprocess.Popen("appium -p %s -bp %s -U %s" %
(aport, bpport, self.device), shell=True)
def start_appium(self):
"""
启动appium
p:appium port
bp:bootstrap port
:return: 返回appium端口参数
"""
aport = random.randint(4700, 4900)
bpport = random.randint(4700, 4900)
self.__start_driver(aport, bpport)
log.info(
'start appium :p %s bp %s device:%s' %
(aport, bpport, self.device))
time.sleep(10)
return aport
def main(self):
"""
:return: 启动appium
"""
return self.start_appium()
def stop_appium(self):
'''
停止appium
:return:
'''
if platform.system() == 'Windows':
os.popen("taskkill /f /im node.exe")
if __name__ == '__main__':
s = Sp(dev)
s.main()
GetDriver.py用于获取WebDriver实例。platformName、deviceName、appPackage、appActivity等参数配置在config.ini文件中,可通过readconfig.py直接读取。appium_port则由StartAppiumServer.py返回。
s = Sp(deviceName)
appium_port = s.main()
def mydriver():
desired_caps = {
'platformName':platformName,'deviceName':deviceName, 'platformVersion':platformVersion,
'appPackage':appPackage,'appActivity':appActivity,
'unicodeKeyboard':True,'resetKeyboard':True,'noReset':True
}
try:
driver = webdriver.Remote('http://127.0.0.1:%s/wd/hub'%appium_port,desired_caps)
time.sleep(4)
log.info('获取driver成功')
return driver
except WebDriverException:
print 'No driver'
if __name__ == "__main__":
mydriver()
BaseOperate.py对find等基础命令进行了重新封装,主要包含上滑、返回、查找元素等基础操作方法。
#coding=utf-8
#author='Shichao-Dong'
from selenium.webdriver.support.ui import WebDriverWait
from logs import log
import os
import time
'''
一些基础操作:滑动、截图、点击页面元素等
'''
class BaseOperate:
def __init__(self,driver):
self.driver = driver
def back(self):
'''
返回键
:return:
'''
os.popen("adb shell input keyevent 4")
def get_window_size(self):
'''
获取屏幕大小
:return: windowsize
'''
global windowSize
windowSize = self.driver.get_window_size()
return windowSize
def swipe_up(self):
'''
向上滑动
:return:
'''
windowsSize = self.get_window_size()
width = windowsSize.get("width")
height = windowsSize.get("height")
self.driver.swipe(width/2, height*3/4, width/2, height/4, 1000)
def screenshot(self):
now=time.strftime("%y%m%d-%H-%M-%S")
PATH = lambda p: os.path.abspath(
os.path.join(os.path.dirname(__file__), p)
)
screenshoot_path = PATH('../results/screenshoot/')
self.driver.get_screenshot_as_file(screenshoot_path+now+'.png')
def find_id(self,id):
'''
寻找元素
:return:
'''
exsit = self.driver.find_element_by_id(id)
if exsit :
return True
else:
return False
def find_name(self,name):
'''
判断页面是否存在某个元素
:param name: text
:return:
'''
findname = "//*[@text='%s']"%(name)
exsit = self.driver.find_element_by_xpath(findname)
if exsit :
return True
else:
return False
def get_name(self,name):
'''
定位页面text元素
:param name:
:return:
'''
# element = driver.find_element_by_name(name)
# return element
findname = "//*[@text='%s']"%(name)
try:
element = WebDriverWait(self.driver, 10).until(lambda x: x.find_element_by_xpath(findname))
# element = self.driver.find_element_by_xpath(findname)
self.driver.implicitly_wait(2)
return element
except:
self.screenshot()
log.error('未定位到元素:'+'%s')%(name)
def get_id(self,id):
'''
定位页面resouce-id元素
:param id:
:return:
'''
try:
element = WebDriverWait(self.driver, 10).until(lambda x: x.find_element_by_id(id))
# element = self.driver.find_element_by_id(id)
self.driver.implicitly_wait(2)
return element
except:
self.screenshot()
log.error('未定位到元素:'+'%s')%(id)
def get_xpath(self,xpath):
'''
定位页面xpath元素
:param id:
:return:
'''
try:
element = WebDriverWait(self.driver, 10).until(lambda x: x.find_element_by_xpath(xpath))
# element = self.driver.find_element_by_xpath(xpath)
self.driver.implicitly_wait(2)
return element
except:
self.screenshot()
log.error('未定位到元素:'+'%s')%(xpath)
def get_ids(self,id):
'''
定位页面resouce-id元素组
:param id:
:return:列表
'''
try:
# elements = self.driver.find_elements_by_id(id)
elements = WebDriverWait(self.driver, 10).until(lambda x: x.find_elements_by_id(id))
self.driver.implicitly_wait(2)
return elements
except:
self.screenshot()
log.error('未定位到元素:'+'%s')%(id)
def page(self,name):
'''
返回至指定页面
:return:
'''
i=0
while i<10:
i=i+1
try:
findname = "//*[@text='%s']"%(name)
self.driver.find_element_by_xpath(findname)
self.driver.implicitly_wait(2)
break
except :
os.popen("adb shell input keyevent 4")
try:
findname = "//*[@text='确定']"
self.driver.find_element_by_xpath(findname).click()
self.driver.implicitly_wait(2)
except:
os.popen("adb shell input keyevent 4")
try:
self.driver.find_element_by_xpath("//*[@text='工作台']")
self.driver.implicitly_wait(2)
break
except:
os.popen("adb shell input keyevent 4")
Operate.py被认为是框架中最关键的一步。后续的page层均调用此文件进行测试,其核心逻辑是读取YAML文件后,通过if...else...判断对应的operate_type,并执行相应的click、sendkeys等操作。
#coding=utf-8
#author='Shichao-Dong'
from GetYaml import getyaml
from BaseOperate import BaseOperate
class Operate:
def __init__(self,path,driver):
self.path = path
self.driver = driver
self.yaml = getyaml(self.path)
self.baseoperate=BaseOperate(driver)
def check_operate_type(self):
'''
读取yaml信息并执行
element_info:定位元素信息
find_type:属性,id、xpath、text、ids
operate_type: click、sendkeys、back、swipe_up 为back就是返回,暂时就三种
上面三个必填,operate_type必填!!!!!!
send_content:send_keys 时用到
index:ids时用到
times:
:return:
'''
for i in range(self.yaml.caselen()):
if self.yaml.get_operate_type(i) == 'click':
if self.yaml.get_findtype(i) == 'text':
self.baseoperate.get_name(self.yaml.get_elementinfo(i)).click()
elif self.yaml.get_findtype(i) == 'id':
self.baseoperate.get_id(self.yaml.get_elementinfo(i)).click()
elif self.yaml.get_findtype(i) == 'xpath':
self.baseoperate.get_xpath(self.yaml.get_elementinfo(i)).click()
elif self.yaml.get_findtype(i) == 'ids':
self.baseoperate.get_ids(self.yaml.get_elementinfo(i))[self.yaml.get_index(i)].click()
elif self.yaml.get_operate_type(i) == 'send_keys':
if self.yaml.get_findtype(i) == 'text':
self.baseoperate.get_name(self.yaml.get_elementinfo(i)).send_keys(self.yaml.get_send_content(i))
elif self.yaml.get_findtype(i) == 'id':
self.baseoperate.get_id(self.yaml.get_elementinfo(i)).send_keys(self.yaml.get_send_content(i))
elif self.yaml.get_findtype(i) == 'xpath':
self.baseoperate.get_xpath(self.yaml.get_elementinfo(i)).send_keys(self.yaml.get_send_content(i))
elif self.yaml.get_findtype(i) == 'ids':
self.baseoperate.get_ids(self.yaml.get_elementinfo(i))[self.yaml.get_index(i)].send_keys(self.yaml.get_send_content(i))
elif self.yaml.get_operate_type(i) == 'back':
for n in range(self.yaml.get_backtimes(i)):
self.baseoperate.back()
elif self.yaml.get_operate_type(i) == 'swipe_up':
for n in range(self.yaml.get_backtimes(i)):
self.baseoperate.swipe_up()
def back_home(self):
'''
返回至工作台
:return:
'''
self.baseoperate.page('工作台')
公共模块的代码介绍至此结束。在开发此框架时,大部分精力投入在公共模块的构建上,因此认为这部分值得深入研究。
Page层实现
Page层实现
page层包含最小用例集,每个模块对应一个文件夹。以客户管理模块为例,目前编写了两个用例:新增和排序,文件结构如下:

相关代码实现非常简洁,具体如下:
import sys
reload(sys)
sys.setdefaultencoding('utf8')
import codecs,os
from public.Operate import Operate
from public.GetYaml import getyaml
PATH = lambda p: os.path.abspath(
os.path.join(os.path.dirname(__file__), p)
)
yamlpath = PATH("../../testyaml/cm/cm-001addcm.yaml")
class AddcmPage:
def __init__(self,driver):
self.path = yamlpath
self.driver = driver
self.operate = Operate(self.path,self.driver)
def operateap(self):
self.operate.check_operate_type()
def home(self):
self.operate.back_home()
用例执行机制
用例执行机制
该部分利用unittest框架运行所有测试用例并生成报告。以客户模块为例,对应文件为CmTest.py。
from page.cm.CmAddcmPage import AddcmPage
from page.cm.CmSortcmPage import SortcmPage
from public.GetDriver import mydriver
driver = mydriver()
import unittest,time
class Cm(unittest.TestCase):
def test_001addcm(self):
'''
新增客户
:return:
'''
add = AddcmPage(driver)
add.operateap()
add.home()
def test_002sortcm(self):
'''
客户排序
:return:
'''
sort = SortcmPage(driver)
sort.sortlist()
sort.home()
def test_999close(self):
driver.quit()
time.sleep(10)
if __name__ == "__main__":
unittest.main()
首先从page层导入需要运行的用例,然后使用unittest执行。若需运行所有测试用例,则需调用runtest.py脚本。
import time,os
import unittest
import HTMLTestRunner
from testcase.CmTest import Cm
def testsuit():
suite = unittest.TestSuite()
suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(Cm),
])
# runner = unittest.TextTestRunner(verbosity=2)
# runner.run(suite)
now=time.strftime("%y-%m-%d-%H-%M-%S")
PATH = lambda p: os.path.abspath(
os.path.join(os.path.dirname(__file__), p)
)
dirpath = PATH("./results/waiqin365-")
filename=dirpath + now +'result.html'
fp=open(filename,'wb')
runner=HTMLTestRunner.HTMLTestRunner(stream=fp,title='waiqin365 6.0.6beta test result',description=u'result:')
runner.run(suite)
fp.close()
if __name__ =="__main__":
testsuit()
整体思路类似,先导入用例,再将其加入测试套件(suite)中执行。
总结与展望
总结与展望
目前框架已初步实现数据与用例的分离,但YAML的编写要求较高,格式错误可能导致运行失败。此外,还有以下方面可以进一步优化:
- 增加对弹窗的判断与处理机制。
- 实现连接断开后的自动重连机制。
- 增加测试失败后的自动重跑机制。
至此,关于Python+Appium+YAML移动端自动化测试框架的实现详解介绍完毕。更多相关Python、Appium、YAML自动化测试内容,请搜索本站以前的文章或继续浏览下面的相关文章。希望大家以后多多支持本站!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













