Appium自动化测试框架:关键字驱动+数据驱动
1. 关键字驱动框架简介
原理及特点
- 关键字驱动测试是数据驱动测试的一种改进类型,它也被称为表格驱动测试或者基于动作字的测试。
- 主要关键字包括三类:被操作对象(Item)、操作行为(Operation)和操作值(Value),用面向对象形式可将其表现为 Item.Operation(Value)。
- 将测试逻辑按照这些关键字进行分解,形成数据文件。
- 用关键字的形式将测试逻辑封装在数据文件中,测试工具只要能够解释这些关键字即可对其应用自动化。
优势
- 执行人员可以不需要太多的技术:一旦框架建立,手工测试人员和非技术人员都可以很容易的编写自动化测试脚本。
- 简单易懂:它存在 Excel 表格中,没有编码,测试脚本容易阅读和理解。关键字和操作行为这样的手工测试用例,使它变得更容易编写和维护。
- 早期介入:可以在应用未提交测试之前,就可以建立关键字驱动测试用例对象库,从而减少后期工作。使用需求和其它相关文档进行收集信息,关键字数据表可以建立手工测试程序。
- 代码的重用性:用关键字的形式将测试用例及数据进行组装并解释执行,提高代码的可重用性。
2. 框架结构说明
框架结构
整个测试框架分为四层,通过分层的方式,测试代码更容易理解,维护起来较为方便。
第一层是“测试工具层”:
- util 包:用于实现测试过程中调用的工具类方法,例如读取配置文件、页面元素的操作方法、操作 Excel 文件、生成测试报告、发送邮件等。
- conf 包:配置文件及全局变量。
- log 目录:日志输出文件。
- exception_pic 目录:失败用例的截图保存目录。
第二层是“服务层”:相当于对测试对象的一个业务封装。对于接口测试,是对远程方法的一个实现;对于 UI 测试,是对页面元素或操作的一个封装。
- action 包:封装具体的页面动作,如点击、输入文本等。
第三层是“测试用例逻辑层”:该层主要是将服务层封装好的各个业务对象,组织成测试逻辑,进行校验。
- bussiness_process 包:基于关键字的形式,实现单条、多条用例的测试脚本逻辑。
- test_data 目录:Excel 数据文件,包含用例步骤、被操作对象、操作动作、操作值、测试结果等。
第四层是“测试场景层”:将测试用例组织成测试场景,实现各种级别 cases 的管理,如冒烟,回归等测试场景。
- main.py:本框架工程的运行主入口。
框架特点
- 基于关键字测试框架,即使不懂开发技术的测试人员也可以实施自动化测试,便于在整个测试团队中推广和使用自动化测试技术,降低自动化测试实施的技术门槛。
- 使用外部测试数据文件,使用Excel管理测试用例的集合和每个测试用例的所有执行步骤,实现在一个文件中完成测试用例的维护工作。
- 通过定义关键字、操作元素的定位方式和定位表达式和操作值,就可以实现每个测试步骤的执行,可以更加灵活地实现自动化测试的需求。
- 基于关键字的方式,可以进行任意关键字的扩展,以满足更加复杂的自动化测试需求。
- 实现定位表达式和测试代码的分离,实现定位表达式直接在数据文件中进行维护。
- 框架提供日志功能,方便调试和监控自动化测试程序的执行。
3. 框架代码实现
action 包
action 包为框架第二层“服务层”,相当于对测试对象的一个业务封装。对于接口测试,是对远程方法的一个实现;对于 UI 测试,是对页面元素或操作的一个封装。
page_action.py
本模块基于关键字格式,封装了页面操作的常用函数,如打开 APP、点击、输入文本等。
1 import traceback2 import os3 import time4 from appium import webdriver5 from util.get_desired_caps import get_desired_caps6 from util.datetime_util import *7 from util.find_element_util import *8 from util.global_var import *9 from util.log_util import *
10
11
12 DRIVER = ""
13
14
15 # 打开APP,获取webdriver对象
16 def open_app():
17 global DRIVER
18 desired_caps = get_desired_caps()
19 DRIVER = webdriver.Remote(APPIUM_SERVER, desired_caps)
20
21
22 # 设定开始活动页
23 def open_start_activity(app_name, start_activity_name):
24 global DRIVER
25 DRIVER.start_activity(app_name, start_activity_name)
26
27
28 # 退出APP
29 def quit_app():
30 global DRIVER
31 DRIVER.quit()
32
33
34 # 在页面输入框中输入数据
35 def input_string(location_type, locator_expression, input_content):
36 global DRIVER
37 find_element(DRIVER, location_type, locator_expression).send_keys(input_content)
38
39
40 # 清除输入框默认内容
41 def clear(location_type, locator_expression):
42 global DRIVER
43 find_element(DRIVER, location_type, locator_expression).clear()
44
45
46 # 点击页面元素
47 def click(location_type, locator_expression):
48 global DRIVER
49 find_element(DRIVER, location_type, locator_expression).click()
50
51
52 # 断言界面源码是否存在某关键字或关键字符串
53 def assert_string_in_pagesource(assert_string):
54 global DRIVER
55 try:
56 assert assert_string in DRIVER.page_source, "%s not found in page source!" % assert_string
57 info("断言成功【关键字:{}】".format(assert_string))
58 except:
59 error("断言失败【关键字:{}】".format(assert_string))
60 raise
61
62
63 # 强制等待
64 def sleep(sleep_seconds):
65 time.sleep(int(sleep_seconds))
66
67
68 # 批量断言
69 def assert_app_list(location_type, locator_expression, assert_string):
70 global DRIVER
71 assert_string_list = assert_string.split(",")
72 elements = find_element(DRIVER, location_type, locator_expression)
73 for element in elements[:3]:
74 assert element.text in assert_string_list
75
76
77 # 截图函数
78 def take_screenshot():
79 global DRIVER
80 # 创建当前日期目录
81 dir = os.path.join(EXCEPION_PIC_PATH, get_chinese_date())
82 if not os.path.exists(dir):
83 os.makedirs(dir)
84 # 以当前时间为文件名
85 file_name = get_chinese_time()
86 file_path = os.path.join(dir, file_name+".png")
87 try:
88 DRIVER.get_screenshot_as_file(file_path)
89 # 返回截图文件的绝对路径
90 return file_path
91 except:
92 print("截图发生异常【{}】".format(file_path))
93 traceback.print_exc()
94 return file_path
business_process 包
business_process 包是框架第三层“测试用例逻辑层”,该层主要是将服务层封装好的各个业务对象,组织成测试逻辑,进行校验。
case_process.py
- 测试用例文件的一行数据,拼接其中的操作动作、操作对象、操作值等关键字,形成与 page_action.py 中的函数相对应的字符串,并通过 eval() 转成表达式以执行用例。
- 记录该用例的测试结果,如测试执行结果、测试执行时间等。
- 如需数据驱动的用例集,则获取数据驱动的数据源集合,循环将每组数据传递给用例步骤。
- 如果遇到需要参数化的值 ${变量名},则根据数据驱动的数据源,根据变量名进行参数化。
data_source_process.py
本模块实现了获取数据驱动所需的数据源集合。
- 根据数据源 sheet 名,获取该 sheet 所有行数据,每行数据作为一组测试数据。
- 每行数据作为一个字典,存储在一个列表中。如 [{"登录用户名": "xxx", "登录密码": "xxx", ...}, {...}, ...]
1 from util.excel_util import Excel2 from util.global_var import *3 from util.log_util import *4 5 6 # 数据驱动7 # 每行数据作为一个字典,存储在一个列表中。如[{"登录用户名": "xxx", "登录密码": "xxx", ...}, {...}, ...]8 def get_test_data(excel_file_path, sheet_name):9 # excel对象初始化
10 if isinstance(excel_file_path, Excel):
11 excel = excel_file_path
12 else:
13 excel = Excel(excel_file_path)
14 # 校验sheet名
15 if not excel.get_sheet(sheet_name):
16 error("sheet【】不存在,停止执行!" % sheet_name)
17 return
18 result_list = []
19 all_row_data = excel.get_all_row_data()
20 if len(all_row_data) <= 1:
21 error("sheet【】数据不大于1行,停止执行!" % sheet_name)
22 return
23 # 将参数化的测试数据存入全局字典
24 head_line_data = all_row_data[0]
25 for data in all_row_data[1:]:
26 if data[-1].lower() == "n":
27 continue
28 row_dict = {}
29 for i in range(len(data[:-1])):
30 row_dict[head_line_data[i]] = data[i]
31 result_list.append(row_dict)
32 return result_list
33
34
35 if __name__ == "__main__":
36 from util.global_var import *
37 print(get_test_data(TEST_DATA_FILE_PATH, "搜索词"))
38 # [{'搜索词': 'python', '断言词': 'python'}, {'搜索词': 'mysql', '断言词': 'mysql5.6'}]
main_process.py
本模块基于 case_process.py 和 data_source_process.py,实现关键字驱动+数据驱动的测试用例集的执行。
- suite_process():执行具体的测试用例步骤 sheet(如“登录” sheet、“搜索” sheet 等)
- main_suite_process():执行“测试用例”主 sheet 的用例集。每行用例集对应一个用例步骤 sheet 和数据源 sheet。
1 from util.excel_util import *2 from util.datetime_util import *3 from util.log_util import *4 from util.global_var import *5 from bussiness_process.case_process import execute_case6 from bussiness_process.data_source_process import get_test_data7 8 9 # 执行具体的测试用例步骤sheet10 def suite_process(excel_file_path, sheet_name, test_data_source=None):11 """12 :param excel_file_path: excel文件绝对路径或excel对象13 :param sheet_name: 测试步骤sheet名14 :param test_data_source: 数据驱动的数据源,默认没有15 :return:16 """17 # 记录测试结果统计18 global TOTAL_CASE19 global PASS_CASE20 global FAIL_CASE21 # 整个用例sheet的测试结果,默认为全部通过22 suite_test_result = True23 # excel对象初始化24 if isinstance(excel_file_path, Excel):25 excel = excel_file_path26 else:27 excel = Excel(excel_file_path)28 if not excel.get_sheet(sheet_name):29 error("sheet【】不存在,停止执行!" % sheet_name)30 return31 # 获取测试用例sheet的全部行数据32 all_row_data = excel.get_all_row_data()33 if len(all_row_data) <= 1:34 error("sheet【】数据不大于1行,停止执行!" % sheet_name)35 return36 # 标题行数据37 head_line_data = all_row_data[0]38 # 切换到测试结果明细sheet,准备写入测试结果39 if not excel.get_sheet("测试结果明细"):40 error("【测试结果明细】sheet不存在,停止执行!")41 return42 excel.write_row_data(head_line_data, None, True, "green")43 # 执行每行的测试用例44 for row_data in all_row_data[1:]:45 result_data = execute_case(excel, row_data, test_data_source)46 # 无需执行的测试步骤,跳过47 if result_data is None:48 continue49 TOTAL_CASE += 150 if result_data[CASESTEP_TEST_RESULT_COL_NO].lower() == "fail":51 suite_test_result = False52 FAIL_CASE += 153 else:54 PASS_CASE += 155 excel.write_row_data(result_data)56 # 切换到测试结果统计sheet,写入统计数据57 if not excel.get_sheet("测试结果统计"):58 error("【测试结果统计】sheet不存在,停止执行!")59 return60 excel.insert_row_data(1, [TOTAL_CASE, PASS_CASE, FAIL_CASE])61 return excel, suite_test_result62 63 64 # 执行【测试用例】主sheet的用例集65 def main_suite_process(excel_file_path, sheet_name):66 # 初始化excel对象67 excel = Excel(excel_file_path)68 if not excel:69 error("excel数据文件【%s】不存在!" % excel_file_path)70 return71 if not excel.get_sheet(sheet_name):72 error("sheet名称【%s】不存在!" % sheet_name)73 return74 # 获取所有行数据75 all_row_datas = excel.get_all_row_data()76 if len(all_row_datas) <= 1:77 error("sheet【%s】数据不大于1行,停止执行!" % sheet_name)78 return79 # 标题行数据80 head_line_data = all_row_datas[0]81 for row_data in all_row_datas[1:]:82 # 跳过不需要执行的测试用例集83 if row_data[TESTCASE_IS_EXECUTE_COL_NO].lower() == "n":84 info("#" * 50 + " 测试用例集【%s】无需执行!" % row_data[TESTCASE_CASE_NAME_COL_NO] + "#" * 50 + "\n")85 continue86 # 记录本用例集的测试时间87 row_data[TESTCASE_TEST_TIME_COL_NO] = get_english_datetime()88 # 校验用例步骤sheet名是否存在89 if row_data[TESTCASE_CASE_STEP_SHEET_NAME_COL_NO] not in excel.get_all_sheet():90 error("#" * 50 + " 用例步骤集【%s】不存在! " % row_data[TESTCASE_CASE_STEP_SHEET_NAME_COL_NO] + "#" * 50 + "\n")91 row_data[TESTCASE_TEST_RESULT_COL_NO] = "Fail"92 excel.write_row_data(head_line_data, None, True, "red")93 excel.write_row_data(row_data)94 continue95 # 判断本测试用例集是否进行数据驱动96 if row_data[TESTCASE_DATA_SOURCE_SHEET_NAME_COL_NO]:97 # 校验测试数据集sheet名是否存在98 if row_data[TESTCASE_DATA_SOURCE_SHEET_NAME_COL_NO] not in excel.get_all_sheet():99 error("#" * 50 + " 测试数据集【%s】不存在! " % row_data[TESTCASE_DATA_SOURCE_SHEET_NAME_COL_NO] + "#" * 50 + "\n") 100 row_data[TESTCASE_TEST_RESULT_COL_NO] = "Fail" 101 excel.write_row_data(head_line_data, None, True, "red") 102 excel.write_row_data(row_data) 103 continue 104 # 获取测试数据集 105 test_data_source = get_test_data(excel, row_data[TESTCASE_DATA_SOURCE_SHEET_NAME_COL_NO]) 106 # 每条数据进行一次本用例集的测试 107 for data_source in test_data_source: 108 info("-" * 50 + " 测试用例集【%s】开始执行!" % row_data[TESTCASE_CASE_NAME_COL_NO] + "-" * 50) 109 excel, test_result_flag = suite_process(excel, row_data[TESTCASE_CASE_STEP_SHEET_NAME_COL_NO], data_source) 110 # 记录本用例集的测试结果 111 if test_result_flag: 112 info("#" * 50 + " 测试用例集【%s】执行成功! " % row_data[TESTCASE_CASE_NAME_COL_NO] + "#" * 50 + "\n") 113 row_data[TESTCASE_TEST_RESULT_COL_NO] = "Pass" 114 else: 115 error("#" * 50 + " 测试用例集【%s】执行失败! " % row_data[TESTCASE_CASE_NAME_COL_NO] + "#" * 50 + "\n") 116 row_data[TESTCASE_TEST_RESULT_COL_NO] = "Fail" 117 # 全部测试步骤结果写入后,最后写入本用例集的标题行和测试结果行数据 118 # 切换到“测试结果明细”sheet,以写入测试执行结果 119 excel.get_sheet("测试结果明细") 120 excel.write_row_data(head_line_data, None, True, "red") 121 excel.write_row_data(row_data) 122 # 本用例集无需数据驱动 123 else: 124 info("-" * 50 + " 测试用例集【%s】开始执行!" % row_data[TESTCASE_CASE_NAME_COL_NO] + "-" * 50) 125 excel, test_result_flag = suite_process(excel, row_data[TESTCASE_CASE_STEP_SHEET_NAME_COL_NO]) 126 # 记录本用例集的测试结果 127 if test_result_flag: 128 info("#" * 50 + " 测试用例集【%s】执行成功! " % row_data[TESTCASE_CASE_NAME_COL_NO] + "#" * 50 + "\n") 129 row_data[TESTCASE_TEST_RESULT_COL_NO] = "Pass" 130 else: 131 error("#" * 50 + " 测试用例集【%s】执行失败! " % row_data[TESTCASE_CASE_NAME_COL_NO] + "#" * 50 + "\n") 132 row_data[TESTCASE_TEST_RESULT_COL_NO] = "Fail" 133 # 全部测试步骤结果写入后,最后写入本用例集的标题行和测试结果行数据 134 # 切换到“测试结果明细”sheet,以写入测试执行结果 135 excel.get_sheet("测试结果明细") 136 excel.write_row_data(head_line_data, None, True, "red") 137 excel.write_row_data(row_data) 138 return excel 139 140 141 if __name__ == "__main__": 142 from util.report_util import create_excel_report_and_send_email 143 # excel, _ = suite_process(TEST_DATA_FILE_PATH, "进入主页") 144 # excel, _ = suite_process(excel, "登录") 145 excel = main_suite_process(TEST_DATA_FILE_PATH, "测试用例") 146 create_excel_report_and_send_email(excel, "182230124@qq.com", "app自动化测试", "请查收附件:app自动化测试报告")
util 包
util 包属于第一层的测试工具层:用于实现测试过程中调用的工具类方法,例如读取配置文件、页面元素的操作方法、操作 Excel 文件、生成测试报告、发送邮件等。
global_var.py
本模块用于定义测试过程中所需的全局变量。
1 import os2 3 4 PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))5 6 # APP配置信息路径7 INI_FILE_PATH = os.path.join(PROJECT_DIR, "conf", "desired_caps_config.ini")8 9 # 异常截图路径
10 EXCEPION_PIC_PATH = os.path.join(PROJECT_DIR, "exception_pic")
11
12 # 日志配置文件路径
13 LOG_CONF_FILE_PATH = os.path.join(PROJECT_DIR, "conf", "logger.conf")
14
15 # 测试数据文件路径
16 TEST_DATA_FILE_PATH = os.path.join(PROJECT_DIR, "test_data", "test_case.xlsx")
17
18 # 测试报告存放路径
19 TEST_REPORT_FILE_DIR = os.path.join(PROJECT_DIR, "test_report")
20
21 # Appium server地址
22 APPIUM_SERVER = 'http://localhost:4723/wd/hub'
23
24 # 测试数据文件中,测试用例sheet中部分列对应的数字序号
25 TESTCASE_CASE_NAME_COL_NO = 0
26 TESTCASE_FRAMEWORK_TYPE_COL_NO = 1
27 TESTCASE_CASE_STEP_SHEET_NAME_COL_NO = 2
28 TESTCASE_DATA_SOURCE_SHEET_NAME_COL_NO = 3
29 TESTCASE_IS_EXECUTE_COL_NO = 4
30 TESTCASE_TEST_TIME_COL_NO = 5
31 TESTCASE_TEST_RESULT_COL_NO = 6
32
33 # 用例步骤sheet中,部分列对应的数字序号
34 CASESTEP_NAME_COL_NO = 0
35 CASESTEP_ACTION_COL_NO = 1
36 CASESTEP_LOCATE_METHOD_COL_NO = 2
37 CASESTEP_LOCATE_EXPRESSION_COL_NO = 3
38 CASESTEP_OPERATION_VALUE_COL_NO = 4
39 CASESTEP_IS_EXECUTE_COL_NO = 5
40 CASESTEP_TEST_TIME_COL_NO = 6
41 CASESTEP_TEST_RESULT_COL_NO = 7
42 CASESTEP_EXCEPTION_INFO_COL_NO = 8
43 CASESTEP_EXCEPTION_PIC_DIR_COL_NO = 9
44
45 # 数据源sheet中,是否执行列对应的数字编号
46 DATASOURCE_DATA = 0
47 DATASOURCE_KEYWORD = 1
48 DATASOURCE_IS_EXECUTE = 2
49 DATASOURCE_TEST_TIME = 3
50 DATASOURCE_TEST_RESULT = 4
51
52 # 测试执行结果统计
53 TOTAL_CASE = 0
54 PASS_CASE = 0
55 FAIL_CASE = 0
56
57
58 if __name__ == "__main__":
59 print(PROJECT_DIR)
find_element_util.py
本模块封装了基于显式等待的界面元素定位方法。
1 from selenium.webdriver.support.ui import WebDriverWait2 3 4 # 显式等待一个元素5 def find_element(driver, locate_method, locate_exp):6 # 显式等待对象(最多等10秒,每0.2秒判断一次等待的条件)7 return WebDriverWait(driver, 10, 0.2).until(lambda x: x.find_element(locate_method, locate_exp))8 9 # 显式等待一组元素
10 def find_elements(driver, locate_method, locate_exp):
11 # 显式等待对象(最多等10秒,每0.2秒判断一次等待的条件)
12 return WebDriverWait(driver, 10, 0.2).until(lambda x: x.find_elements(locate_method, locate_exp))
excel_util.py
本模块封装了对 excel 的读写操作(openpyxl 版本:3.0.4)。
1 import os2 from openpyxl import load_workbook3 from openpyxl.styles import PatternFill, Font, Side, Border4 from util.datetime_util import *5 from util.global_var import *6 from util.log_util import *7 8 9 # 支持excel读写操作的工具类10 class Excel:11 12 # 初始化读取excel文件13 def __init__(self, file_path):14 if not os.path.exists(file_path):15 return16 self.wb = load_workbook(file_path)17 # 初始化默认sheet18 self.ws = self.wb.active19 self.data_file_path = file_path20 # 初始化颜色字典,供设置样式用21 self.color_dict = {"red": "FFFF3030", "green": "FF008B00"}22 23 def get_all_sheet(self):24 return self.wb.get_sheet_names()25 26 # 打开指定sheet27 def get_sheet(self, sheet_name):28 if sheet_name not in self.get_all_sheet():29 print("sheet名称【%s】不存在!" % sheet_name)30 return31 self.ws = self.wb.get_sheet_by_name(sheet_name)32 return True33 34 # 获取最大行号35 def get_max_row_no(self):36 # openpyxl的API的行、列索引默认都从1开始37 return self.ws.max_row38 39 # 获取最大列号40 def get_max_col_no(self):41 return self.ws.max_column42 43 # 获取所有行数据44 def get_all_row_data(self, head_line=True):45 # 是否需要标题行数据的标识,默认需要46 if head_line:47 min_row = 1 # 行号从1开始,即1为标题行48 else:49 min_row = 250 result = []51 # min_row=None:默认获取标题行数据52 for row in self.ws.iter_rows(min_row=min_row, max_row=self.get_max_row_no(), max_col=self.get_max_col_no()):53 result.append([cell.value for cell in row])54 return result55 56 # 获取指定行数据57 def get_row_data(self, row_num):58 # 0 为标题行59 return [cell.value for cell in self.ws[row_num+1]]60 61 # 获取指定列数据62 def get_col_data(self, col_num):63 # 索引从0开始64 return [cell.value for cell in tuple(self.ws.columns)[col_num]]65 66 # 追加行数据且可以设置样式67 def write_row_data(self, data, font_color=None, border=True, fill_color=None):68 if not isinstance(data, (list, tuple)):69 print("写入数据失败:数据不为列表或元组类型!【%s】" % data)70 self.ws.append(data)71 # 设置字体颜色72 if font_color:73 if font_color.lower() in self.color_dict.keys():74 font_color = self.color_dict[font_color]75 # 设置单元格填充颜色76 if fill_color:77 if fill_color.lower() in self.color_dict.keys():78 fill_color = self.color_dict[fill_color]79 # 设置单元格边框80 if border:81 bd = Side(style="thin", color="000000")82 # 记录数据长度(否则会默认与之前行最长数据行的长度相同,导致样式超过了该行实际长度)83 count = 084 for cell in self.ws[self.get_max_row_no()]:85 # 设置完该行的实际数据长度样式后,则退出86 if count > len(data) - 1:87 break88 if font_color:89 cell.font = Font(color=font_color)90 # 如果没有设置字体颜色,则默认给执行结果添加字体颜色91 else:92 if cell.value is not None and isinstance(cell.value, str):93 if cell.value.lower() == "pass" or cell.value == "成功":94 cell.font = Font(color=self.color_dict["green"])95 elif cell.value.lower() == "fail" or cell.value == "失败":96 cell.font = Font(color=self.color_dict["red"])97 if border:98 cell.border = Border(left=bd, right=bd, top=bd, bottom=bd)99 if fill_color:
100 cell.fill = PatternFill(fill_type="solid", fgColor=fill_color)
101 count += 1
102
103 # 指定行插入数据(行索引从0开始)
104 def insert_row_data(self, row_no, data, font_color=None, border=True, fill_color=None):
105 if not isinstance(data, (list, tuple)):
106 print("写入数据失败:数据不为列表或元组类型!【%s】" % data)
107 for idx, cell in enumerate(self.ws[row_no+1]): # 此处行索引从1开始
108 cell.value = data[idx]
109
110 # 生成写入了测试结果的excel数据文件
111 def save(self, save_file_name, timestamp):
112 save_dir = os.path.join(TEST_REPORT_FILE_DIR, get_chinese_date())
113 if not os.path.exists(save_dir):
114 os.mkdir(save_dir)
115 save_file = os.path.join(save_dir, save_file_name + "_" + timestamp + ".xlsx")
116 self.wb.save(save_file)
117 info("生成测试结果文件:%s" % save_file)
118 return save_file
119
120
121 if __name__ == "__main__":
122 from util.global_var import *
123 from util.datetime_util import *
124 excel = Excel(TEST_DATA_FILE_PATH)
125 excel.get_sheet("测试结果统计")
126 # print(excel.get_all_row_data())
127 # excel.write_row_data(["4", None, "嘻哈"], "green", True, "red")
128 excel.insert_row_data(1, [1,2,3])
129 excel.save(get_timestamp())
ini_reader.py
本模块封装了对 ini 配置文件的读取操作。
1 import os2 import configparser3 4 5 # 读取ini文件的工具类6 class IniParser:7 8 # 初始化打开ini文件9 def __init__(self, file_path):
10 if not os.path.exists(file_path):
11 print("ini文件【%s】不存在!" % file_path)
12 return
13 self.cf = configparser.ConfigParser()
14 self.cf.read(file_path, encoding="utf-8")
15
16 # 获取所有分组
17 def get_sections(self):
18 return self.cf.sections()
19
20 # 获取指定分组的所有键
21 def get_options(self, section):
22 return self.cf.options(section) # 注意,获取的键会自动转小写
23
24 # 获取指定分组的所有键值对
25 def get_items(self, section):
26 return dict(self.cf.items(section)) # 注意,获取的键会自动转小写
27
28 # 获取指定分组指定键的值
29 def get_value(self, section, option):
30 return self.cf.get(section, option)
31
32
33 if __name__ == "__main__":
34 from util.global_var import *
35 p = IniParser(INI_FILE_PATH)
36 print(p.get_sections())
37 print(p.get_options("desired_caps"))
38 print(p.get_items("desired_caps"))
39 print(p.get_value("desired_caps", "deviceName"))
email_util.py
本模块封装了邮件发送功能。(示例代码中的用户名/密码已隐藏)
1 import yagmail2 import traceback3 from util.log_util import *4 5 6 def send_mail(attachments_report_name, receiver, subject, content):7 try:8 # 连接邮箱服务器9 # 注意:若使用QQ邮箱,则password为授权码而非邮箱密码;使用其它邮箱则为邮箱密码
10 # encoding设置为GBK,否则中文附件名会乱码
11 yag = yagmail.SMTP(user="*****@163.com", password="*****", host="smtp.163.com", encoding='GBK')
12
13 # 收件人、标题、正文、附件(若多个收件人或多个附件,则可使用列表)
14 yag.send(to=receiver, subject=subject, contents=content, attachments=attachments_report_name)
15
16 # 可简写:yag.send("****@163.com", subject, contents, report)
17
18 info("测试报告邮件发送成功!【邮件标题:%s】【邮件附件:%s】【收件人:%s】" % (subject, attachments_report_name, receiver))
19 except:
20 error("测试报告邮件发送失败!【邮件标题:%s】【邮件附件:%s】【收件人:%s】" % (subject, attachments_report_name, receiver))
21 error(traceback.format_exc())
22
23
24 if __name__ == "__main__":
25 send_mail("e:\\code.txt", "182230124@qq.com", "测试邮件", "正文")
26
datetime_util.py
本模块实现了获取各种格式的当前日期时间。
1 import time2 3 4 # 返回中文格式的日期:xxxx年xx月xx日5 def get_chinese_date():6 year = time.localtime().tm_year7 if len(str(year)) == 1:8 year = "0" + str(year)9 month = time.localtime().tm_mon
10 if len(str(month)) == 1:
11 month = "0" + str(month)
12 day = time.localtime().tm_mday
13 if len(str(day)) == 1:
14 day = "0" + str(day)
15 return "{}年{}月{}日".format(year, month, day)
16
17
18 # 返回英文格式的日期:xxxx/xx/xx
19 def get_english_date():
20 year = time.localtime().tm_year
21 if len(str(year)) == 1:
22 year = "0" + str(year)
23 month = time.localtime().tm_mon
24 if len(str(month)) == 1:
25 month = "0" + str(month)
26 day = time.localtime().tm_mday
27 if len(str(day)) == 1:
28 day = "0" + str(day)
29 return "{}/{}/{}".format(year, month, day)
30
31
32 # 返回中文格式的时间:xx时xx分xx秒
33 def get_chinese_time():
34 hour = time.localtime().tm_hour
35 if len(str(hour)) == 1:
36 hour = "0" + str(hour)
37 minute = time.localtime().tm_min
38 if len(str(minute)) == 1:
39 minute = "0" + str(minute)
40 second = time.localtime().tm_sec
41 if len(str(second)) == 1:
42 second = "0" + str(second)
43 return "{}时{}分{}秒".format(hour, minute, second)
44
45
46 # 返回英文格式的时间:xx:xx:xx
47 def get_english_time():
48 hour = time.localtime().tm_hour
49 if len(str(hour)) == 1:
50 hour = "0" + str(hour)
51 minute = time.localtime().tm_min
52 if len(str(minute)) == 1:
53 minute = "0" + str(minute)
54 second = time.localtime().tm_sec
55 if len(str(second)) == 1:
56 second = "0" + str(second)
57 return "{}:{}:{}".format(hour, minute, second)
58
59
60 # 返回中文格式的日期时间
61 def get_chinese_datetime():
62 return get_chinese_date() + " " + get_chinese_time()
63
64
65 # 返回英文格式的日期时间
66 def get_english_datetime():
67 return get_english_date() + " " + get_english_time()
68
69
70 # 返回时间戳
71 def get_timestamp():
72 year = time.localtime().tm_year
73 if len(str(year)) == 1:
74 year = "0" + str(year)
75 month = time.localtime().tm_mon
76 if len(str(month)) == 1:
77 month = "0" + str(month)
78 day = time.localtime().tm_mday
79 if len(str(day)) == 1:
80 day = "0" + str(day)
81 hour = time.localtime().tm_hour
82 if len(str(hour)) == 1:
83 hour = "0" + str(hour)
84 minute = time.localtime().tm_min
85 if len(str(minute)) == 1:
86 minute = "0" + str(minute)
87 second = time.localtime().tm_sec
88 if len(str(second)) == 1:
89 second = "0" + str(second)
90 return "{}{}{}_{}{}{}".format(year, month, day, hour, minute, second)
91
92
93 if __name__ == "__main__":
94 print(get_chinese_datetime())
95 print(get_english_datetime())
get_desired_caps.py
本模块实现了获取 ini 配置文件中的 Appium 创建 Session 的配置信息。
1 from util.ini_reader import IniParser2 from util.global_var import INI_FILE_PATH3 4 5 def get_desired_caps():6 pcf = IniParser(INI_FILE_PATH)7 items = pcf.get_items("desired_caps") # 获取的键会自动转成小写8 desired_caps = {9 "platformName": items.get("platformname"),
10 "platformVersion": items.get("platformversion"),
11 "deviceName": items.get("devicename"),
12 "appPackage": items.get("apppackage"),
13 "appActivity": items.get("appactivity"),
14 "unicodeKeyboard": items.get("unicodekeyboard"),
15 "autoAcceptAlerts": items.get("autoacceptalerts"),
16 "resetKeyboard": items.get("resetkeyboard"),
17 "noReset": items.get("noreset"),
18 "newCommandTimeout": items.get("newcommandtimeout")
19 }
20 return desired_caps
21
22
23 if __name__ == "__main__":
24 from util.global_var import *
25 print(get_desired_caps())
log_util.py
封装了日志打印输出、级别设定等功能。
1 import logging2 import logging.config3 from util.global_var import *4 5 6 # 日志配置文件:多个logger,每个logger指定不同的handler7 # handler:设定了日志输出行的格式8 # 以及设定写日志到文件(是否回滚)?还是到屏幕9 # 还定了打印日志的级别
10 logging.config.fileConfig(LOG_CONF_FILE_PATH)
11 logger = logging.getLogger("example01")
12
13
14 def debug(message):
15 logging.debug(message)
16
17
18 def info(message):
19 logging.info(message)
20
21
22 def warning(message):
23 logging.warning(message)
24
25
26 def error(message):
27 logging.error(message)
28
29
30 if __name__ == "__main__":
31 debug("hi")
32 info("gloryroad")
33 warning("hello")
34 error("这是一个error日志")
report_util.py
生成测试结果文件并发送邮件。
1 from util.email_util import send_mail2 from util.log_util import *3 from util.datetime_util import *4 5 6 # 生成测试报告并发送邮件7 def create_excel_report_and_send_email(excel_obj, receiver, subject, content):8 """9 :param excel_obj: excel对象用于保存文件
10 :param timestamp: 用于文件命名的时间戳
11 :return: 返回excel测试报告文件名
12 """
13 time_stamp = get_timestamp()
14 report_path = excel_obj.save(subject, time_stamp)
15 send_mail(report_path, receiver, subject+"_"+time_stamp, content)
conf 目录
conf 目录属于第一层测试工具层,用于存储各配置文件。
desired_caps_config.ini
本配置文件存储了 Appium 创建 Session 的配置信息。
[desired_caps]
platformName=Android
platformVersion=6
deviceName=3DN6T16B26001805
appPackage=com.xsteach.appedu
appActivity=com.xsteach.appedu.StartActivity
unicodeKeyboard=True
autoAcceptAlerts=True
resetKeyboard=True
noReset=True
newCommandTimeout=6000
logger.conf
本配置文件用于日志功能的具体配置。
###############################################
[loggers]
keys=root,example01,example02
[logger_root]
level=DEBUG
handlers=hand01,hand02[logger_example01]
handlers=hand01,hand02
qualname=example01
propagate=0[logger_example02]
handlers=hand01,hand03
qualname=example02
propagate=0###############################################
[handlers]
keys=hand01,hand02,hand03[handler_hand01]
class=StreamHandler
level=INFO
formatter=form01
args=(sys.stderr,)[handler_hand02]
class=FileHandler
level=DEBUG
formatter=form01
args=('E:\\pycharm_project_dir\\AppAutoTest\\log\\app_test.log', 'a')[handler_hand03]
class=handlers.RotatingFileHandler
level=INFO
formatter=form01
args=('E:\\pycharm_project_dir\\AppAutoTest\\log\\app_test.log', 'a', 10*1024*1024, 5)###############################################
[formatters]
keys=form01,form02[formatter_form01]
format=%(asctime)s [%(levelname)s] %(message)s
datefmt=%Y-%m-%d %H:%M:%S[formatter_form02]
format=%(name)-12s: [%(levelname)-8s] %(message)s
datefmt=%Y-%m-%d %H:%M:%S
test_data 目录
test_data 目录用于存放测试数据文件(Excel),存储了用例步骤、用例执行关键字、数据源等测试数据。
main.py
本模块是本框架的运行主入口,属于第四层“测试场景层”,将测试用例组织成测试场景,实现各种级别 cases 的管理,如冒烟,回归等测试场景。
- 基于 business_process/main_process.py 中的模块用例 sheet 执行函数或主 sheet 执行函数,组装测试场景。
- 可直接用代码组装测试场景,也可根据 excel 数据文件的用例集合和用例步骤的维护来设定测试场景。
- 完成测试执行后生成测试结果文件并发送邮件。
1 from bussiness_process.main_process import *2 from util.report_util import *3 4 5 # 组装测试场景6 # 冒烟测试7 def smoke_test(report_name):8 excel, _ = suite_process(TEST_DATA_FILE_PATH, "进入主页")9 excel, _ = suite_process(excel, "登录")
10 excel, _ = suite_process(excel, "退出")
11 # 生成测试报告并发送邮件
12 create_excel_report_and_send_email(excel, ['itsjuno@163.com', '182230124@qq.com'], report_name, "请查收附件:app自动化测试报告")
13
14
15 # 全量测试:执行主sheet的用例集
16 def suite_test(report_name):
17 excel = main_suite_process(TEST_DATA_FILE_PATH, "测试用例集")
18 create_excel_report_and_send_email(excel, ['itsjuno@163.com', '182230124@qq.com'], report_name, "请查收附件:app自动化测试报告")
19
20
21 if __name__ == "__main__":
22 # smoke_test("APP自动化测试报告_冒烟测试")
23 suite_test("APP自动化测试报告_全量测试")
test_report 目录
本目录用于存放测试结果文件。
exception_pic 目录
本目录用于存放失败用例的截图。
log 目录
本目录用于存放日志输出文件(日志内容同时也会输出到控制台)。
log/app_test.log:
Python接口自动化测试零基础入门到精通(2023最新版)
相关文章:

Appium自动化测试框架:关键字驱动+数据驱动
1. 关键字驱动框架简介 原理及特点 关键字驱动测试是数据驱动测试的一种改进类型,它也被称为表格驱动测试或者基于动作字的测试。主要关键字包括三类:被操作对象(Item)、操作行为(Operation)和操作值&…...

简单多状态dp【动态规划】
目录 一、按摩师 二、打家劫舍 三、删除并获得点数 四、粉刷房子 五、买卖股票的最佳时机 六、买卖股票的最佳时机(含手续费) 七、买卖股票的最佳时机III 八、买卖股票的最佳时机IV 一、按摩师 class Solution { public:int massage(vector<int>…...

OpenCV中initUndistortRectifyMap ()函数与十四讲中去畸变公式的区别探究
文章目录 1.十四讲中的去畸变公式2. OpenCV中的去畸变公式3. 4个参数和8个参数之间的区别4.initUndistortRectifyMap()函数源码 最近在使用OpenCV对鱼眼相机图像去畸变时发现一个问题,基于针孔模型去畸变时所使用的参数和之前十四讲以及视觉SLAM中的畸变系数有一点不…...

【C++】C++11——智能指针、内存泄漏、智能指针的使用和原理、RAII、auto_ptr、unique_ptr、shared_ptr、weak_ptr
文章目录 C117.智能指针7.1内存泄漏7.2智能指针的概念7.3智能指针的使用7.3.1 auto_ptr7.3.2 unique_ptr7.3.3 shared_ptr7.3.4 weak_ptr C11 7.智能指针 7.1内存泄漏 什么是内存泄漏: 内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏…...

EDUSRC-记某擎未授权与sql注入
目录 360天擎 - 未授权与sql注入 信息收集 FOFA语法 鹰图搜索 360天擎未授权访问 - 数据库信息泄露 漏洞复现 修复方案 360天擎终端安全管理系统ccid处SQL注入 漏洞复现 手动测试方法 修复方案 360天擎 - 未授权与sql注入 通常访问的页面如下,存在登录框…...

1688拍立淘API接口分享
拍立淘接口,顾名思义,就是通过图片搜索到相关商品列表。通过此接口,可以实现图片搜索爆款商品等功能。 接口地址:1688.item_search_img 公共参数 名称类型必须描述keyString是调用key(必须以GET方式拼接在URL中&…...

昇腾910使用记录
一. 压缩文件和解压文件 1. 压缩文件 tar -czvf UNITE-main.tar.gz ./UNITE-main/2. 解压文件 tar -xvf ./UNITE-main/二. CUDA更改为NPU data[label] data[label].cuda() data[instance] data[instance].cuda() data[image] data[image].cuda()更改为 data[label] da…...

从一部iPhone手机看芯片的分类
目录 问题 iPhone X 手机处理器:A11 iPhone X 的两大存储芯片 数字 IC CPU:计算设备的运算核心和控制核心 GPU:图形处理器 ASIC:为解决特定应用问题而定制设计的集成电路 存储芯片:DRAM 和 NAND Flash iPhone…...

arm day 7
完成字符串收发函数的封装并且验证现象,一个字符串发送接受后会有‘\n’ \r src/uart.c #include"uart.h"void uart4_init() {//设置UART4的RCc时钟使能//RCC_MP_APB1ENSETR[16]->1RCC->MP_APB1ENSETR | (0x1<<16);//设置GPIOB和GPIOG的时钟…...

Java基础面试-面向对象
什么是面向对象? 对比面向过程,是两种不同的处理问题角度 面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者(对象),及各自需要做什么 比如洗衣机洗衣服 面向过程会将任务拆解成一系…...

GCC vs. G++:C 与 C++ 编译器的差异和比较
本文将介绍 GCC(GNU Compiler Collection)和 G 编译器的区别,并对它们在 C 和 C 程序开发中的特性和用法进行比较和总结。 引言 在 C 和 C 程序开发中,选择合适的编译器是至关重要的。GCC(GNU Compiler Collection&a…...

MAC m系列docker login报错
错误:ERROR: failed to solve: XXX error getting credentials - err: exit status 1, out: 解决: vi ~/.docker/config.jsonzsxzsx [15时55分55秒] [~] { {"auths": {"harbor-g42c.corp.matrx.team": {"auth": "…...

Redis通用指令和五大基本数据类型常用指令总结
通用指令 keys parttern 查询key (parttern即通配符,不是正则表达式,例如 keys a? 匹配以a开头的长度为2的key) del key 删除key exists key 获取key是否存在 type key 获取key的类型 expire key seconds 为指定key设置有效期,单位秒 …...

uCharts常用图表组件demo
带渐变阴影的曲线图 <view class"charts-box"><qiun-data-charts type"area" :opts"opts" :chartData"chartData" :ontouch"true":background"rgba(256,256,256,0)" /> </view>data(){return{…...

VNC:Timed out waiting for a response from the computer
VNC的服务端使用的是TigerVNC,客户端使用的是RealVNC TigerVNC按其他博客配好后,防火墙ip什么的都配了,vnc客户端怎么连都是超时。 这里建议大家可以尝试一下重启服务器。我的是CentOS的 shutdown -r now 配了2天,最后服务器重启…...

Kotlin 协程 知识点
Android 上的 Kotlin 协程 | Android Developers (google.cn) 官方网址 1.什么是协程? 我觉得协程就是kotlin中一种优雅的实现异步请求 协程(Coroutines)是一种轻量级的并发编程概念,旨在简化异步编程和并发任务的处理。它是…...

简单大方的自我介绍 PPT 格式
自我介绍是展示自己的机会,同时也是展现自信和魅力的重要时刻。通过简单大方的PPT格式,可以更好地展示自己的个性和才华。下面是一些建议,帮助你在自我介绍中展现自信和魅力。 1. 打造简洁而有吸引力的PPT布局: - 选择简洁大方的背…...

panads操作excel
panads简介 pandas是基于Numpy创建的Python包,内置了大量标准函数,能够高效地解决数据分析数据处理和分析任务,pandas支持多种文件的操作,比如Excel,csv,json,txt 文件等,读取文件之…...

【MySQL】联合查询、子查询、合并查询
这里提供了三个表: 表1: mysql> select * from class; -------------- | id | name | -------------- | 1 | 一班 | | 2 | 二班 | | 3 | 三班 | -------------- 3 rows in set (0.01 sec) 表2: mysql> select * fro…...

小程序中如何设置所服务地区的时区
在全球化的背景下,小程序除了在中国使用外,还为海外的华人地区提供服务。例如我们采云小程序为泰国、阿根廷、缅甸等国家的商家就提供过微信小程序。这些商家开通小程序,为本地的华人提供服务。但通常小程序的开发者/服务商位于中国ÿ…...

Linux环境安装mysql8.0
1个人习惯我喜欢给软件安装在/use/local下,我使用的finalshell软件,直接手动新建一个文件夹名字为mysql 2下载mysql wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.20-linux-glibc2.12-x86_64.tar.xz 3解压文件 tar -xvf mysql-8.0.2…...

STM32_DMA_多通道采集ADC出现错位现象
STM32_DMA_多通道采集ADC出现错位现象 问题描述: adcSensorValue[0],adcSensorValue[3],adcSensorValue[6]… //存储通道1数据 adcSensorValue[1],adcSensorValue[4],adcSensorValue[7]… //存储通道2数据 adcSensorValue[2],adcSensorValue[5],adcSensorValue[8]……...

Linux内存管理 (2):memblock 子系统的建立
前一篇:Linux内存管理 (1):内核镜像映射临时页表的建立 文章目录 1. 前言2. 分析背景3. memblock 简介3.1 memblock 数据结构3.2 memblock 接口 4. memblock 的构建过程 1. 前言 限于作者能力水平,本文可能存在谬误,因此而给读者…...

创新学习方式,电大搜题助您迈向成功之路
近年来,随着信息技术的发展,互联网在教育领域发挥的作用越来越显著。贵州开放大学作为国内首家电视大学,一直致力于创新教学模式,帮助学生更好地获取知识。在学习过程中,学生常常遇到疑难问题,而解决这些问…...

Mybatis整理
Mybatis 定义 Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,加载驱动、创建连接、创建statement等繁杂的过程,开发者开发时只需要关注如何编写SQL语句,可以严格控制sql执行性能,灵…...

pytorch定义datase多次重复采样
有的时候训练需要对样本重复抽样为一个batch,可以按如下格式定义: class TrainLoader(Dataset):def __init__(self, fns, repeat1):super(TrainLoader, self).__init__()self.length len(fns) # 数据数量self.repeat repeat # 数据重复次数def __getitem__(self,…...

自动化测试 —— Pytest fixture及conftest详解!
前言 fixture是在测试函数运行前后,由pytest执行的外壳函数。fixture中的代码可以定制,满足多变的测试需求,包括定义传入测试中的数据集、配置测试前系统的初始状态、为批量测试提供数据源等等。fixture是pytest的精髓所在,类似u…...

Nginx解析漏洞
常见的解析漏洞: IIS 5.x/6.0解析漏洞 IIS 7.0/IIS 7.5/ Nginx <0.8.3畸形解析漏洞 Nginx <8.03 空字节代码执行漏洞 Apache解析漏洞 Nginx文件解析漏洞 对于任意文件名,例如:cd.jpg在后面添加/x.php后,即可将文件作为php解析。 原理…...

【机器学习】决策树原理及scikit-learn使用
文章目录 决策树详解ID3 算法C4.5算法CART 算法 scikit-learn使用分类树剪枝参数重要属性和接口 回归树重要参数,属性及接口交叉验证代码示例 一维回归的图像绘制 决策树详解 决策树(Decision Tree)是一种非参数的有监督学习方法,…...

#基于一个小车项目的FREERTOS分析(一)系统时钟
系统时钟 //初始化延迟函数 //SYSTICK的时钟固定为AHB时钟,基础例程里面SYSTICK时钟频率为AHB/8 //这里为了兼容FreeRTOS,所以将SYSTICK的时钟频率改为AHB的频率! //SYSCLK:系统时钟频率 /* 系统定时器是一个 24bit 的向下递减的计数器&…...