Table of contents
Test report sending mail class
Design flow chart
This picture is some design ideas of my excel interface test framework.
First read the excel file to get the test information, and then use the unittest to test through the encapsulated requests method.
Among them, the parameters associated with the interface are searched and replaced by regular expressions. For this reason, I have specially opened up a global variable pool to manage various variables.
Finally, a test report is generated through HTMLrunner. If the execution fails, send a test report result email.
Excel and result preview
This is the test case organization chart of excel.
This is the HTML test report generated after running.
The excel report generated after running this time. You can see that I deliberately 预期正则
set the wrong value in , and then when the use case failed, I also marked the failed expected value.
The email received after the test failed
Well, the above are some simple introductions, and we start to get to the point.
Framework
First of all, to develop such an excel interface automation test project, we must have a clear design idea, so that we will understand what we want to do in the process of developing the framework.
directory/file | illustrate | Is it a python package |
---|---|---|
common | D | yes |
core | Core classes, encapsulation, requests etc. |
yes |
data | The excel file storage directory used for the test | |
logs | log directory | |
tests | Test case directory | yes |
utils | Tools, such as: log | yes |
config.py | configuration file | |
run.py | executable file |
Excel related
use case design
This time, the login interface is still used 智学网
. The login interface and login verification interface in Zhixue.com are used, and there are dependent parameters between these two interfaces.
configuration file
Create it in the root directory of the project config.py
, and put all the configuration information you can think of into this file for unified management.
#!/usr/bin/env python3
# coding=utf-8
import os
class CF:
"""配置文件"""
# 项目目录
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
# Excel首行配置
NUMBER = 0
NAME = 1
METHOD = 2
URL = 3
ROUTE = 4
HEADERS = 5
PARAMETER = 6 # 参数
EXPECTED_CODE = 7 # 预期响应码
EXPECTED_REGULAR = 8 # 预期正则
EXPECTED_VALUE = 9 # 预期结果值
SPEND_TIME = 10 # 响应时间
TEST_RESULTS = 11 # 测试结果
EXTRACT_VARIABLE = 12 # 提取变量
RESPONSE_TEXT = 13 # 响应文本
# 字体大小
FONT_SET = "微软雅黑"
FONT_SIZE = 16
# 颜色配置
COLOR_PASSED = "90EE90"
COLOR_FAILED = "FA8072"
# 邮箱配置
EMAIL_INFO = {
'username': '[email protected]',
'password': 2,
'smtp_host': 'smtp.qq.com',
'smtp_port': 465
}
# 收件人
ADDRESSEE = ['[email protected]']
if __name__ == '__main__':
print(CF.EXPECTED_CODE)
read/write excel
Create a new file in the common directory excelset.py
. In this file, we need to implement, read the use cases in excel, write the test results and draw the corresponding colors, and it takes a long time to write the test.
#!/usr/bin/env python
# coding=utf-8
import shutil
import openpyxl
from config import CF
from openpyxl.styles import Font
from openpyxl.styles import PatternFill
from common.variables import VariablePool
class ExcelSet:
"""Excel配置"""
def __init__(self):
shutil.copyfile(VariablePool.get('excel_input'), VariablePool.get('excel_output'))
self.path = VariablePool.get('excel_output')
self.wb = openpyxl.load_workbook(self.path)
self.table = self.wb.active
def get_cases(self, min_row=2):
"""获取用例"""
all_cases = []
for row in self.table.iter_rows(min_row=min_row):
all_cases.append((self.table.cell(min_row, CF.NAME + 1).value,
min_row, [cell.value for cell in row]))
min_row += 1
return all_cases
def write_color(self, row_n, col_n, color=CF.COLOR_FAILED):
"""写入颜色"""
cell = self.table.cell(row_n, col_n + 1)
fill = PatternFill("solid", fgColor=color)
cell.fill = fill
def write_results(self, row_n, col_n, value, color=True):
"""写入结果"""
cell = self.table.cell(row_n, col_n + 1)
cell.value = value
font = Font(name=CF.FONT_SET, size=CF.FONT_SIZE)
cell.font = font
if color:
if value.lower() in ("fail", 'failed'):
fill = PatternFill("solid", fgColor=CF.COLOR_FAILED)
cell.fill = fill
elif value.lower() in ("pass", "ok"):
fill = PatternFill("solid", fgColor=CF.COLOR_PASSED)
cell.fill = fill
self.wb.save(self.path)
excel_set = ExcelSet()
if __name__ == '__main__':
print(excel_set.get_cases())
log encapsulation
logger.py
The log is an essential thing in a project, and it can give feedback on problems in the first time.
#!/usr/bin/env python3
# coding=utf-8
import os
import logging
from config import CF
from datetime import datetime
class Logger:
def __init__(self):
self.logger = logging.getLogger()
if not self.logger.handlers:
self.logger.setLevel(logging.DEBUG)
# 创建一个handler,用于写入日志文件
fh = logging.FileHandler(self.log_path, encoding='utf-8')
fh.setLevel(logging.DEBUG)
# 创建一个handler,用于输出到控制台
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
# 定义handler的输出格式
formatter = logging.Formatter(self.fmt)
fh.setFormatter(formatter)
ch.setFormatter(formatter)
# 给logger添加handler
self.logger.addHandler(fh)
self.logger.addHandler(ch)
@property
def log_path(self):
logs_path = os.path.join(CF.BASE_DIR, 'logs')
if not os.path.exists(logs_path):
os.makedirs(logs_path)
now_month = datetime.now().strftime("%Y%m")
return os.path.join(logs_path, '{}.log'.format(now_month))
@property
def fmt(self):
return '%(levelname)s %(asctime)s %(filename)s:%(lineno)d %(message)s'
log = Logger().logger
if __name__ == '__main__':
log.info("你好")
regular operation
regular.py
It plays a decisive role in the extraction and passing of interface associated parameters.
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import re
from utils.logger import log
from common.variables import VariablePool
from core.serialize import is_json_str
class Regular:
"""正则类"""
def __init__(self):
self.reg = re.compile
def finds(self, string):
return self.reg(r'\{
{(.*?)}\}').findall(string)
def subs(self, keys, string):
result = None
log.info("提取变量:{}".format(keys))
for i in keys:
if VariablePool.has(i):
log.info("替换变量:{}".format(i))
comment = self.reg(r"\{
{%s}}" % i)
result = comment.sub(VariablePool.get(i), string)
log.info("替换结果:{}".format(result))
return result
def find_res(self, exp, string):
"""在结果中查找"""
if is_json_str(string):
return self.reg(r'\"%s":"(.*?)"' % exp).findall(string)[0]
else:
return self.reg(r'%s' % exp).findall(string)[0]
if __name__ == '__main__':
a = "{'data': {'loginName': 18291900215, 'password': '{
{dd636482aca022}}', 'code': None, 'description': 'encrypt'}}"
print(Regular().finds(a))
core operation
Define variable pool
variables.py
The global variable pool is here, is it very simple, but the effect is indeed huge.
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
class VariablePool:
"""全局变量池"""
@staticmethod
def get(name):
"""获取变量"""
return getattr(VariablePool, name)
@staticmethod
def set(name, value):
"""设置变量"""
setattr(VariablePool, name, value)
@staticmethod
def has(name):
return hasattr(VariablePool, name)
if __name__ == '__main__':
VariablePool.set('name', 'wxhou')
print(VariablePool.get('name'))
Package requests
request.py
The most core part is the secondary encapsulation of the python requests library. It is used to realize the request of the interface and obtain the returned result.
#!/usr/bin/env python
# coding=utf-8
import urllib3
import requests
from config import CF
from utils.logger import log
from common.regular import Regular
from common.setResult import replace_param
from core.serialize import deserialization
from requests.exceptions import RequestException
from common.variables import VariablePool
urllib3.disable_warnings()
class HttpRequest:
"""二次封装requests方法"""
http_method_names = 'get', 'post', 'put', 'delete', 'patch', 'head', 'options'
def __init__(self):
self.r = requests.session()
self.reg = Regular()
def send_request(self, case, **kwargs):
"""发送请求
:param case: 测试用例
:param kwargs: 其他参数
:return: request响应
"""
if case[CF.URL]:
VariablePool.set('url', case[CF.URL])
if case[CF.HEADERS]:
VariablePool.set('headers', deserialization(case[CF.HEADERS]))
method = case[CF.METHOD].upper()
url = VariablePool.get('url') + case[CF.ROUTE]
self.r.headers = VariablePool.get('headers')
params = replace_param(case)
if params: kwargs = params
try:
log.info("Request Url: {}".format(url))
log.info("Request Method: {}".format(method))
log.info("Request Data: {}".format(kwargs))
def dispatch(method, *args, **kwargs):
if method in self.http_method_names:
handler = getattr(self.r, method)
return handler(*args, **kwargs)
else:
raise AttributeError('request method is ERROR!')
response = dispatch(method.lower(), url, **kwargs)
log.info(response)
log.info("Response Data: {}".format(response.text))
return response
except RequestException as e:
log.exception(format(e))
except Exception as e:
raise e
Serialization and deserialization
serialize.py
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import json
from json.decoder import JSONDecodeError
def deserialization(content: json):
"""
反序列化
json对象 -> python数据类型
"""
return json.loads(content)
def serialization(content, ensure_ascii=True):
"""
序列化
python数据类型 -> json对象
"""
return json.dumps(content, ensure_ascii=ensure_ascii)
def is_json_str(string):
"""判断是否是json格式字符串"""
if isinstance(string, str):
try:
json.loads(string)
return True
except JSONDecodeError:
return False
return False
if __name__ == '__main__':
a = "{'data': {'loginName': 18291900215, 'password': 'dd636482aca022', 'code': None, 'description': 'encrypt'}}"
print(is_json_str(a))
test result
checkResult.py
In this file, we will do the expected validation of the results returned by the tests.
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import re
from config import CF
from utils.logger import log
from requests import Response
from common.excelset import excel_set
def check_result(r: Response, number, case):
"""获取结果"""
results = []
excel_set.write_results(number, CF.SPEND_TIME, r.elapsed.total_seconds(), color=False)
if case[CF.EXPECTED_CODE]:
res = int(case[CF.EXPECTED_CODE]) == r.status_code
results.append(res)
if not res: excel_set.write_color(number, CF.EXPECTED_CODE)
log.info(f"预期响应码:{case[CF.EXPECTED_CODE]},实际响应码:{r.status_code}")
if case[CF.EXPECTED_VALUE]:
res = case[CF.EXPECTED_VALUE] in r.text
results.append(res)
if not res: excel_set.write_color(number, CF.EXPECTED_VALUE)
log.info(f"预期响应值:{case[CF.EXPECTED_VALUE]},实际响应值:{r.text}")
if case[CF.EXPECTED_REGULAR]:
res = r'%s' % case[CF.EXPECTED_REGULAR]
ref = re.findall(res, r.text)
results.append(ref)
if not ref: excel_set.write_color(number, CF.EXPECTED_REGULAR)
log.info(f"预期正则:{res},响应{ref}")
if all(results):
excel_set.write_results(number, CF.TEST_RESULTS, 'Pass')
log.info(f"用例【{case[CF.NAME]}】测试成功!")
else:
excel_set.write_results(number, CF.TEST_RESULTS, 'Failed')
assert all(results), f"用例【{case[CF.NUMBER]}{case[CF.NAME]}】测试失败:{results}"
Setting parameters
setResult.py
In this file, we have realized the extraction of the return value of the interface, and realized the function of the interface passing parameters.
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from requests import Response
from utils.logger import log
from common.regular import Regular
from common.excelset import excel_set
from common.variables import VariablePool
from core.serialize import is_json_str, deserialization
from config import CF
reg = Regular()
def get_var_result(r: Response, number, case):
"""替换变量"""
if case[CF.EXTRACT_VARIABLE]:
for i in case[CF.EXTRACT_VARIABLE].split(','):
result = reg.find_res(i, r.text)
VariablePool.set(i, result)
log.info(f"提取变量{i}={result}")
if not VariablePool.get(i):
excel_set.write_results(number, CF.EXTRACT_VARIABLE, f"提变量{i}失败")
excel_set.write_results(number, CF.RESPONSE_TEXT,
f"ResponseCode:{r.status_code}\nResponseText:{r.text}")
def replace_param(case):
"""传入参数"""
if case[CF.PARAMETER]:
if is_json_str(case[CF.PARAMETER]):
is_extract = reg.finds(case[CF.PARAMETER])
if is_extract:
return deserialization(reg.subs(is_extract, case[CF.PARAMETER]))
return deserialization(case[CF.PARAMETER])
test operation
test_api.py
We use unittest for testing. In the preconditions and postconditions, we initialize and close the session on the encapsulated HttpRequest method.
Use the expend method in the parameterized library to perform parameterized read execution for use cases in excel.
#!/usr/bin/env python
# coding=utf-8
import unittest
from parameterized import parameterized
from common.excelset import excel_set
from core.request import HttpRequest
from common.checkResult import check_result
from common.setResult import get_var_result
class TestApi(unittest.TestCase):
"""测试接口"""
@classmethod
def setUpClass(cls) -> None:
cls.req = HttpRequest()
@classmethod
def tearDownClass(cls) -> None:
cls.req.r.close()
@parameterized.expand(excel_set.get_cases())
def test_api(self, name, number, case):
"""
测试excel接口用例
"""
r = self.req.send_request(case)
get_var_result(r, number, case)
check_result(r, number, case)
if __name__ == '__main__':
unittest.main(verbosity=2)
Test report sending mail class
run.py
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import os
import platform
import argparse
import unittest
from common.variables import VariablePool
from utils.send_mail import send_report_mail
from utils.HTMLTestRunner import HTMLTestRunner
def running(path):
"""运行"""
test_case = unittest.defaultTestLoader.discover('tests', 'test*.py')
with open(path, 'wb') as fp:
runner = HTMLTestRunner(stream=fp,
title='Excel接口测试',
description="用例执行情况",
verbosity=2)
result = runner.run(test_case)
if result.failure_count:
send_report_mail(path)
def file_path(arg):
"""获取输入的文件路径"""
if 'Windows' in platform.platform():
_dir = os.popen('chdir').read().strip()
else:
_dir = os.popen('pwd').read().strip()
if _dir in arg:
return arg
return os.path.join(_dir, arg)
def main():
"""主函数"""
parser = argparse.ArgumentParser(description="运行Excel接口测试")
parser.add_argument('-i', type=str, help='原始文件')
parser.add_argument('-o', type=str, default='report.xlsx', help="输出文件")
parser.add_argument('-html', type=str, default='report.html', help="报告文件")
args = parser.parse_args()
VariablePool.set('excel_input', file_path(args.i))
VariablePool.set('excel_output', file_path(args.o))
VariablePool.set('report_path', file_path(args.html))
running(VariablePool.get('report_path'))
if __name__ == '__main__':
main()
run
It is worth noting that when running the test, you must close the office and open the excel file.
In the final file, I used argparse for command line management, which means that we can test through the command line without caring about the directory where excel is stored.
python run.py -i data\usercase.xlsx
Enter the following command to execute it.
INFO 2020-07-30 22:07:52,713 request.py:40 Request Url: https://www.zhixue.com/weakPwdLogin/?from=web_login
INFO 2020-07-30 22:07:52,714 request.py:41 Request Method: POST
INFO 2020-07-30 22:07:52,715 request.py:42 Request Data: {'data': {'loginName': 18291900215, 'password': 'dd636482aca022', 'code': None, 'descriptio
n': 'encrypt'}}
INFO 2020-07-30 22:08:17,204 request.py:55 <Response [200]>
INFO 2020-07-30 22:08:17,204 request.py:56 Response Data: {"data":"1500000100070008427","result":"success"}
INFO 2020-07-30 22:08:17,207 setResult.py:20 提取变量data=1500000100070008427
INFO 2020-07-30 22:08:17,307 checkResult.py:18 预期响应码:200,实际响应码:200
INFO 2020-07-30 22:08:17,308 checkResult.py:23 预期响应值:"result":"success",实际响应值:{"data":"1500000100070008427","result":"success"}
INFO 2020-07-30 22:08:17,310 checkResult.py:29 预期正则:[\d]{16},响应['1500000100070008']
INFO 2020-07-30 22:08:17,356 checkResult.py:32 用例【登录】测试成功!
ok test_api_0__ (test_api.TestApi)
INFO 2020-07-30 22:08:17,358 regular.py:20 提取变量:['data']
INFO 2020-07-30 22:08:17,359 regular.py:23 替换变量:data
INFO 2020-07-30 22:08:17,361 regular.py:26 替换结果:{"data": {"userId": "1500000100070008427"}}
INFO 2020-07-30 22:08:17,363 request.py:40 Request Url: https://www.zhixue.com/loginSuccess/
INFO 2020-07-30 22:08:17,366 request.py:41 Request Method: POST
INFO 2020-07-30 22:08:17,367 request.py:42 Request Data: {'data': {'userId': '1500000100070008427'}}
INFO 2020-07-30 22:08:20,850 request.py:55 <Response [200]>
INFO 2020-07-30 22:08:20,851 request.py:56 Response Data: {"result":"success"}
INFO 2020-07-30 22:08:20,932 checkResult.py:18 预期响应码:200,实际响应码:200
INFO 2020-07-30 22:08:20,933 checkResult.py:23 预期响应值:"result":"success",实际响应值:{"result":"success"}
INFO 2020-07-30 22:08:20,935 checkResult.py:29 预期正则:11,响应[]
F test_api_1__ (test_api.TestApi)
Time Elapsed: 0:00:28.281434
测试结果邮件发送成功!
enforce rules
(venv) C:\Users\hoou\PycharmProjects\httptest-excel>python run.py -h
usage: run.py [-h] [-i I] [-o O] [-html HTML]
运行Excel接口测试
optional arguments:
-h, --help show this help message and exit
-i I 原始文件
-o O 输出文件
-html HTML 报告文件
Enter at the command linepython run.py excel路径 新excel路径 报告路径
If you do not enter 新excel路径
and 报告路径
, run.py
two report.xlsx and report.html will be generated in the directory.
The excel test framework of this article is complete.