本文参考:https://my.oschina.net/u/3041656/blog/820023
公司在做中台管理系统,考虑到数据测试比较多,就打算做接口测试。请教过朋友,他们主要使用jmeter做接口测试,本来我也想使用jmeter做测试,但考虑到jmeter自动化需要懂java才能做二次封装,坦白了我不会java,还没学哈哈哈,然后就用了我唯一会的python找了一些资料参考封装了一个,其实也不算封装,东凑凑西凑凑就出来,今天刚写好,发出来大家给点意见,毕竟集思广益嘛
项目目录
不废话,一一介绍各个py是干啥的
common.py文件里面封装一些公共方法,读xlsx文件和xml文件以及读取yaml文件
```python
import os
import xml.etree.ElementTree as ET
import yaml
from xlrd import open_workbook
class Common:
def __init__(self):
""" 指定文件的路径 """
self.rootPath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def get_xls(self, xls_name, sheet_name):
"""
获取xls文件
:param xls_name: 文件名
:param sheet_name:
:return:
"""
cls = []
# get xls file's path
xlsPath = os.path.join(self.rootPath, "testFile", 'case', xls_name)
# open xls file
file = open_workbook(xlsPath)
# get sheet by name
sheet = file.sheet_by_name(sheet_name)
# get one sheet's rows
nrows = sheet.nrows
for i in range(nrows):
if sheet.row_values(i)[0] != u'case_name':
cls.append(sheet.row_values(i))
return cls
def get_yaml(self, funcname, yaml_file):
""" 读取yaml里面的数据"""
with open(os.path.join(self.rootPath + "/Data/", yaml_file), "r", encoding='utf8') as f:
data = yaml.load(f, Loader=yaml.Loader)
tmpdata = data[funcname]
return tmpdata
def get_xml_sql(self, filename, sql_id):
"""
get sql xml
:return:
"""
sql_path = os.path.join(self.rootPath, "testFile/xmlSql", filename)
tree = ET.parse(sql_path)
root = tree.getroot()
sql = {}
for data in root.findall('sql'):
sql_id_xml = data.get("id")
if sql_id_xml == sql_id:
sql[sql_id_xml] = data.text
sql = sql.get(sql_id)
return sql
common = Common()
config读取配置文件,我的配置文件写在yaml文件中管理,放在Data文件夹中
```python
# coding=utf-8
"""
读取全局配置
"""
from Common.common import common
# 配置环境
version = common.get_yaml("version", "config.yaml")
"""
配置数据
"""
ApiUrl = common.get_yaml(version, "config.yaml")['ApiUrl']
VerificationCode = common.get_yaml(version, "config.yaml")['VerificationCode']
config.yaml
version: test
test:
ApiUrl: http://localhost:3000/
VerificationCode: '8888'
database:
host: "192.168.1.90"
username: "root"
password: ""
port: 2019
configDB连接数据库用的
import pymysql
from Common import Log
from Common.config import version
from Common.common import common
class MyDB:
def __init__(self):
self.logger = Log.MyLog()
database = common.get_yaml(version, "config.yaml")['database']
self.host = database['host']
self.port = database['port']
self.user = database['username']
self.password = database['password']
self.db = None
self.cursor = None
def connect_db(self):
"""
connect to database
:return:
"""
try:
# connect to DB
self.db = pymysql.connect(host=self.host, port=self.port, user=self.user, passwd=self.password, charset='utf8')
# create cursor
self.cursor = self.db.cursor()
print("Connect DB successfully!")
self.logger.info("Connect DB successfully!")
except ConnectionError as ex:
self.logger.error(str(ex))
print(ex)
def execute_sql(self, sql):
"""
execute sql
:param sql:
:return:
"""
# executing sql
self.cursor.execute(sql)
return self.cursor
@staticmethod
def get_all(cursor):
"""
get all result after execute sql
:param cursor:
:return:
"""
value = cursor.fetchall()
return value
@staticmethod
def get_one(cursor):
"""
get one result after execute sql
:param cursor:
:return:
"""
value = cursor.fetchone()
return value
def close_db(self):
"""
close database
:return:
"""
self.db.close()
print("Database closed!")
self.logger.info("Database closed!")
MyDB = MyDB()
HttpRequests.py发送接口请求,先设置接口地址,请求方式,请求参数,最后调用send_requests发送请求
import json
import requests
from Common.Log import MyLog
from Common.config import ApiUrl
class HttpRequests(object):
"""
eg: request = HttpRequests()
response = request(method, url, data)
or
response = request.send_request(method, url, data)
print(response.text)()
self.headers = {}
self.params = {}
"""
def __init__(self):
self.session = requests.Session()
self.data = None
self.params = {}
self.url = None
self.method = None
self.files = {}
self.state = 0
def set_url(self, url_path):
"""
set url
:param: interface url
:return:
"""
self.url = ApiUrl + url_path
def set_method(self, method):
"""
set method
:param method:
:return:
"""
self.method = method
def set_headers(self, header):
"""
set headers
:param header:
:return:
"""
self.headers = header
def set_params(self, param):
"""
set params
:param param:
:return:
"""
self.params = param
def set_data(self, data):
"""
set data
:param data:
:return:
"""
self.data = str(data)
def set_files(self, filename):
"""
set upload files
:param filename:
:return:
"""
if filename != '':
file_path = 'F:/AppTest/Test/interfaceTest/testFile/img/' + filename
self.files = {'file': open(file_path, 'rb')}
if filename == '' or filename is None:
self.state = 1
def send_request(self, params_type='json', **kwargs):
"""
发送接口请求
:param params_type: 默认参数类型json格式
:param kwargs:
:return:
"""
method = self.method.upper()
params_type = params_type.upper()
if isinstance(self.data, str):
try:
self.data = json.loads(self.data)
except Exception as e:
self.data = eval(self.data)
MyLog.info(e)
if 'GET' == method:
response = self.session.request(method=method, url=self.url, params=self.params, **kwargs)
elif 'POST' == method:
if params_type == 'FORM': # 发送表单数据,使用data参数传递
response = self.session.request(method=method, url=self.url, data=self.data, **kwargs)
elif params_type == 'JSON': # 如果接口支持application/json类型,则使用json参数传递
response = self.session.request(method=method, url=self.url, json=self.data, **kwargs)
else: # 如果接口需要传递其他类型的数据比如 上传文件,调用下面的请求方法
response = self.session.request(method=method, url=self.url, **kwargs)
# 如果请求方式非 get 和post 会报错,当然你也可以继续添加其他的请求方法
else:
raise ValueError('request method "{}" error ! please check'.format(method))
return response
def close_session(self):
self.session.close()
try:
del self.session.cookies['JSESSIONID']
except Exception as e:
MyLog.info(e)
raise e
HttpRequests = HttpRequests()
最后common文件只剩下Log日志封装
# -*- coding: utf-8 -*-
"""
封装log方法
"""
import logging
import os
import time
LEVELS = {
'debug': logging.DEBUG,
'info': logging.INFO,
'warning': logging.WARNING,
'error': logging.ERROR,
'critical': logging.CRITICAL
}
logger = logging.getLogger()
level = 'default'
def create_file(filename):
path = filename[0:filename.rfind('/')]
if not os.path.isdir(path):
os.makedirs(path)
if not os.path.isfile(filename):
fd = open(filename, mode='w', encoding='utf-8')
fd.close()
else:
pass
def set_handler(levels):
if levels == 'error':
logger.addHandler(MyLog.err_handler)
logger.addHandler(MyLog.handler)
def remove_handler(levels):
if levels == 'error':
logger.removeHandler(MyLog.err_handler)
logger.removeHandler(MyLog.handler)
def get_current_time():
return time.strftime(MyLog.date, time.localtime(time.time()))
class MyLog:
path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
log_file = path+'/Log/log.log'
err_file = path+'/Log/err.log'
logger.setLevel(LEVELS.get(level, logging.NOTSET))
create_file(log_file)
create_file(err_file)
date = '%Y-%m-%d %H:%M:%S'
handler = logging.FileHandler(log_file, encoding='utf-8')
err_handler = logging.FileHandler(err_file, encoding='utf-8')
@staticmethod
def debug(log_meg):
set_handler('debug')
log_meg = str(log_meg)
logger.debug("[DEBUG " + get_current_time() + "]" + log_meg)
remove_handler('debug')
@staticmethod
def info(log_meg):
set_handler('info')
log_meg = str(log_meg)
logger.info("[INFO " + get_current_time() + "]" + log_meg)
remove_handler('info')
@staticmethod
def warning(log_meg):
set_handler('warning')
log_meg = str(log_meg)
logger.warning("[WARNING " + get_current_time() + "]" + log_meg)
remove_handler('warning')
@staticmethod
def error(log_meg):
set_handler('error')
log_meg = str(log_meg)
logger.error("[ERROR " + get_current_time() + "]" + log_meg)
remove_handler('error')
@staticmethod
def critical(log_meg):
set_handler('critical')
log_meg = str(log_meg)
logger.error("[CRITICAL " + get_current_time() + "]" + log_meg)
remove_handler('critical')
if __name__ == "__main__":
MyLog.debug("This is debug message")
MyLog.info("This is info message")
MyLog.warning("This is warning message")
MyLog.error("This is error")
MyLog.critical("This is critical message")
run.py运行文件, 运行不起来可能是你没安装allure报告,allure安装包: https://pan.baidu.com/s/19eohM2P6VNWCaJETIb93Aw, 提取码: 1xcc
import os
import subprocess
import time
import pytest
from Common.Log import MyLog
PATH = os.path.split(os.path.realpath(__file__))[0]
xml_report_path = PATH + "/report/xml"
html_report_path = PATH + "/report/html"
tm = time.strftime("%Y-%m-%d-%H:%M:%S", time.localtime(time.time()))
def invoke(md):
output, errors = subprocess.Popen(md, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
o = output.decode("utf-8")
return o
if __name__ == '__main__':
MyLog.info("======================" + tm + "======================")
args = ['-s', '-q', '--alluredir', xml_report_path]
pytest.main(args)
cmd = 'allure generate %s -o %s --clean' % (xml_report_path, html_report_path)
invoke(cmd)
testFile里面放xlsx数据,看一下文件里面内容,注意纯数字需要把单元格式修改成文本,不然读出来会在数字后增加2.0, 还有读不出来可能是你的插件版本不对,按requirements.txt文件里的版本来就对了
**最后重点讲一下测试用例, setup_class和teardown_class在这个测试类运行之前连接数据库和运行完成关闭数据库连接,具体的使用方法可以查资料,pytest.mark.parametrize就是参数化,参数名要与xlsx文件中对应哦,pytest.fixture这个装饰器我也是刚了解不久,目前用来装饰sql查询,然后可以在测试用例中直接把函数名当参数传进去,是不是很方便,哈哈 **
import pytest
from Common.configDB import MyDB
from Common.HttpRequests import HttpRequests
from Common.Log import MyLog
from Common.config import *
login_xls = common.get_xls("userCase.xlsx", "login")
class TestLogin:
def setup_class(self):
MyDB.connect_db()
def teardown_class(self):
MyDB.close_db()
@pytest.mark.parametrize("case_name, url_path, method, phone, password, result, code, msg", login_xls)
def test_login(self, case_name, url_path, method, phone, password, result, code, msg, database_data):
# 设置url
HttpRequests.set_url(url_path)
# 设置请求方式
HttpRequests.set_method(method)
# 设置请求参数
data = {"code": VerificationCode, "key": "", "loginphone": phone, "password": password}
HttpRequests.set_data(data)
# 发送请求
self.response = HttpRequests.send_request()
# 检查结果
self.check_result(msg, code, result, database_data)
MyLog.info("{}测试结束".format(case_name))
def check_result(self, msg, code, result, database_data):
response_json = self.response.json()
print(type(result))
if result == '0':
assert response_json['msg'] == msg
assert response_json['code'] == int(code)
assert response_json['data'] == database_data
elif result == '1':
assert response_json['msg'] == msg
assert response_json['code'] == int(code)
@pytest.fixture()
def database_data(self):
sql = common.get_xml_sql('login.xml', 'queryUser')
data = MyDB.execute_sql(sql).fetchall()
return data
写到最后有一个遗留问题,数据库查询出来的数据和接口返回的数据做对比很麻烦,我现在只写了一条数据对比,实际项目中可能有很多数据都需要对比,一一对比感觉很麻烦,有没有哪位大神知道怎么写简单些
补充一下运行环境
allure-pytest==2.8.36
allure-python-commons==2.8.36
atomicwrites==1.4.0
attrs==20.3.0
certifi==2020.12.5
chardet==4.0.0
colorama==0.4.4
idna==2.10
importlib-metadata==3.7.0
iniconfig==1.1.1
numpy==1.19.5
packaging==20.9
pluggy==0.13.1
py==1.10.0
PyMySQL==0.9.3
pyparsing==2.4.7
pytest==6.2.2
pytest-repeat==0.9.1
pytest-rerunfailures==9.1.1
PyYAML==5.4.1
requests==2.25.1
six==1.15.0
toml==0.10.2
typing-extensions==3.7.4.3
urllib3==1.26.3
xlrd==1.2.0
zipp==3.4.1