Hybrid test automation framework (keyword + data driven)
Keyword-driven or table-driven testing framework
This framework requires the development of data tables and keywords. These data tables and keywords are independent of the test automation tool that executes them, and can be used to "drive" the test script code of the application and data under test. Keyword-driven testing looks very similar to manual test cases. In a keyword-driven test, the functions of the application under test are written into a table together with the execution steps of each test.
This testing framework can generate a large number of test cases with very little code. The same code is reused while using the data table to generate individual test cases.
Hybrid test automation framework
The most common execution framework is a combination of all the techniques introduced above, taking their strengths and making up for their weaknesses. This hybrid testing framework evolved from most frameworks over time and through several projects.
Unittest details about the test report showing the use case name:
There are two built-in properties in unittest, you can customize the use case name
Previous effect:
Use built-in attribute effects:
Framework design goals
The designed framework is given directly to testers, and other testers only need to simply add test cases to it continuously; therefore, our framework design must be simplified in three ways: simple operation, simple maintenance, and simple expansion.
When designing the framework, it must be combined with the business process, not just relying on technology to achieve it. In fact, technology implementation is not difficult, but the understanding and grasp of the business process is difficult.
When designing the framework, the basic ones should be encapsulated into public ones, such as get requests, post requests and assertions into the same basic general class.
Test cases should be separated from the code to facilitate use case management and implemented using a data-driven framework.
As shown below:
By entering test cases in Excel, the test cases are automatically executed after the framework is run, and an HTML web page version of the test report is generated.
Report results:
Technical points used in the framework
1. Language: python
2. Test framework: unittest (assertEqual) or pytest
3. Interface call: requests (API is very simple)
4. Data-driven: paramunittest (parameterization can be achieved by assembling data in a certain format)
5. Data management: xlrd (read excel file data), configparser (read configuration file)
6. Data format conversion: ast, json
7. Log processing: logging --- clear execution process and quick problem location
8. Test report: HTMLTestReportCN (produced and designed by netizens, the display is clear and beautiful)
9. Test email sending test report: smtplib (email content format setting), email (sending and receiving emails)
10. Continuous integration: Jenkins (execute interface test script according to strategy)
(Recommended) Hybrid test automation framework (keyword + data driven)
Data source implementation:
The data source currently uses excel, and the data is as follows:
Link: https://pan.baidu.com/s/1VvvGYRvGbElSlP6ngg0ktw
Extraction code: ppua
Recommended tutorials related to automated testing:
The latest automated testing self-study tutorial in 2023 is the most detailed tutorial for newbies to get started in 26 days. Currently, more than 300 people have joined major companies by studying this tutorial! ! _bilibili_bilibili
2023 latest collection of Python automated test development framework [full stack/practical/tutorial] collection essence, annual salary after learning 40W+_bilibili_bilibili
Recommended tutorials related to test development
The best in the entire network in 2023, the Byte test and development boss will give you on-site teaching and teach you to become a test and development engineer with an annual salary of one million from scratch_bilibili_bilibili
postman/jmeter/fiddler test tool tutorial recommendation
The most detailed collection of practical tutorials on JMeter interface testing/interface automated testing projects. A set of tutorials for learning jmeter interface testing is enough! ! _bilibili_bilibili
To teach yourself how to capture packets with fiddler in 2023, please be sure to watch the most detailed video tutorial on the Internet [How to Learn to Capture Packets with Fiddler in 1 Day]! ! _bilibili_bilibili
In 2023, the whole network will be honored. The most detailed practical teaching of Postman interface testing at Station B can be learned by novices_bilibili_bilibili
Idea: Use python to read excel data; use xlrd3
Frame 01: New project API_KEY_WORD_TEST_FRAME;
Step 1. Create a new common py folder and a conf ordinary folder in the project root directory; the samples folder is a demo used to write test code;
Step 2. Create a new config.ini file under conf
Write code:
[default] # Main aircraft site hosts = api.weixin.qq.com
Step 3. Create new ini_file_utils.py files and config_utils.py files under common
The ini_file_utils.py file is as follows:
Summarize:
Optical theory is useless. You must learn to follow along and practice it in order to apply what you have learned to practice. At this time, you can learn from some practical cases.
If it is helpful to you, please like and save it to give the author an encouragement. It also makes it easier for you to search quickly next time.
If you don’t understand, please consult the small card below. The blogger also hopes to learn and improve with like-minded testers.
At the appropriate age, choose the appropriate position and try to give full play to your own advantages.
My path to automated test development is inseparable from plans at each stage, because I like planning and summarizing.
Test development video tutorials and study notes collection portal! !
Write code:
# 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'))
The config_utils.py file is as follows:
Write code:
# 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)
Step 4. Write a linear script under the samples file to read the merged cells in excel
The Excel table is as follows:
Write code:
# encoding: utf-8 # @author: Jeffrey # @file: demo01.py # @time: 2022/8/7 14:53 # @desc: Reading of merged cells in excel import xlrd3 work_book = xlrd3.open_workbook('test_data.xlsx') # Create a workbook object sheet_obj = work_book.sheet_by_name('Sheet1') # Create a table object print(sheet_obj.cell_value(1,2)) # Get the value of the cell (row, column), starting from 0, the actual serial number of the row and column is -1 print(sheet_obj.cell_value(7,3)) # Get the value of the cell print(sheet_obj.cell_value(1,0)) # Merged cells, the obtained value is empty print(sheet_obj.cell_value(7,0)) # Merged cells, the obtained value is empty # Contains four elements (starting row, ending row, actual column, ending column), including the front and not the end print(sheet_obj.merged_cells) # [(1, 5, 0, 1), (5, 9, 0, 1)] # Idea step 1: Determine whether a cell is a merged cell x = 3; y = 0 if x>=1 and x<5: if y>=0 and y<1: print('Merge cells') else: print('non-merged cells') else: print('Non-merged cells') # Idea step 2: How to write a for loop 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) # Idea step 3: Integrate ideas 1 and 2 into cells to determine whether they are merged cells 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('Merge cells') break else: print('non-merged cells') else: print('Non-merged cells') # Idea step 4: Let the values of the merged cells be equal to the value of the first merged cell, and the non-merged cells will be the original values 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) #Make the appeal code into a method 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)
Step 5. Create a new excel_file_utils.py file under common
Write code:
# 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())
Test execution results:
Step 6. Create a new test_data ordinary folder in the project root directory and put the test case files in it.
Step 7. Create a new testcase_data_utils.py file under common to convert the test case data into the format required by the framework.
Write code:
# encoding: utf-8 # @author: Jeffrey # @file: testcase_data_utils.py # @time: 2022/8/7 17:02 # @desc: Data format conversion: Convert test case data into the format required by the framework 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): '''Convert the test case data in excel to [{},{},{}] to {"":[],"":[], "":[]}''' test_case_data_dict = {} for row_data in self.test_data: test_case_data_dict.setdefault( row_data['test case number'],[] )\ .append(row_data) return test_case_data_dict def convert_testcase_data_to_list(self): '''Convert the data {"":[],"":[],"":[]} into ["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())
The purpose of the convert_testcase_data_to_dict() method is to separate the use case data by use case number and use case data
'''Convert the test case data in excel to [{},{},{}] to {"":[],"":[], "":[]}'''
View execution results:
The purpose of the convert_testcase_data_to_list() method is to separate the use case numbers and use case steps
'''Convert the data {"":[],"":[],"":[]} into ["case_id":key," ;case_step":value]'''
converted data
Framework 02, encapsulate requests requests, create a new requests_utils.py file under common
Step 1, encapsulate the get request
Write code:
# encoding: utf-8 # @author: Jeffrey # @file: requests_utils.py # @time: 2022/8/7 21:42 # @desc: Encapsulate requests 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['Request Address']) # Use json.loads() to convert the string into a dictionary url_params = json.loads(requests_info['Request Parameters (get)']) # Use the ternary operator to determine if the request header information is empty and return an empty dictionary {} header_info = (json.loads(requests_info['Request header information']) if requests_info['Request header information'] else {} ) response = self.session.get(url=url, params=url_params, headers=header_info) response.encoding = response.apparent_encoding # Prevent response text from being garbled print(response.text) if __name__ == '__main__': requests_info = {'Request header information': '', 'Request address': '/cgi-bin/token', 'Request parameters (get)': '{"grant_type":"client_credential",' '"appid":"wxf1856",' '"secret":"92a113bd423c99"}'} result = RequestsUtils().get(requests_info) print(result)
Step 2. Use all the returned response information as a dictionary with an additional code field.
Write code:
result = { 'code':0, 'response_code':response.status_code, 'response_info':response.reason, 'response_headers':response.headers, 'response_body':response.text }