混合测试自动化框架(关键字+数据驱动)
关键字驱动或表驱动的测试框架
这个框架需要开发数据表和关键字。这些数据表和关键字独立于执行它们的测试自动化工具,并可以用来“驱动"待测应用程序和数据的测试脚本代码,关键字驱动测试看上去与手工测试用例很类似。在一个关键字驱动测试中,把待测应用程序的功能和每个测试的执行步骤一起写到一个表中。
这个测试框架可以通过很少的代码来产生大量的测试用例。同样的代码在用数据表来产生各个测试用例的同时被复用。
混合测试自动化框架
最普遍的执行框架是上面介绍的所有技术的一个结合,取其长处,弥补其不足。这个混合测试框架是由大部分框架随着时间并经过若干项目演化而来的。
unittest关于测试报告展示用例名称的细节:
在unittest有两个内置属性,可以自定义用例名称
之前的效果:
使用内置属性效果:
框架设计目标
设计出来的框架是直接给测试人员,而且其他的测试人员只需要简单的向里面不断的补充测试用例即可;所以我们的框架设计必须三简化即操作简单,维护简单,扩展简单。
设计框架的同时一定要结合业务流程,而不仅仅靠技术实现,其实技术实现不难,难点对业务流程的理解和把握。
设计框架时要将基础的封装成公用的,如:get请求、post请求和断言封装成同基础通用类。
测试用例要与代码分开,这样便于用例管理,采用数据驱动框架实现。
如下图所示:
通过在excel录入测试用例,框架运行后自动进行用例执行,产生html网页版本的测试报告。
报告结果:
框架用到的技术点
1、语言:python
2、测试框架:unittest(assertEqual)或pytest
3、接口调用:requests(API非常简洁)
4、数据驱动:paramunittest (组装一定的格式数据就可以参数化)
5、数据管理:xlrd(读取excel文件数据)、configparser(读取配置文件)
6、数据格式的转换:ast,json
7、日志处理:logging ---清晰的执行过程,快速定位问题
8、测试报表:HTMLTestReportCN(由网友制作设计,显示清晰美观)
9、测试邮件发送测试报告:smtplib(邮件内容格式设置)、email(收发邮件)
10、持续集成:Jenkins(按策略执行接口测试脚本)
(推荐)混合测试自动化框架(关键字+数据驱动)
数据源实现:
数据源目前使用excel,数据如下:
思路:使用python读取excel数据;使用xlrd3
框架01:新建项目API_KEY_WORD_TEST_FRAME;
步骤1、在项目根目录下新建common的py文件夹和conf的普通文件夹;samples文件夹是用来写测试代码的demo;
步骤2、在conf下新建config.ini文件
编写代码:
[default] # 主机地址 hosts = api.weixin.qq.com
步骤3、在common下新建ini_file_utils.py文件和config_utils.py文件
ini_file_utils.py文件如下:
编写代码:
# encoding: utf-8 # @author: Jeffrey # @file: ini_file_utils.py # @time: 2022/8/4 22:23 # @desc: 读取、写入ini文件 import os import configparser class IniFileUtils: #和框架业务无关的底层代码==》公共底层代码 def __init__(self,file_path): self.ini_file_path = file_path self.conf_obj = configparser.ConfigParser() self.conf_obj.read(self.ini_file_path, encoding='utf-8') def get_config_value(self,section, key): value = self.conf_obj.get(section, key) return value def set_config_value(self,section, key, value): '''设置config.ini文件中的值''' self.conf_obj.set(section, key, value) config_file_obj = open(self.ini_file_path, 'w') self.conf_obj.write(config_file_obj) config_file_obj.close() if __name__ == '__main__': current_path = os.path.dirname(__file__) config_file_path = os.path.join(current_path, '../conf/config.ini') ini_file = IniFileUtils(config_file_path) print(ini_file.get_config_value('default', 'HOSTS'))
config_utils.py文件如下:
编写代码:
# encoding: utf-8 # @author: Jeffrey # @file: config_utils.py # @time: 2022/8/4 22:26 # @desc: 封装读取ini文件的方法 import os from common.ini_file_utils import IniFileUtils current_path = os.path.dirname(os.path.abspath(__file__)) config_file_path = os.path.join(current_path, '../conf/config.ini') class LocalConfig(): # #和框架业务有关系的底层代码 def __init__(self,file_path = config_file_path): self.ini_file_obj = IniFileUtils(file_path) @property def get_hosts(self): '''获取ini文件中的hosts值''' hosts_value = self.ini_file_obj.get_config_value('default', 'hosts') return hosts_value local_config = LocalConfig() if __name__ == '__main__': print(local_config.get_hosts)
步骤4、在samples文件下编写线性脚本,读取excel中的合并单元格
Excel表格如下:
编写代码:
# encoding: utf-8 # @author: Jeffrey # @file: demo01.py # @time: 2022/8/7 14:53 # @desc: excel中合并单元格的读取 import xlrd3 work_book = xlrd3.open_workbook('test_data.xlsx') # 创建一个工作簿对象 sheet_obj = work_book.sheet_by_name('Sheet1') # 创建一个表格对象 print(sheet_obj.cell_value(1,2)) # 获取单元格的值(行,列),从0开始计算,行列实际序号-1 print(sheet_obj.cell_value(7,3)) # 获取单元格的值 print(sheet_obj.cell_value(1,0)) # 合并的单元格,获取的值为空 print(sheet_obj.cell_value(7,0)) # 合并的单元格,获取的值为空 # 包含四个元素(起始行,结束行,其实列,结束列) 包前不包后 print(sheet_obj.merged_cells) # [(1, 5, 0, 1), (5, 9, 0, 1)] # 思路步骤一:判断一个单元格是否是合并的单元格 x = 3 ; y = 0 if x>=1 and x<5: if y>=0 and y<1: print('合并单元格') else: print('非合并单元格') else: print('非合并单元格') # 思路步骤二:for循环的写法 for (min_row,max_row,min_col,max_col) in [(1, 5, 0, 1), (5, 9, 0, 1)]: print(min_row,max_row,min_col,max_col) # 思路步骤三:把思路一和二整合进单元格判断是否是合并的单元格 x = 6 ; y = 0 for (min_row,max_row,min_col,max_col) in sheet_obj.merged_cells: if x >= min_row and x < max_row: if y >= min_col and y < max_col: print('合并单元格') break else: print('非合并单元格') else: print('非合并单元格') # 思路步骤四:让合并单元格的值都等于合并第一个单元格的值,非合并单元格为原值 x = 4 ; y = 1 cell_value = None for (min_row,max_row,min_col,max_col) in sheet_obj.merged_cells: if x >= min_row and x < max_row: if y >= min_col and y < max_col: cell_value = sheet_obj.cell_value(min_row,min_col) break else: cell_value = sheet_obj.cell_value(x, y) else: cell_value = sheet_obj.cell_value(x, y) print(cell_value) # 把上诉代码做成一个方法 def get_merged_cell_value(row_index,col_index): cell_value = None for (min_row, max_row, min_col, max_col) in sheet_obj.merged_cells: if row_index >= min_row and row_index < max_row: if col_index >= min_col and col_index < max_col: cell_value = sheet_obj.cell_value(min_row, min_col) break else: cell_value = sheet_obj.cell_value(row_index, col_index) else: cell_value = sheet_obj.cell_value(row_index, col_index) return cell_value print(get_merged_cell_value(8,0)) # 获取excel中所有的数据 线性脚本 # 步骤一:线性脚本 head = sheet_obj.row_values(0) print(head) # ['学习课程', '步骤序号', '步骤操作', '完成情况'] excel_list = [] excel_dict = {} excel_dict[head[0]] = get_merged_cell_value(1,0) excel_dict[head[1]] = get_merged_cell_value(1,1) excel_dict[head[2]] = get_merged_cell_value(1,2) excel_dict[head[3]] = get_merged_cell_value(1,3) excel_list.append(excel_dict) print(excel_list) # 步骤二: 使用for循环封装 head = sheet_obj.row_values(0) excel_data_list = [] for j in range(1,sheet_obj.nrows): row_value_dict = {} for i in range(sheet_obj.ncols): row_value_dict[head[i]] = get_merged_cell_value(j,i) excel_data_list.append(row_value_dict) print(excel_data_list) for data in excel_data_list: print(data)
步骤5、在common下新建excel_file_utils.py文件
编写代码:
# encoding: utf-8 # @author: Jeffrey # @file: excel_file_utils.py # @time: 2022/8/7 15:52 # @desc: 封装读取excel文件 import os import xlrd3 class ExcelFileUtils(): def __init__(self,excel_file_path, sheet_name): self.excel_file_path = excel_file_path self.sheet_name = sheet_name self.sheet_obj = self.get_sheet() def get_sheet(self): '''根据excel路径已经表名称 创建一个表格对象''' workbook = xlrd3.open_workbook(self.excel_file_path) sheet = workbook.sheet_by_name(self.sheet_name) return sheet def get_row_count(self): '''获取表格实际行数''' row_count = self.sheet_obj.nrows return row_count def get_col_count(self): '''获取表格的实际列数''' col_count = self.sheet_obj.ncols return col_count def get_merged_cell_value(self,row_index, col_index): ''' :param row_index: 行下标 :param col_index: 列下标 :return: 获取单元格的内容 ''' cell_value = None for (min_row, max_row, min_col, max_col) in self.sheet_obj.merged_cells: if row_index >= min_row and row_index < max_row: if col_index >= min_col and col_index < max_col: cell_value = self.sheet_obj.cell_value(min_row, min_col) break else: cell_value = self.sheet_obj.cell_value(row_index, col_index) else: cell_value = self.sheet_obj.cell_value(row_index, col_index) return cell_value def get_all_excel_data_list(self): '''获取excel中所有的数据,以列表嵌套字典的形式''' excel_data_list = [] head = self.sheet_obj.row_values(0) for j in range(1,self.get_row_count()): row_value_dict = {} for i in range(self.get_col_count()): # sheet_obj.ncols 动态获取表格多少列 row_value_dict[head[i]] = self.get_merged_cell_value(j,i) excel_data_list.append(row_value_dict) return excel_data_list if __name__ == '__main__': current_path = os.path.dirname(__file__) file_path = os.path.join(current_path, '../samples/test_data.xlsx') excel_obj = ExcelFileUtils(file_path, 'Sheet1') print(excel_obj.get_all_excel_data_list()) print(excel_obj.get_col_count())
测试执行结果:
步骤6、在项目根目录下新建test_data普通文件夹,把测试用例文件放里面
步骤7、在common下新建testcase_data_utils.py文件把测试用例的数据转换成框架需要用到的格式
编写代码:
# encoding: utf-8 # @author: Jeffrey # @file: testcase_data_utils.py # @time: 2022/8/7 17:02 # @desc: 数据格式转换:把测试用例数据转换成框架需要用到的格式 import os from common.excel_file_utils import ExcelFileUtils current_path = os.path.dirname(__file__) file_path = os.path.join(current_path, '../test_data/testcase_infos.xlsx') class TestCaseDataUtils: def __init__(self): self.test_data = ExcelFileUtils(excel_file_path=file_path, sheet_name='Sheet1')\ .get_all_excel_data_list() def convert_testcase_data_to_dict(self): '''把excel中的测试用例数据转换为[{},{},{}]转换成{"":[],"":[],"":[]}''' test_case_data_dict = {} for row_data in self.test_data: test_case_data_dict.setdefault( row_data['测试用例编号'],[] )\ .append(row_data) return test_case_data_dict def convert_testcase_data_to_list(self): '''把数据{"":[],"":[],"":[]} 转换成["case_id":key,"case_step":value]''' test_case_data_list = [] for key, value in self.convert_testcase_data_to_dict().items(): case_info_dict = {} case_info_dict['case_id'] = key case_info_dict['case_step'] = value test_case_data_list.append(case_info_dict) return test_case_data_list if __name__ == '__main__': # print(TestCaseDataUtils().convert_testcase_data_to_dict()) print(TestCaseDataUtils().convert_testcase_data_to_list())
convert_testcase_data_to_dict()方法的目的是把用例数据以用例编号和用例数据分开
'''把excel中的测试用例数据转换为[{},{},{}]转换成{"":[],"":[],"":[]}'''
查看执行结果:
convert_testcase_data_to_list()方法的目的是以用例编号和用例步骤分开
'''把数据{"":[],"":[],"":[]} 转换成["case_id":key,"case_step":value]'''
转换后数据
框架02,封装requests请求,在common下新建一个requests_utils.py文件
步骤1,封装get请求
编写代码:
# encoding: utf-8 # @author: Jeffrey # @file: requests_utils.py # @time: 2022/8/7 21:42 # @desc: 封装requests 请求 import json import requests from common.config_utils import local_config class RequestsUtils: def __init__(self): self.hosts = local_config.get_hosts self.session = requests.session() def get(self,requests_info): url = 'https://%s%s' % (self.hosts, requests_info['请求地址']) # 使用json.loads()把字符串转成字典 url_params = json.loads(requests_info['请求参数(get)']) # 使用三元运算符判断请求头部信息为空,返回空字典{} header_info = ( json.loads(requests_info['请求头部信息']) if requests_info['请求头部信息'] else {} ) response = self.session.get(url=url, params=url_params, headers=header_info) response.encoding = response.apparent_encoding # 防止响应正文乱码 print(response.text) if __name__ == '__main__': requests_info = {'请求头部信息': '', '请求地址': '/cgi-bin/token', '请求参数(get)': '{"grant_type":"client_credential",' '"appid":"wxf14419077f707856",' '"secret":"92a113bd4b5ffdc72144740dc7123c99"}'} result = RequestsUtils().get(requests_info) print(result)
步骤2、把返回的所有响应信息,作为一个字典,且多一个code字段
编写代码:
result = { 'code':0, 'response_code':response.status_code, 'response_info':response.reason, 'response_headers':response.headers, 'response_body':response.text }
步骤3、封装post请求
编写代码:
def post(self,requests_info): url = 'https://%s%s' % (self.hosts, requests_info['请求地址']) # 使用json.loads()把字符串转成字典 url_params = ( json.loads(requests_info['请求参数(get)']) if requests_info['请求参数(get)'] else {} ) # 使用三元运算符判断请求头部信息为空,返回空字典{} header_info = ( json.loads(requests_info['请求头部信息']) if requests_info['请求头部信息'] else {} ) post_data = ( json.loads(requests_info['请求参数(post)']) if requests_info['请求参数(post)'] else {} ) response = self.session.post(url=url, params=url_params, headers=header_info, json=post_data) response.encoding = response.apparent_encoding # 防止响应正文乱码 result = { 'code':0, 'response_code':response.status_code, 'response_info':response.reason, 'response_headers':response.headers, 'response_body':response.text } return result 测试代码: if __name__ == '__main__': requests_info = {'请求方式': 'post', '请求头部信息': '', '请求地址': '/cgi-bin/tags/create', '请求参数(get)': '{"access_token":"59_KUGkJ-kvKqIeBihdARRDWVg-aXjVBmcKDgxZcwcjcONMrOUD6RyZWHKAdtvcp6IgP5BpK4t4cCO4DRNilZ85_UFph2TTs6nPYRfMYH836oD8H9MEqnIXAGgAEANQW"}', '请求参数(post)': '{"tag":{"name":"P1P2new12"}}'} result = RequestsUtils().post(requests_info) print(result)
步骤4、封装request方法,进行判断请求方式,在调用get或post
编写代码:
def request(self,requests_info): """封装判断请求方式""" request_type = requests_info['请求方式'] if request_type == "get": result = self.get(requests_info) elif request_type == "post": result = self.post(requests_info) else: result = {'code':1,'result':'请求方式不支持'} return result
if __name__ == '__main__': requests_info = {'请求方式': 'head', '请求头部信息': '', '请求地址': '/cgi-bin/tags/create', '请求参数(get)': '{"access_token":"59_KUGkJ-kvKqIrOUD6RyZR9dJWHK_UFph2TTs6nPYRfMYH836oD8H9MEqnIXAGgAEANQW"}', '请求参数(post)': '{"tag":{"name":"P1P2new12"}}'} result = RequestsUtils().request(requests_info) print(result)
查看执行结果:
步骤5、小优化,由于对外的requests,只需要出现requests方法,所以get/post可以在类中做成私有的
标签:__,file,self,excel,value,关键字,测试,自动化,row From: https://www.cnblogs.com/YouJeffrey/p/17132042.html