1. 关键字驱动框架简介

原理及特点

  1. 关键字驱动测试是数据驱动测试的一种改进类型,它也被称为表格驱动测试或者基于动作字的测试。
  2. 主要关键字包括三类:被操作对象(Item)、操作行为(Operation)和操作值(Value),用面向对象形式可将其表现为 Item.Operation(Value)
  3. 将测试逻辑按照这些关键字进行分解,形成数据文件。
  4. 用关键字的形式将测试逻辑封装在数据文件中,测试工具只要能够解释这些关键字即可对其应用自动化。

优势

  1. 执行人员可以不需要太多的技术:一旦框架建立,手工测试人员和非技术人员都可以很容易的编写自动化测试脚本。
  2. 简单易懂:它存在Excel表格中,没有编码,测试脚本容易阅读和理解。关键字和操作行为这样的手工测试用例,使它变得更容易编写和维护。
  3. 早期介入:可以在应用未提交测试之前,就可以建立关键字驱动测试用例对象库,从而减少后期工作。使用需求和其它相关文档进行收集信息,关键字数据表可以建立手工测试程序。
  4. 代码的重用性:用关键字的形式将测试用例及数据进行组装并解释执行,提高代码的可重用性。

2. 工程结构说明

工程结构

整个测试框架分为四层,通过分层的方式,测试代码更容易理解,维护起来较为方便。

第一层是“测试工具层”:

  • util 包:用于实现测试过程中调用的工具类方法,例如读取配置文件、页面元素的操作方法、操作 Excel 文件、生成测试报告、发送邮件等。
  • conf 包:配置文件及全局变量。
  • log 目录:日志输出文件。
  • exception_pic 目录:失败用例的截图保存目录。

第二层是“服务层”:相当于对测试对象的一个业务封装。对于接口测试,是对远程方法的一个实现;对于 UI 测试,是对页面元素或操作的一个封装

  • action 包:封装具体的页面动作,如点击、输入文本等。

第三层是“测试用例逻辑层”:该层主要是将服务层封装好的各个业务对象,组织成测试逻辑,进行校验

  • bussiness_process 包:基于关键字的形式,实现单条、多条用例的测试脚本逻辑。
  • test_data 目录:Excel 数据文件,包含用例步骤、被操作对象、操作动作、操作值、测试结果等。

第四层是“测试场景层”:将测试用例组织成测试场景,实现各种级别 cases 的管理,如冒烟,回归等测试场景

  • main.py:本框架工程的运行主入口。

框架特点

  1. 基于关键字测试框架,即使不懂开发技术的测试人员也可以实施自动化测试,便于在整个测试团队中推广和使用自动化测试技术,降低自动化测试实施的技术门槛。
  2. 使用外部测试数据文件,使用Excel管理测试用例的集合和每个测试用例的所有执行步骤,实现在一个文件中完成测试用例的维护工作。
  3. 通过定义关键字、操作元素的定位方式和定位表达式和操作值,就可以实现每个测试步骤的执行,可以更加灵活地实现自动化测试的需求。
  4. 基于关键字的方式,可以进行任意关键字的扩展,以满足更加复杂的自动化测试需求。
  5. 实现定位表达式和测试代码的分离,实现定位表达式直接在数据文件中进行维护。
  6. 框架提供日志功能,方便调试和监控自动化测试程序的执行。

3. 工程代码实现

action 包

action 包为框架第二层“服务层”,相当于对测试对象的一个业务封装。对于接口测试,是对远程方法的一个实现;对于 UI 测试,是对页面元素或操作的一个封装。

page_action.py

该模块基于关键字格式,封装了页面操作的常用函数,如打开浏览器、点击、输入文本等。

  1 from selenium import webdriver2 import time3 import traceback4 from util.datetime_util import *5 from util.find_element_util import *6 from util.ini_parser import *7 from util.log_util import *8 9 10 DRIVER = ""11 12 13 # 初始化浏览器14 def init_browser(browser_name):15     global DRIVER16     if browser_name.lower() == "chrome":17         DRIVER = webdriver.Chrome(CHROME_DRIVER)18     elif browser_name.lower() == "firefox":19         DRIVER = webdriver.Firefox(FIREFOX_DRIVER)20     elif browser_name.lower() == "ie":21         DRIVER = webdriver.Ie(IE_DRIVER)22     else:23         warning("浏览器【%s】不支持,已默认启动chrome" % browser_name)24         DRIVER = webdriver.Chrome(CHROME_DRIVER)25 26 27 # 访问指定url28 def visit(url):29     global DRIVER30     DRIVER.get(url)31 32 33 # 输入操作34 def input(locate_method, locate_exp, value):35     global DRIVER36     # 方式1:直接传定位方式和定位表达式37     if locate_method in ["id", "xpath", "classname", "name", "tagname", "linktext",38                              "partial link text", "css selector"]:39         find_element(DRIVER, locate_method, locate_exp).send_keys(value)40     # 方式2:通过ini文件的key找到value,再分割定位方式和定位表达式41     else:42         parser = IniParser(ELEMENT_FILE_PATH)43         locate_method, locate_exp = tuple(parser.get_value(locate_method, locate_exp).split(">"))44         find_element(DRIVER, locate_method, locate_exp).send_keys(value)45 46 47 # 点击操作48 def click(locate_method, locate_exp):49     global DRIVER50     # 方式1:直接传定位方式和定位表达式51     if locate_method in ["id", "xpath", "classname", "name", "tagname", "linktext",52                              "partial link text", "css selector"]:53         find_element(DRIVER, locate_method, locate_exp).click()54     # 方式2:通过ini文件的key找到value,再分割定位方式和定位表达式55     else:56         parser = IniParser(ELEMENT_FILE_PATH)57         locate_method, locate_exp = tuple(parser.get_value(locate_method, locate_exp).split(">"))58         find_element(DRIVER, locate_method, locate_exp).click()59 60 61 # 清空输入框操作62 def clear(locate_method, locate_exp):63     global DRIVER64     # 方式1:直接传定位方式和定位表达式65     if locate_method in ["id", "xpath", "classname", "name", "tagname", "linktext",66                              "partial link text", "css selector"]:67         find_element(DRIVER, locate_method, locate_exp).clear()68     # 方式2:通过ini文件的key找到value,再分割定位方式和定位表达式69     else:70         parser = IniParser(ELEMENT_FILE_PATH)71         locate_method, locate_exp = tuple(parser.get_value(locate_method, locate_exp).split(">"))72         find_element(DRIVER, locate_method, locate_exp).clear()73 74 75 # 切换frame76 def switch_frame(locate_method, locate_exp):77     global DRIVER78     # 方式1:直接传定位方式和定位表达式79     if locate_method in ["id", "xpath", "classname", "name", "tagname", "linktext",80                              "partial link text", "css selector"]:81         DRIVER.switch_to.frame(find_element(DRIVER, locate_method, locate_exp))82     # 方式2:通过ini文件的key找到value,再分割定位方式和定位表达式83     else:84         parser = IniParser(ELEMENT_FILE_PATH)85         locate_method, locate_exp = tuple(parser.get_value(locate_method, locate_exp).split(">"))86         DRIVER.switch_to.frame(find_element(DRIVER, locate_method, locate_exp))87 88 89 # 切换主frame90 def switch_home_frame():91     global DRIVER92     DRIVER.switch_to.default_content()93 94 95 # 断言96 def assert_word(keyword):97     global DRIVER98     assert keyword in DRIVER.page_source99
100
101 # 休眠
102 def sleep(times):
103     time.sleep(int(times))
104
105
106 # 关闭浏览器
107 def quit():
108     global DRIVER
109     DRIVER.quit()
110
111
112 # 截图函数
113 def take_screenshot():
114     global DRIVER
115     # 创建当前日期目录
116     dir = os.path.join(SCREENSHOT_PATH, get_chinese_date())
117     if not os.path.exists(dir):
118         os.makedirs(dir)
119     # 以当前时间为文件名
120     file_name = get_chinese_time()
121     file_path = os.path.join(dir, file_name+".png")
122     try:
123         DRIVER.get_screenshot_as_file(file_path)
124         # 返回截图文件的绝对路径
125         return file_path
126     except:
127         error("截图发生异常【{}】\n{}".format(file_path, traceback.format_exc()))
128         return file_path
129
130
131 if __name__ == "__main__":
132     init_browser("chrome")
133     visit("http://mail.126.com")
134     print(take_screenshot())

business_process 包

business_process 包是框架第三层“测试用例逻辑层”,该层主要是将服务层封装好的各个业务对象,组织成测试逻辑,进行校验。

case_process.py

  • 测试用例文件的一行数据,拼接其中的操作动作、操作对象、操作值等关键字,形成与 page_action.py 中的函数相对应的字符串,并通过 eval() 转成表达式以执行用例。
  • 记录该用例的测试结果,如测试执行结果、测试执行时间等。
  • 如需数据驱动的用例集,则获取数据驱动的数据源集合,循环将每组数据传递给用例步骤。
  • 如果遇到需要参数化的值 ${变量名},则根据数据驱动的数据源,根据变量名进行参数化。

 1 import traceback2 import re3 from util.global_var import *4 from util.log_util import *5 from util.datetime_util import *6 from util.excel_util import Excel7 from action.page_action import *8 9
10 # 执行一条测试用例(即一行测试数据)
11 def execute_case(excel_file_path, case_data, test_data_source=None):
12     # 用例数据格式校验
13     if not isinstance(case_data, (list, tuple)):
14         error("测试用例数据格式有误!测试数据应为列表或元组类型!【%s】" % case_data)
15         case_data[TEST_SCRIPT_EXCEPTION_INFO_COL] = "测试用例数据格式有误!应为列表或元组类型!【%s】" % case_data
16         case_data[TEST_SCRIPT_TEST_RESULT_COL] = "Fail"
17     # 该用例无需执行
18     if case_data[TEST_SCRIPT_IS_EXECUTE_COL].lower() == "n":
19         info("测试用例步骤【%s】无需执行" % case_data[TEST_SCRIPT_NAME_COL])
20         return
21     # excel对象初始化
22     if isinstance(excel_file_path, Excel):
23         excel = excel_file_path  # 如果传入的是excel对象,则直接使用
24     else:
25         excel = Excel(excel_file_path)  # 如果传入的是文件路径,则初始化excel对象
26     # 获取各关键字
27     operation_action = case_data[TEST_SCRIPT_ACTION_COL]  # 操作动作(即函数名)
28     locate_method = case_data[TEST_SCRIPT_LOCATE_METHOD_COL]  # 定位方式
29     locate_expression = case_data[TEST_SCRIPT_LOCATE_EXPRESSION_COL]  # 定位表达式
30     operation_value = case_data[TEST_SCRIPT_VALUE_COL]  # 操作值
31     # 由于数据驱动,需要进行参数化的值
32     if test_data_source:
33         if re.search(r"\$\{\w+\}", str(operation_value)):
34             # 取出需要参数化的值
35             key = re.search(r"\$\{(\w+)\}", str(operation_value)).group(1)
36             operation_value = re.sub(r"\$\{\w+\}", str(test_data_source[key]), str(operation_value))
37             # 将参数化后的值回写excel测试结果中,便于回溯
38             case_data[TEST_SCRIPT_VALUE_COL] = operation_value
39     # 拼接关键字函数
40     if locate_method and locate_expression:
41         if operation_value:
42             func = "%s('%s', '%s', '%s')" % (operation_action, locate_method, locate_expression, operation_value)
43         else:
44             func = "%s('%s', '%s')" % (operation_action, locate_method, locate_expression)
45     else:
46         if operation_value:
47             func = "%s('%s')" % (operation_action, operation_value)
48         else:
49             func = "%s()" % operation_action
50     # 执行用例
51     try:
52         eval(func)
53         info("测试用例步骤执行成功:【{}】 {}".format(case_data[TEST_SCRIPT_NAME_COL], func))
54         case_data[TEST_SCRIPT_TEST_RESULT_COL] = "Pass"
55     except:
56         info("测试用例步骤执行失败:【{}】 {}".format(case_data[TEST_SCRIPT_NAME_COL], func))
57         case_data[TEST_SCRIPT_TEST_RESULT_COL] = "Fail"
58         error(traceback.format_exc())
59         # 进行截图
60         case_data[TEST_SCRIPT_SCREENSHOT_PATH_COL] = take_screenshot()
61         # 异常信息记录
62         case_data[TEST_SCRIPT_EXCEPTION_INFO_COL] = traceback.format_exc()
63     # 测试时间记录
64     case_data[TEST_SCRIPT_TEST_TIME_COL] = get_english_datetime()
65     return case_data
66
67
68 if __name__ == "__main__":
69     excel = Excel(TEST_DATA_FILE_PATH)
70     excel.get_sheet("登录(调试用)")
71     all_data = excel.get_all_row_data()
72     for data in all_data[1:]:
73         execute_case(excel, data)

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         # 最后一列为“是否执行”列,无需取值
30         for i in range(len(data[:-1])):
31             row_dict[head_line_data[i]] = data[i]
32         result_list.append(row_dict)
33     return result_list
34
35
36 if __name__ == "__main__":
37     from util.global_var import *
38     print(get_test_data(TEST_DATA_FILE_PATH, "搜索词"))
39     # [{'搜索词': '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 business_process.case_process import execute_case6 from business_process.data_source_process import get_test_data7 8 9 # 执行具体模块的用例sheet(登录sheet,添加联系人sheet等)10 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【%s】不存在,停止执行!" % sheet_name)30         return31     # 获取测试用例集sheet的全部行数据32     all_row_data = excel.get_all_row_data()33     if len(all_row_data) <= 1:34         error("sheet【%s】数据不大于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[TEST_SCRIPT_TEST_RESULT_COL].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         # 校验用例步骤sheet名是否存在83         if row_data[MAIN_CASE_SCRIPT_SHEET_COL] not in excel.get_all_sheet():84             error("#" * 50 + " 用例步骤集【%s】不存在! " % row_data[MAIN_CASE_SCRIPT_SHEET_COL] + "#" * 50 + "\n")85             row_data[MAIN_CASE_TEST_RESULT_COL] = "Fail"86             excel.write_row_data(head_line_data, None, True, "red")87             excel.write_row_data(row_data)88             continue89         # 跳过不需要执行的测试用例集90         if row_data[MAIN_CASE_IS_EXECUTE_COL].lower() == "n":91             info("#" * 50 + " 测试用例集【%s】无需执行!" % row_data[MAIN_CASE_CASE_NAME_COL] + "#" * 50 + "\n")92             continue93         # 记录本用例集的测试时间94         row_data[MAIN_CASE_TEST_TIME_COL] = get_english_datetime()95         # 判断本测试用例集是否进行数据驱动96         if row_data[MAIN_CASE_DATA_SOURCE_SHEET_COL]:97             # 校验测试数据集sheet名是否存在98             if row_data[MAIN_CASE_DATA_SOURCE_SHEET_COL] not in excel.get_all_sheet():99                 error("#" * 50 + " 测试数据集【%s】不存在! " % row_data[MAIN_CASE_DATA_SOURCE_SHEET_COL] + "#" * 50 + "\n")
100                 row_data[MAIN_CASE_TEST_RESULT_COL] = "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[MAIN_CASE_DATA_SOURCE_SHEET_COL])
106             # 每条数据进行一次本用例集的测试
107             for data_source in test_data_source:
108                 info("-" * 50 + " 测试用例集【%s】开始执行!" % row_data[MAIN_CASE_CASE_NAME_COL] + "-" * 50)
109                 excel, test_result_flag = suite_process(excel, row_data[MAIN_CASE_SCRIPT_SHEET_COL], data_source)
110                 # 记录本用例集的测试结果
111                 if test_result_flag:
112                     info("#" * 50 + " 测试用例集【%s】执行成功! " % row_data[MAIN_CASE_CASE_NAME_COL] + "#" * 50 + "\n")
113                     row_data[MAIN_CASE_TEST_RESULT_COL] = "Pass"
114                 else:
115                     error("#" * 50 + " 测试用例集【%s】执行失败! " % row_data[MAIN_CASE_CASE_NAME_COL] + "#" * 50 + "\n")
116                     row_data[MAIN_CASE_TEST_RESULT_COL] = "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[MAIN_CASE_CASE_NAME_COL] + "-" * 50)
125             excel, test_result_flag = suite_process(excel, row_data[MAIN_CASE_SCRIPT_SHEET_COL])
126             # 记录本用例集的测试结果
127             if test_result_flag:
128                 info("#" * 50 + " 测试用例集【%s】执行成功! " % row_data[MAIN_CASE_SCRIPT_SHEET_COL] + "#" * 50 + "\n")
129                 row_data[MAIN_CASE_TEST_RESULT_COL] = "Pass"
130             else:
131                 error("#" * 50 + " 测试用例集【%s】执行失败! " % row_data[MAIN_CASE_SCRIPT_SHEET_COL] + "#" * 50 + "\n")
132                 row_data[MAIN_CASE_TEST_RESULT_COL] = "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_1, "登录1")
144     excel = main_suite_process(TEST_DATA_FILE_PATH, "测试用例集")
145     create_excel_report_and_send_email(excel, "182230124@qq.com", "UI自动化测试", "请查收附件:UI自动化测试报告")

util 包

util 包属于第一层的测试工具层:用于实现测试过程中调用的工具类方法,例如读取配置文件、页面元素的操作方法、操作 Excel 文件、生成测试报告、发送邮件等。

global_var.py

本模块用于定义测试过程中所需的全局变量。

 1 import os2 3 4 # 工程根路径5 PROJECT_ROOT_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))6 7 # 元素定位方法的ini配置文件路径8 ELEMENT_FILE_PATH = os.path.join(PROJECT_ROOT_PATH, "conf", "ElementsRepository.ini")9
10 # excel文件路径
11 TEST_DATA_FILE_PATH = os.path.join(PROJECT_ROOT_PATH, "test_data", "test_case.xlsx")
12
13 # 驱动路径
14 CHROME_DRIVER = "E:\\auto_test_driver\\chromedriver.exe"
15 IE_DRIVER = "E:\\auto_test_driver\\IEDriverServer.exe"
16 FIREFOX_DRIVER = "E:\\auto_test_driver\\geckodriver.exe"
17
18 # 截图路径
19 SCREENSHOT_PATH = os.path.join(PROJECT_ROOT_PATH, "exception_pic")
20
21 # 日志配置文件路径
22 LOG_CONF_FILE_PATH = os.path.join(PROJECT_ROOT_PATH, "conf", "Logger.conf")
23
24 # 测试报告存放路径
25 TEST_REPORT_FILE_DIR = os.path.join(PROJECT_ROOT_PATH, "test_report")
26
27 # 对应excel测试数据文件中具体模块sheet中的列号
28 TEST_SCRIPT_NAME_COL = 1
29 TEST_SCRIPT_ACTION_COL = 2
30 TEST_SCRIPT_LOCATE_METHOD_COL = 3
31 TEST_SCRIPT_LOCATE_EXPRESSION_COL = 4
32 TEST_SCRIPT_VALUE_COL = 5
33 TEST_SCRIPT_IS_EXECUTE_COL = 6
34 TEST_SCRIPT_TEST_TIME_COL = 7
35 TEST_SCRIPT_TEST_RESULT_COL = 8
36 TEST_SCRIPT_EXCEPTION_INFO_COL = 9
37 TEST_SCRIPT_SCREENSHOT_PATH_COL = 10
38
39 # 对应excel测试数据文件中“测试用例集”sheet列号
40 MAIN_CASE_CASE_NAME_COL = 3
41 MAIN_CASE_BROWSER_NAME_COL = 5
42 MAIN_CASE_SCRIPT_SHEET_COL = 6
43 MAIN_CASE_DATA_SOURCE_SHEET_COL = 7
44 MAIN_CASE_IS_EXECUTE_COL = 8
45 MAIN_CASE_TEST_TIME_COL = 9
46 MAIN_CASE_TEST_RESULT_COL = 10
47
48 # 测试结果统计
49 TOTAL_CASE = 0
50 PASS_CASE = 0
51 FAIL_CASE = 0
52
53
54 if __name__ == "__main__":
55     print(PROJECT_ROOT_PATH)

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             error("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     # print(excel.get_row_data(1))
128     # print(excel.get_col_data(1))
129     # excel.write_row_data(["4", None, "嘻哈"], "green", True, "red")
130     excel.insert_row_data(1, [1,2,3])
131     excel.save(get_timestamp())  

   

ini_reader.py

本模块封装了对 ini 配置文件的读取操作。

 1 import configparser2 3 4 class IniParser:5 6     # 初始化打开指定ini文件并指定编码7     def __init__(self, file_path):8         self.cf = configparser.ConfigParser()9         self.cf.read(file_path, encoding="utf-8")
10
11     # 获取所有分组名称
12     def get_sections(self):
13         return self.cf.sections()
14
15     # 获取指定分组的所有键
16     def get_options(self, section):
17         return self.cf.options(section)
18
19     # 获取指定分组的键值对
20     def get_items(self, section):
21         return self.cf.items(section)
22
23     # 获取指定分组的指定键的值
24     def get_value(self, section, key):
25         return self.cf.get(section, key)
26
27
28 if __name__ == "__main__":
29     from conf.global_var import *
30     parser = IniParser(ELEMENT_FILE_PATH)
31     print(parser.get_sections())
32     print(parser.get_options("126mail_indexPage"))
33     print(parser.get_value("126mail_indexPage", 'indexpage.frame'))

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", "测试邮件", "正文")

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

log_util.py

该模块封装了日志打印输出、级别设定等功能。

 1 import logging2 import logging.config3 from conf.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("hiphop")
33     warning("hello")
34     error("这是一个error日志")

report_util.py

生成测试结果文件并发送邮件。

 1 from util.email_util import send_mail2 from util.datetime_util import *3 4 5 # 生成测试报告并发送邮件6 def create_excel_report_and_send_email(excel_obj, receiver, subject, content):7     """8     :param excel_obj: excel对象用于保存文件9     :param timestamp: 用于文件命名的时间戳
10     :return: 返回excel测试报告文件名
11     """
12     time_stamp = get_timestamp()
13     report_path = excel_obj.save(subject, time_stamp)
14     send_mail(report_path, receiver, subject+"_"+time_stamp, content)

conf 目录

conf 目录属于第一层测试工具层,用于存储各配置文件。

elements_repository.ini

该配置文件存储了各页面的元素对象的定位方式和定位表达式。

 1 [126mail_indexPage]2 indexPage.loginlink=xpath>//a[contains(text(),'密码登录')]3 indexPage.frame=xpath>//iframe[contains(@id,'x-URS-iframe')]4 indexPage.username=xpath>//input[@name='email']5 indexPage.password=xpath>//input[@name='password']6 indexPage.loginbutton=id>dologin7 8 [126mail_homePage]9 homePage.addressLink=xpath>//div[text()='通讯录']
10
11 [126mail_contactPersonPage]
12 contactPersonPage.createButton=xpath>//span[text()='新建联系人']
13 contactPersonPage.name=xpath>//a[@title='编辑详细姓名']/preceding-sibling::div/input
14 contactPersonPage.email=xpath>//*[@id='iaddress_MAIL_wrap']//input
15 contactPersonPage.starContacts=xpath>//span[text()='设为星标联系人']/preceding-sibling::span/b
16 contactPersonPage.phone=xpath>//*[@id='iaddress_TEL_wrap']//dd//input
17 contactPersonPage.otherinfo=xpath>//textarea
18 contactPersonPage.confirmButton=xpath>//span[.='确 定']

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\\UIKeywordFramework\\log\\ui_test.log', 'a')[handler_hand03]
class=handlers.RotatingFileHandler
level=INFO
formatter=form01
args=('E:\\pycharm_project_dir\\UIKeywordFramework\\log\\ui_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 business_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     # 生成测试报告并发送邮件
11     create_excel_report_and_send_email(excel, ['itsjuno@163.com', '182230124@qq.com'], report_name, "请查收附件:UI自动化测试报告")
12
13
14 # 全量测试:执行主sheet的用例集
15 def suite_test(report_name):
16     excel = main_suite_process(TEST_DATA_FILE_PATH, "测试用例集")
17     create_excel_report_and_send_email(excel, ['itsjuno@163.com', '182230124@qq.com'], report_name, "请查收附件:UI自动化测试报告")
18
19
20 if __name__ == "__main__":
21     # smoke_test("UI自动化测试报告_冒烟测试")
22     suite_test("UI自动化测试报告_全量测试")

test_report 目录

本目录用于存放测试结果文件。

exception_pic 目录

本目录用于存放失败用例的截图。

log 目录

本目录用于存放日志输出文件(日志内容同时也会输出到控制台)。

log/ui_test.log:

UI 自动化测试框架:关键字驱动+数据驱动相关推荐

  1. 自动化测试框架——关键字驱动

    目录 1.内容介绍 2.什么是关键字驱动 2.1.解决什么问题? 2.2.如何来做? 2.3.概念 3.​如何设计关键字驱动 3.1.如何设计程序? 3.2.Cases编写 3.3.Web操作 3.4 ...

  2. ui自动化测试框架_数据驱动 vs 关键字驱动:对搭建UI自动化测试框架的探索

    谢谢打开这篇文章的每个你 关注我们 点击右上角 ··· 设为星标 UI自动化测试用例剖析 让我们先从分析一端自动化测试案例的代码开始我们的旅程.以下是我之前写的一个自动化测试的小Demo.这个Demo ...

  3. 能涨薪3k的UI 自动化测试框架:关键字驱动+数据驱动

    [文章末尾给大家留下了大量的福利] 1. 关键字驱动框架简介 原理及特点 关键字驱动测试是数据驱动测试的一种改进类型,它也被称为表格驱动测试或者基于动作字的测试. 主要关键字包括三类:被操作对象(It ...

  4. python3.7界面设计_基于selenium+Python3.7+yaml+Robot Framework的UI自动化测试框架

    前端自动化测试框架 项目说明 本框架是一套基于selenium+Python3.7+yaml+Robot Framework而设计的数据驱动UI自动化测试框架,Robot Framework 作为执行 ...

  5. 实战 | UI 自动化测试框架设计与 PageObject 改造

    本文节选自霍格沃兹<测试开发实战进阶>课程教学内容,进阶学习文末加群. 在 UI 自动化测试过程中,面对复杂的业务场景,经常会遇到这样的挑战: 简单的录制/回放速度快,但无法适应复杂场景: ...

  6. 测试开发 - 十年磨一剑(五)UI自动化测试框架与分层结构

    一.UI自动化测试框架 Selenium是自动化工具,工具,工具!面试的时候不想再听到谁说用的测试框架是Selenium. 构成框架的组件,最起码应该具备以下的功能,才能够称为一个完整的自动化测试框架 ...

  7. python ui自动化测试框架_基于python语言下的UI自动化测试框架搭建(一)

    最近在搭一个UI自动化测试框架,想把整个搭建过程分享出来,如果有不对的地方,希望大家能够指正,首先创建一个名称为,antomation_framework_demo的工程文件, pycharm中工程及 ...

  8. 整装待发 QTA UI自动化测试框架迎来大更新

    2019独角兽企业重金招聘Python工程师标准>>> 整装待发 QTA UI自动化测试框架迎来大更新 QTA是什么 QTA是一个跨平台的测试自动化工具,适用于后台.原生或混合型客户 ...

  9. WEB UI自动化测试框架搭建(一)_公用方法Utils

    本栏目内的所有项目使用的都是PyCharm 2020.1专业版,可以下载后自行在网上找教程破解. WEB UI自动化测试框架搭建(一)~(七)源代码:https://download.csdn.net ...

最新文章

  1. 漫谈 ClickHouse 在实时分析系统中的定位与作用
  2. java 微网站_java架构之路-(微服务专题)初步认识微服务与nacos初步搭建
  3. Matlab | Matlab从入门到放弃(9)——浮点数取整
  4. 转:linux中fork()函数详解
  5. 线程中task取消_Rust Async: async-task源码分析
  6. python爬虫入门,10分钟就够了,这可能是我见过最简单的基础教学
  7. visio 画箭头_在visio2013中画箭头的具体操作
  8. MapReduce中名字的通俗解释--故事会
  9. linux中test的用法,如何在Linux中使用test命令
  10. UG CAM 开发获取工序导航器当前选择的操作、程序组、几何体、刀具方法,获得名字并修改名字
  11. 二维码制作并压缩下载
  12. 开启 Linux 版的 Window 子系统(WSL)
  13. 中科院计算所培训中心四季度课程安排
  14. Hibernate 官网 config code
  15. Android 模拟器加速
  16. 大数据技术原理与应用第2版-林子雨版-课后习题答案
  17. 我的.Subtext二次开发之路系列:兵马未动,粮草先行
  18. 7nm舱泊一体SoC的新玩家
  19. Oracle用户的状态有几种?分别表示什么含义?
  20. 如何理解深度学习中的卷积?

热门文章

  1. 从基于网络的安装服务器安装操作系统,使用Windows2012R2 WDS服务部署Windows 10
  2. android图形编辑6,图片编辑P图制作app
  3. java中针对safari、chrome下载pdf、excel、word等文档变成exe文件解决办法
  4. 老夫花了 3 天时间整理了一份史上最强 Java 学习路线图,送给有缘人!
  5. 封装类Double与基本类型double
  6. 秦淮数据发布2021 ESG报告 连续三年PUE优于行业平均水平
  7. Cutterman切图神器安装
  8. SpringBoot原理篇:bean的多种加载方法之 使用上下文对象regist注入bean
  9. 作为IT人,如何靠副业赚到第一桶金!
  10. 一周极客热文:从分析8000条软件工程师招聘信息所学到的