(测试用例读取写入excel)appium+pytest数据驱动
Python 中处理 excel 数据的模块非常多,比如: xlxd(只读)、xlwd(只写)、openpyxl(可读写)
Excel 文件和下面的py文件代码一定要在同一个文件夹内,不然需要指定具体的 Excel 文件路径
注意:excel文件为xlsx,不能是xls再转换成xlsx格式的文件,会报错
执行excel中存放的字符串对应的方法
使用的是string的反射机制、利用类对象的__getattribute__(funcname)()实现动态调用类对象的方法
比如:find操作分为find_element、find_elements,那怎么让driver执行具体的函数?
driver.__getattribute__(find_)(getattr(AppiumBy,selector_),selector_value_)
其中find_是获取的excel定义的调用哪个find方法,值是find_element、find_elements
driver.__getattribute__(find_)就是返回find_element或find_elements函数的引用,就是driver.find_element()或driver.find_elements
getattr(AppiumBy,selector_)是按照excel中指定的定位类型XPATH、ID动态获取AppiumBy类变量值
driver.__getattribute__(find_)(getattr(AppiumBy,selector_),selector_value_)就等于
driver.find_elements(AppiumBy.ID,selector_value_) 假设selector_是ID
怎么将执行的结果存放到excel指定名称的变量?
通过driver.__getattribute__(find_)(getattr(AppiumBy,selector_),selector_value_)返回了一个Weblement对象,需要赋值变量名为excel中定义的save_object值。假设save_object的值为ele1
如果直接写save_object =driver.__getattribute__(find_)(getattr(AppiumBy,selector_),selector_value_)
那save_object 这个变量的值的appUI 元素,而不是ele1=ui元素赋值
如何给指定变量名的变量赋值?
可以通过自定义一块空间,然后对内存空间进行属性名、属性值的赋值 setattr()
实例化类的时候就会申请一块空间
故创建一个Context类,在需要赋值给设定的变量时
使用setattr(context,destname,destvalue)去设定,还是看刚才的例子,可以通过下面代码将find_element()返回的元素赋值给save_obj_指定的变量名
setattr(Context,save_obj_,driver.__getattribute__(find_)(getattr(AppiumBy,selector_),selector_value_))
后续通过getattr(context,obj_name)拿到obj_name这个变量名的值
1.excel读写
# excel_readUtil.py from openpyxl import load_workbook import pandas class HandleExcel: """ 封装excel文件处理类 """ def __init__(self, filename, sheetname=None): """ 定义构造方法 :param filename: 文件名=实例属性 :param sheetname: 表单名,如果表单名只有一个可以设置为默认值 """ self.filename = filename self.sheetname = sheetname def get_cases(self): """ 获取所有的测试用例,实例方法 :return:为嵌套字典的列表 """ # 打开文件:使用load_workbook传入文件名 wb = load_workbook(self.filename) # 返回创建一个Workbook的对象, 相当是一个excel文件 if self.sheetname is None: # 定位表单,判断是否制定表单默认空,为第一个表单 ws = wb.active # active 获取第一个表单 else: ws = wb[self.sheetname] # 否则获取指定的表单 # min_row = 最小行号,max_row=最大行号(可以不写) # min_col = 最小列号,max_col=最大列号 # values_only = 获取单元格的值 # 获取表头的信息,使用 iter_rows方法,嵌套元祖的元祖,省略最小行号 head_data_tuple = tuple(ws.iter_rows(max_row=1, values_only=True))[0] one_list = [] for one_tuple in tuple(ws.iter_rows(min_row=2, values_only=True)): # 不需要表头最小行号为2,不需要最大行号,最大最小列号 # zip 函数将表头的元祖与每一行用例所在的元祖进行拼接,dict转换为字典后,添加到列表当中 one_list = [] one_list.append(dict(zip(head_data_tuple, one_tuple))) return one_list # 为嵌套字典的列表 def get_sheet_name(self): excel_total = pandas.ExcelFile(filename) if self.sheetname is None: sheet_names = excel_total.sheet_names # print(sheet_names) return sheet_names else: print("当前标签名: " + self.sheetname) def get_one_case(self, row): """ 获取某一条测试用例 :param row: 行号 :return:嵌套字典的列表,使用位置进行获取 """ return self.get_cases()[row - 1] def get_max_row(self): # 获取用例数量 list1 = self.get_cases() max_num = len(list1) return max_num def write_result(self, row, actual, result): """ 写入数据到测试用例指定的行列中 :param row: 行号 :param actual: 实际结果 :param result: 用例执行的结果(Pass或者Fail) :return: """ # 同一个Workbook对象, 如果将数据写入到多个表单中, 那么只有最后一个表单能写入成功,需要创建不同的对象 other_wb = load_workbook(self.filename) # 创建对象 = 打开一个文件 if self.sheetname is None: other_ws = other_wb.active else: other_ws = other_wb[self.sheetname] # 写入 if isinstance(row, int) and (2 <= row <= other_ws.max_row): # 不能修改表头,下一行开始,行号大于2,小于最大的行号 other_ws.cell(row=row, column=6, value=actual) # 在第六行写入实际结果 other_ws.cell(row=row, column=7, value=result) # 在第七行写入用例执行的结果 other_wb.save(self.filename) # save 保存文件 other_wb.close() # close关闭 ----- 读数据的时候不需要关闭,写数据的时候可关闭或不关闭 else: # 如果不是整数,行号小于2,并且大于最大的行号 print("传入的行号有误, 行号应为大于1的整数") def choose_case(self, choose_name, y_name="是否执行"): list_case = self.get_cases() num_case = self.get_max_row() run_list = [] for i in range(0, num_case): y = list_case[i][y_name] if y == 'y': aa = list_case[i][choose_name] run_list.append(aa) else: print("跳过该用例: "+list_case[i][choose_name]) print(run_list) return run_list if __name__ == '__main__': # 自己写的模块自己用使用 main 函数 filename = "C://study//pythonT//pythonProject//test_cases//caseqq.xlsx" sheetname = "case" # 指定第二个表单名 # 创建一个对象,filename=文件名和sheetname=表单名可以不传 # do_excel = HandleExcel(filename) # 传文件名,不传默认第一个表单 do_excel = HandleExcel(filename, sheetname) # 获取所有的测试用例cases,使用对象调用实例方法 cases = do_excel.get_cases() # print(cases) # print(cases[0]['序号']) # 写入,在第二行写入"20230918", "设置Pass" # do_excel.write_result(2, "20230918", "设置pass") # sheets = do_excel.get_sheet_name() do_excel.choose_case('步骤名') # excel被选择的字段
2.单例模式
# driver_configure.py # coding:utf-8 __author__ = 'may' ''' description:driver配置 ''' import os.path from appium import webdriver from config import operator_yaml from config.all_path import project_path class DriverConfigure(object):
# _instance = None def __new__(cls, *args, **kw): """ 使用单例模式将类型设置为运行时只有一个实例, 在其他python类中使用基类时, 可以创建多个对象,保证所有的对象都基于一个浏览 :param args: :param kw: :return: hasattr()函数功能用来检测对象object中是否含有名为**的属性, 如果有就返回True,没有就返回False """ if not hasattr(cls, '_instance'): orig = super(DriverConfigure, cls) path = os.path.join(project_path, "config\config.yaml") data = operator_yaml.readconfigyaml(path) # 远程控制,通过appium可设置;若是真机,直接填写http://localhost:4723/wd/hub 或者http://127.0.0.1:4723/wd/hub即可 cls._instance = orig.__new__(cls) # 发送指令到appium server cls._instance.driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", data["desired_caps"]) return cls._instance class DriverClinet(DriverConfigure): def get_driver(self): return self.driver
3.action 文件
# ren_excel_step.py from util.excel_readUtil import HandleExcel from appium.webdriver import WebElement from appium.webdriver.common.appiumby import AppiumBy from config.driver_configure import DriverClinet from util.assertUtil import AssertUtil import allure class Context: pass def step_case(fimename, sheetname): driver = DriverClinet().get_driver() case = HandleExcel(fimename, sheetname).get_cases() # 遍历测试用例下的具体步骤 for step in case: action_ = step.get('action', None) # 如果用step['action'],当step没有action会抛异常,action指操作有定位元素、点击、输入等操作 desc_d = step.get('description', None) # 获取描述内容 find_ = step.get('find', None) # 定位元素,是find_element、还是find_elements selector_ = step.get('selector', None) # 定位元素的类型 selector_value_ = step.get('selector_value', None) # 定位元素的值 save_obj_ = step.get('save_object', None) # 将操作的结果保存的对象名 operate_obj_ = step.get('operate_object', None) # 对哪个对象进行操作 inputtext_ = step.get('inputtext', None) # 输入操作的驶入内容 attribute_value_ = step.get('attribute_value', None) # 获取对象什么属性 if action_ == 'desc': allure.dynamic.title(desc_d) if action_ == 'find': if save_obj_: if selector_ and selector_value_: # driver.find_element(getattr(AppiumBy,selector_),selector_value_) # #使用getattr(AppiumBy,selector_) 将excel定义的ID、XPATH按照字符串取到AppiumBy中对应的定位值 setattr(Context, save_obj_, driver.__getattribute__(find_)(getattr(AppiumBy, selector_), selector_value_)) # driver.__getattribute__(self, __name) # driver.__getattribute__(find_)通过对象按照传入的参数,执行对象指定的方法, # 如果是find_element就执行find_element,如果是find_elements就执行find_elements, # 思考: 需要考虑将find查找到的对象用save_object定义的变量名的变量去接收,需要自定义一块空间,然后利用setattribute将值赋给指定的变量 else: raise ValueError( '在用例文件{}行缺少定义selector_或selector_value_的值'.format(case.index(step))) pass else: raise ValueError('在用例文件{}行缺少定义save_obj的值'.format(case.index(step))) if action_ == 'click': if operate_obj_: getattr(Context, operate_obj_).__getattribute__(action_)() # print("运行click") else: raise ValueError('在用例文件{}行缺少定义operate_obj_的值'.format(case.index(step))) if action_ == 'send_keys': if operate_obj_: if inputtext_: getattr(Context, operate_obj_).__getattribute__(action_)(inputtext_) # print("运行send_keys") else: raise ValueError('在用例文件{}行缺少定义inputtext_的值'.format(case.index(step))) else: raise ValueError('在用例文件{}行缺少定义operate_obj_的值'.format(case.index(step))) if action_ == 'get_attribute': if save_obj_: if operate_obj_: if attribute_value_: ele_obj = getattr(Context, operate_obj_) if isinstance(ele_obj, WebElement): # 如果是find_element返回的是单个元素 setattr(Context, save_obj_, ele_obj.__getattribute__(action_)(attribute_value_)) if isinstance(ele_obj, list): # 如果是find_elements返回的是list setattr(Context, save_obj_, [i.__getattribute__(action_)(attribute_value_) for i in ele_obj]) else: raise ValueError('在用例文件{}行缺少定义attribute_value_的值'.format(case.index(step))) else: raise ValueError('在用例文件{}行缺少定义operate_obj_的值'.format(case.index(step))) pass else: raise ValueError('在用例文件{}行缺少定义save_obj_的值'.format(case.index(step))) if action_ == 'assert': step_ = step.get('step_name', None) # 获取步骤名 assert_type_ = step.get('assert_type', None) assert_value_ = step.get('assert_value', None) expect_value_ = step.get('expect_value', None) if step_: # 判断步骤名称,设置为必填的情况,要不然无法执行assert指令,如果不需要allure报告的话,该判断可以不用 with allure.step(step_): if assert_value_: if str(assert_value_).find('$') == 0: # 需要取变量 assert_value_ = getattr(Context, str(assert_value_).lstrip('$')) if str(expect_value_).find('$') == 0: expect_value_ = getattr(Context, str(expect_value_).lstrip('$')) if assert_type_ and assert_value_: AssertUtil(assert_type_, getattr(Context, assert_value_), expect_value_) """ if assert_type_ == 'assert_text_in': AssertUtil(assert_type_, getattr(Context, assert_value_), expect_value_,) AssertUtil.assert_text_in(getattr(Context, assert_value_), expect_value_, '{}不在{}中'.format(expect_value_, assert_value_)) if assert_type_ == 'assert_equal': assert assert_value_ == expect_value_, "实际值{}与期望值{}不相等".format(assert_value_, expect_value_) if assert_type_ == 'assert_not_none': assert assert_value_, '期望值{}是None'.find(assert_value_) """ else: raise ValueError('在用例文件{}行缺少定义step_的值'.format(case.index(step))) else: raise ValueError('在用例文件{}行缺少定义assert_type_的值'.format(case.index(step)))
setattr(Context, save_obj_, [i.__getattribute__(action_)(attribute_value_) for i in ele_obj])
[i.__getattribute__(action_)(attribute_value_) for i in ele_obj]
是一个列表推导式,用于遍历 ele_obj
中的每一个元素 i
,获取其 action_
属性,并调用该属性对应的方法,将 attribute_value_
作为参数传入,然后将结果组成一个新的列表。
整段代码的意思是:对于 ele_obj
中的每一个元素 i
,获取其 action_
属性,并调用该属性对应的方法,将 attribute_value_
作为参数传入,然后将结果设置为 Context
对象的 save_obj_
属性
4.测试用例调用:
test_*.py
import allure import pytest from util.loggerUtil import Logger import os from action import run_excel_step from config.all_path import project_path from config import read_yaml from util.excel_readUtil import HandleExcel path = os.path.join(project_path, "config\mydata.yaml") data = read_yaml.YamlUtil(path).read_yaml() case_y = HandleExcel(data['filename'], data['sheetname']).choose_case('用例名称') # 获取要执行的测试用例 print(case_y) @allure.epic('QQ项目') @allure.feature('测试手机QQ登录界面') class TestA: @pytest.mark.parametrize('case_yy', case_y) def test_10(self, case_yy): filename = data['filename'] # filename = 'C://Users//YM520//Desktop//caseqq.xlsx' # sheetname = 'test_01_login' run_excel_step.step_case(filename, case_yy) allure.dynamic.story(case_yy) file_log = 'example.log' logger = Logger(file_log).logger_may() logger.info("验证logger") if __name__ == '__main__': pytest.main()
run.py
import pytest import os if __name__ == '__main__': # pytest.main(['-vs', './test_cases/test_01.py']) # os.system('allure generate ./temp -o ./report --clean') pytest.main(["-s", "./", "--capture=sys"]) # --capture=sys会把报错的情况写进测试报告中 os.system('allure generate report/result -o report/allure_html --clean')
运行报告: