一、环境搭建
Appium+Python3
python第三方库:Appium-Python-Client、ddt、xlrd、xlutils
二、项目结构设计
三、代码实现
四、具体分析
1、Appium的server封装(多设备)
首先检测电脑已连接设备信息,保存设备信息,Server按设备数量命令行启动多个服务,以下为server类部分代码:
class Server(object):
def __init__(self):
self.project_dir = Tools.get_project_dir()
device_config_path = self.project_dir + '/config/device.ini'
self.cf = ConfigOperate(device_config_path)
def main(self, device_list=None):
"""
多进程启动appium server主程序
:param device_list: 设备列表
:return:
"""
if device_list is None: # 如果设备列表没有传进来,则自动去当前系统里去获取
device_list = self._get_android_devices() # 获取安卓设备列表
if device_list:
appium_port_list = self._create_port_list(4700, len(device_list))
bootstrap_port_list = self._create_port_list(4900, len(device_list))
self.cf.clean() # 先清空device配置文件
for i in range(len(device_list)):
port = appium_port_list[i]
bport = bootstrap_port_list[i]
device = device_list[i]
server_log_path = os.path.join(Tools.get_project_dir(), 'logs', 'servers', '%s_%s.log' % (device, Tools.get_now_date('%Y-%m-%d_%H:%M:%S')))
self.cf.write_config(device, {'port': port, 'bport': bport, 'server_log_path': server_log_path}) # 把设备信息和端口信息写入配置文件
t = multiprocessing.Process(target=self.start_server, args=(port, bport, device, server_log_path))
t.daemon = True # 设置为守护进程,且一定要在t.start()前设置,设置t为守护进程,禁止t创建子进程,并且父进程代码执行结束,t即终止运行
t.start()
else:
raise ValueError('当前没有任何连接设备')
def start_server(self, port, bport, device, log_path=None):
"""
创建子进程运行appium的server
:param port: 端口
:param bport: b端口
:param device: 设备名
:param log_path: log路径
"""
if log_path:
command = 'appium -p {port} --bootstrap-port {bport} -U {device} --log {log_path} ' \
'--local-timezone --session-override'.format(
port=port, bport=bport, device=device, log_path=log_path)
else:
command = 'appium -p {port} --bootstrap-port {bport} -U {device} ' \
'--local-timezone --session-override'.format(
port=port, bport=bport, device=device)
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
if len(result.stderr) > 0:
message = '--------启动port端口为%s,bport端口为%s,设备uuid为%s的appium服务失败!!!---------\n' \
'error_message:%s' % (port, bport, device, result.stderr)
else:
message = '--------启动port端口为%s,bport端口为%s,设备uuid为%s的appium服务成功!!!---------\n' \
% (port, bport, device)
Logging().info(message)
2、Appium操作封装
为了方便,把appium的基础操作封装成一个个的关键字,输入、点击、滑动等等:
class KeyWords:
# 初始化
def __init__(self, driver):
self.driver = driver
self._element_finder = ElementFinder()
self.log = logging.getLogger('main.keywords') # 继承日志设置
def click_element(self, locator):
"""
封装点击元素操作
:param locator: 自定义元素
:return:
"""
self._element_finder.find(self.driver, locator).click()
def input_text(self, locator, text):
"""
输入文本
:param locator: 元素
:param text: 文本
:return:
"""
self._element_finder.find(self.driver, locator).send_keys(text)
def swipe_left(self, duration=1000):
"""
封装滑动方法(按比例往左滑动)
:param duration: 时间
:return:
"""
try:
self._swipe_by_percent(80, 50, 20, 50, duration)
except Exception as e:
self.log.error('滑动屏幕出错'.format(e))
3、excel基础操作封装
excel的读取、保存等操作用到了xlrd和xlutils库,主要实现整个excel数据的读取、获取单元格内容、获取一行数据和数据写入等基础操作
class ExcelOperate(object):
def __init__(self, file_path, sheet_name):
self.file_path = file_path
self.sheet_name = sheet_name
self.excel = self._get_all_excel()
self.sheet_data = self._get_sheet_data(sheet_name)
def _get_all_excel(self):
"""
获取整个excel数据
:return:
"""
excel = xlrd.open_workbook(self.file_path)
return excel
def _get_sheet_data(self, sheet_name=0):
"""
获取excel中的某个表数据
:param sheet_name: 表名,可以是下标也可以是名称
:return:
"""
if isinstance(sheet_name, int):
sheet = self.excel.sheet_by_index(sheet_name)
else:
sheet = self.excel.sheet_by_name(sheet_name)
return sheet
def get_rows(self):
"""
获取excel行数
:return:
"""
return self.sheet_data.nrows
def get_cell_value(self, row, column):
"""
获取单元格内容
:param row: 行号
:param column: 列号
:return:
"""
data = self.sheet_data.cell(row, column).value
return data
def get_row_values(self, row):
"""
获取一行的数据
:param row:
"""
return self.sheet_data.row_values(row)
def write_value(self, row, column, value):
"""
数据写入
:param row: 行
:param column: 列
:param value: 数据
:return:
"""
read_value = self.excel
write_data = copy(read_value)
write_save = write_data.get_sheet(self.sheet_name)
write_save.write(row, column, value)
write_data.save(self.file_path)
4、Excel和Appium操作结合
excel和appium的操作封装好了,那两者结合就可以实现excel读取数据,appium按读取数据执行用例的目的,当然这里Excel需要结合自己需求来设计。
4.1 Excel内容设计
EXCEL里第一行里是表头,表头是固定的。执行用例是按页面+步骤+元素+操作值的组合判断来执行相应的操作,比如页面=首页,步骤=点击,元素=我的头像按钮的组合,那操作就是点击首页上的我的头像按钮。
4.2部分代码实现如下:
class ExcelHandle(object):
def __init__(self, driver, data):
self.driver = driver
self.key_words = KeyWords(driver)
self.cf = ConfigOperate(APP_INFO_CONFIG_PATH)
self.log = logging.getLogger('main.' + __name__)
self.handle_page = data.get('页面', None)
self.handle_step = data.get('步骤', None)
self.element_key = data.get('元素', None)
self.handle_value = data.get('操作值', None)
self.description = data.get('描述', None)
self.step = { # 执行步骤
'输入': self._step_input,
'点击': self._step_click,
'左滑': self._step_swipe_left,
'右滑': self._step_swipe_right,
'上滑': self._step_swipe_up,
'下滑': self._step_swipe_down,
'验证元素存在': self._expect_has_element,
'验证元素不存在': self._expect_has_not_element,
'验证toast': self._get_toast,
'等待': self._wait_second,
}
def run(self):
self.log.info('开始执行用例: 页面-{ym},步骤-{bz},元素-{ys}{isczz}'.format(
ym=self.handle_page,
bz=self.handle_step,
ys=self.element_key,
isczz=',操作值-%s' % self.handle_value if self.handle_value else '')
)
step_fun = self.step.get(self.handle_step, None) # 获取操作函数名
if step_fun is None:
message = '没有找到步骤为"%s"的操作代码!' % self.handle_step
self.log.error(message)
raise ValueError(message)
return step_fun()
这样EXCEL里每次读取一行数据,appium就会执行相应操作,用例就可以按照自定的规则写在excel里就行,方便管理和维护。
4.3元素信息保存和读取
appium定位元素是按照id、xpath、android等方式来定位具体元素的,故在保存元素的时候,需把定位方式和元素一起保存,后再分割处理.
5、unittest+ddt启动测试
5.1 unittest类重构传参
因为是多设备运行,所以多进程运行时需要传入不同的设备信息,所以重写unittest.TestCase的构造函数
def __init__(self, methodName='runTest', param=None):
super(CaseTests, self).__init__(methodName)
global device
device = param
5.2 运用ddt库数据驱动,批量执行用例
@ddt.data(*ExcelData(EXCEL_BASE_PATH, sheet_name=0).dict_data())
@screenshot
def test_excel(self, data):
run_result = ExcelHandle(self.driver, data).run()
# 把结果保存到result文件夹里
excel_op = ExcelData(os.path.join(EXCEL_RESULT_DIR, 'case_result_%s.xls' % self.now), sheet_name=0)
excel_op.result_write(data['rowNum'], run_result)
self.assertEqual(run_result, 'PASS')
5.3 运行
启动Appium Server类,再启动多进程分别为各个设备执行用例。
if __name__ == '__main__':
cf = ConfigOperate(DEVICE_LIST_CONFIG_PATH)
section_list = cf.get_sections()
server = Server()
# 先尝试停掉上次运行后的僵尸appium服务
for section in section_list:
port = cf.get_value(section, 'port')
bport = cf.get_value(section, 'bport')
for p in [port, bport]:
server.kill_server(p)
time.sleep(2)
server.main() # 启动Appium服务
time.sleep(10) # 加个时间等待服务先启动
process = []
for device in section_list:
p = multiprocessing.Process(target=start, args=(device,))
p.start()
process.append(p)
for p in process:
p.join() # 等待所有进程执行完毕
五、其他
主要的思路就是这些,后面就是丰富关键字库,以及提高稳定性和错误兼容度。另外,现在只做了Android的支持,后面需考虑同时运行IOS和Android设备的兼容。