(测试用例读取写入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这个变量名的值

关于getattr()和setattr()函数详解

 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')

 

运行报告:

 

5.优化及补充

1)对find_element进行封装,增加显性等待,以及其他操作,详情查看

2)断言封装,可参考python断言封装

3)allure定制报告部分,查看allure定制报告

4)有关日志logger


原文链接

posted @ 2023-10-13 20:31  yimu-yimu  阅读(271)  评论(0编辑  收藏  举报