结构介绍

common 公共代码文件夹

casedata.py

import pandas
from common.log import log
def read_cases(xlsfile, prefixs, dict_indexs, columns=None, col_type=None): #读含多个参数列的用例的函数
try:
xlsfile='../excelcase/'+xlsfile
data=pandas.read_excel(xlsfile, usecols=columns, dtype=col_type, keep_default_na=False)
if type(prefixs) in(list,tuple) and type(dict_indexs) in(list,tuple):
prefixs_and_indexs=zip(prefixs,dict_indexs)
elif type(prefixs)==str and type(dict_indexs)==int:
prefixs_and_indexs=((prefixs,dict_indexs),) #二维元组
else:
exit('prefixs的类型只能是列表或元组或字符串,dict_indexs的类型只能是列表或元组或整数')
for prefix, dict_index in prefixs_and_indexs:
cols=data.filter(regex='^'+prefix, axis=1) #过滤出前缀开头的列
col_names=cols.columns.values #以前缀prefix开头的列名
col_names_new=[i[len(prefix):] for i in col_names]#真正的参数名
col_values=cols.values.tolist() #前缀开头的多行数据列表
cols=[] #新的存字典的列表
for value in col_values:
col_dict=dict(zip(col_names_new, value))
cols.append(col_dict)
data.drop(col_names, axis=1, inplace=True)#drop删列存回data
data.insert(dict_index, prefix, cols)
cases=data.values.tolist()
log().info(f'读测试用例文件{xlsfile}成功')
return cases
except BaseException as e:
log().error(f'读测试用例文件{xlsfile}出错==错误类型:{type(e).__name__},错误内容:{e}')
exit()
def read_dict_cases(xlsfile,columns=None): #读带{:}参数的用例的函数
data=pandas.read_excel(xlsfile, usecols=columns)
cases=data.values.tolist()
for case in cases:
for i in range(len(case)):
if str(case[i]).startswith('{') and str(case[i]).endswith('}') and ':' in str(case[i]):
case[i]=eval(case[i])
return cases
if __name__=='__main__':
read_cases('login.xlsx','arg_',4,col_type={'arg_password':str})
# read_cases('signup.xlsx', ['arg_','expect_'], [4,5], col_type={'arg_password': str,'arg_confirm':str})

conf.py

import configparser
from common.log import log
class Conf: #配置文件类
def __init__(self): #构造方法
self.read_entry()
self.read_server_conf()
self.read_db_conf()
def read_entry(self): #读入口配置文件方法
try:
conf = configparser.ConfigParser()
conf.read('../conf/entry.ini')
self.__which_server = conf.get('entry', 'which_server')
self.__which_db = conf.get('entry', 'which_db') #__表示禁止在类外使用__which_server和__which_db
log().info(f'读入口配置文件../conf/entry.ini成功==接口服务器入口名:{self.__which_server},数据库服务器入口名:{self.__which_db}')
except BaseException as e:
log().error(f'读入口配置文件../conf/entry.ini出错==错误类型:{type(e).__name__},错误内容:{e}')
exit() #退出python
def read_server_conf(self): #读接口服务器配置文件方法
try:
conf = configparser.ConfigParser()
conf.read('../conf/server.conf', encoding='utf-8')
which_server = self.__which_server
ip = conf.get(which_server, 'ip')
port = conf.get(which_server, 'port')
self.host = 'http://%s:%s' % (ip, port) #接口地址url的一部分
log().info(f'读接口服务器配置文件../conf/server.conf成功==接口服务器地址:{self.host}')
except BaseException as e:
log().error(f'读接口服务器配置文件../conf/server.conf出错==错误类型:{type(e).__name__},错误内容:{e}')
exit()
def read_db_conf(self): #读数据库配置文件方法
try:
conf = configparser.ConfigParser()
conf.read('../conf/db.conf')
which_db = self.__which_db
host = conf.get(which_db, 'host')
db = conf.get(which_db, 'db')
user = conf.get(which_db, 'user')
passwd = conf.get(which_db, 'passwd')
self.dbinfo = {'host': host, 'db': db, 'user': user, 'passwd': passwd}
log().info(f'读数据库服务器配置文件../conf/server.conf成功==数据库信息:{self.dbinfo}')
except BaseException as e:
log().error(f'读数据库服务器配置文件../conf/server.conf出错==错误类型:{type(e).__name__},错误内容:{e}')
exit()
def update_entry(self): #修改入口名方法
try:
is_update = input('是否修改入口名(y/Y表示是,其他表示否):')
if is_update in {'y', 'Y'}:
new_server = input('新接口服务器入口名:')
new_db = input('新数据库服务器入口名:')
if {new_server, new_db}.issubset({'debug', 'formal', 'smoke', 'regress'}):
old_server, old_db = self.__which_server, self.__which_db
if new_server != old_server and new_db != old_db:
conf = configparser.ConfigParser()
conf.read('../conf/entry.ini')
conf.set('entry', 'which_server', new_server)
conf.set('entry', 'which_db', new_db)
file = open('../conf/entry.ini', 'w') # w不能省略
conf.write(file)
file.close()
log().info('成功将入口名(%s,%s)修改为(%s,%s)' % (old_server, old_db, new_server, new_db))
self.__init__() #可以主动调用构造
# print(self.host,self.dbinfo) #调试
else:
log().info('入口名(%s,%s)未发生改变' % (old_server, old_db))
else:
exit('入口名错误,只能输入debug、smoke、formal、regress之一') #exit也会抛出异常
else:
log().info('取消修改入口名')
except BaseException as e:
log().error(f'修改../conf/entry.ini出错==错误类型:{type(e).__name__},错误内容:{e}')
exit()
if __name__=='__main__':
a=Conf()
# a.update_entry()

db.py

import os, pymysql
from common.conf import Conf
from common.log import log
def read_sqls(*sqlfiles): # 读sql语句文件方法
try:
if not sqlfiles: # 表示sqlfiles为空
sqlfiles = tuple([i for i in os.listdir('../initsqls') if i.endswith('.sql')]) #不要直接写(),否则结果是对象
# print(sqlfiles) #调试
sqls = [] # 存sql语句的列表
for file in sqlfiles: # file为文件名
data = open('../initsqls/'+file, 'r') # data表示文件中所有行
for sql in data: # sql是一行
if sql.strip() and not sql.startswith('--'):
sqls.append(sql.strip())
log().info(f'读sql语句文件{sqlfiles}成功')
return sqls
except BaseException as e:
log().error(f'读sql语句文件{sqlfiles}出错==错误类型:{type(e).__name__},错误内容:{e}')
exit()
class DB:
def __init__(self): #构造方法:连接数据库
dbinfo = Conf().dbinfo
try:
self.__conn = pymysql.connect(**dbinfo) #私有成员变量
self.__cursor = self.__conn.cursor()
log().info(f'连接数据库{dbinfo}成功')
except BaseException as e:
log().error(f'连接数据库{dbinfo}出错==错误类型:{type(e).__name__},错误内容:{e}')
exit()
def init_db(self, *sqlfiles): #初始化数据库方法
conn, cursor = self.__conn, self.__cursor
sqls = read_sqls(*sqlfiles)
try:
for sql in sqls:
cursor.execute(sql)
conn.commit()
conn.close()
log().info(f'执行造数代码,初始化数据库成功')
except BaseException as e:
log().error(f'执行造数代码,初始化数据库出错==错误类型:{type(e).__name__},错误内容:{e}')
exit()
def check_db(self,case_info, args, check_sql, db_expect_rows): #验库方法
conn, cursor = self.__conn,self.__cursor
try:
cursor.execute(check_sql)
db_actual_rows = cursor.fetchone()[0]
if db_actual_rows == db_expect_rows:
log().info(f'{case_info}==落库检查通过')
return True, '' #测试通过时,没有断言失败消息
else:
msg=f'{case_info}==落库检查失败==检查的数据:{args}==预期行数:{db_expect_rows}==实际行数:{db_actual_rows}'
log().warning(msg)
return False, msg
except BaseException as e:
log().error(f'{case_info}==落库检查出错==检查的数据:{args}==预期行数:{db_expect_rows}==错误类型:{type(e).__name__},错误内容:{e}')
exit()
if __name__=='__main__':
# read_sqls()
# read_sqls('login.sql')
# read_sqls('log.sql')
a=DB()
# a.init_db()
# a.init_db('login.sql')
# a.init_db('login.sql', 'signup.sql')
a.check_db('总行数',{'a':23},'select count(*) from user',6)
# q=tuple(i*i for i in range(10)) #可以用元组推导式
# print(q)

log.py

import logging #1、导入模块logging
def log():
logger=logging.getLogger() #2、创建(获得)日志对象(只创建一次对象)
if not logger.handlers: #如果logger对象中不存在处理器
logger.setLevel(logging.INFO) #3、设置日志(最低输出)等级
formater=logging.Formatter('%(asctime)s %(levelname)s [%(message)s] %(filename)s:%(lineno)s') #4、设置日志输出格式
console=logging.StreamHandler() #5、创建日志流处理器(输出到控制台)
console.setFormatter(formater) #6、设置日志流处理器的输出格式
logger.addHandler(console) #7、日志流处理器增加到日志对象
console.close() #8、关闭日志流处理器(日志对象负责输出日志)
file=logging.FileHandler('../log/exam.log', encoding='utf-8') #9、创建日志文件处理器,可省参数mode表示写入模式,默认是追加
file.setFormatter(formater) #10、设置日志文件处理器的输出格式
logger.addHandler(file) #11、日志文件处理器增加到日志对象
file.close() #12、关闭日志文件处理器
return logger
#调试:输出日志
if __name__=='__main__':
log().info('成功的消息')
log().warning('警告信息')
log().error('错误信息')
# logging.info('成功信息')
# logging.warning('警告')
# logging.error('错误')

senddata.py

import requests
from common.log import log
def send_request(method, url, args): #发送请求
try:
send="requests.%s('%s',%s)"%(method, url, args)
# print(send) #调试
res=eval(send)
# print(res.headers['Content-Type']) #响应/返回值类型
if 'text' in res.headers['Content-Type']:
res_type='text' #返回值类型
actual=res.text #实际结果,类型:text/html; charset=gbk
elif 'json' in res.headers['Content-Type']:
res_type='json'
actual=res.json() #实际结果,类型:application/json;charset=utf8
else:
pass
log().info(f'使用{method}方法将参数{args}发送给接口地址{url}成功')
return res_type, actual
except BaseException as e:
log().error(f'使用{method}方法将参数{args}发送给接口地址{url}出错==错误类型:{type(e).__name__},错误内容:{e}')
exit()
#比对响应结果函数
def check(case_info, res_type, actual, expect):
try:
passed=False #预置变量,表示测试不通过
if res_type=='text':
if expect in actual:
passed=True
elif res_type=='json':
if expect==actual:
passed=True
else: pass
if passed:
msg='' #测试通过时,断言失败消息为空
log().info(f'{case_info}==比对响应结果通过')
else:
msg=f'{case_info}==比对响应结果失败==预期:{expect}==实际:{actual}' #给将来的assert断言使用的断言失败消息
log().warning(msg)
return passed, msg
except BaseException as e:
log().error(f'{case_info}==比对响应结果出错==错误类型:{type(e).__name__},错误内容:{e}')
exit()
if __name__=='__main__':
# send_request('post','http://192.168.237.128/exam/login/',{'username':'admin','password':'123456'})
# send_request('post', 'http://192.168.237.128/exam/signup/', {'username': 'admin', 'password': '123456','confirm':'123456','name':'管理员'})
check('登录成功','text','登录成功','登录成功')
check('登录成功', 'text', '登成功', '登录成功')
check('登录成功', 'json', {'a':1}, {'a':1})
check('登录成功', 'json', {'a':1}, {'a':2})

conf 配置文件夹

db.conf

[debug]
host=192.168.16.128
db=exam
user=root
passwd=123456
[smoke]
host=192.168.237.128
db=exam
user=root
passwd=123456
[formal]
host=192.168.150.213
db=exam
user=root
passwd=123456
[regress]
host=192.168.16.194
db=exam
user=root
passwd=123456

entry.ini

[entry]
which_server = debug
which_db = debug

server.conf

[debug] #调试接口服务器
ip=192.168.16.128
port=80
[smoke] #冒烟
ip=192.168.237.128
port=80
[formal] #正式
ip=192.168.150.213
port=80
[regress] #回归
ip=192.168.16.223
port=8000

excelcase 用例文件夹

login.xlsx

case_id

case_name

api_path

method

arg_username

arg_password

expect

login_01

测试登录成功

/exam/login/

post

test01

123456

登录成功

login_02

测试用户名错误

/exam/login/

post

test02

123456

用户名或密码错误

login_03

测试密码错误

/exam/login/

post

test03

123

用户名或密码错误

login_04

测试用户名和密码均错误

/exam/login/

post

test04

123

用户名或密码错误

login_05

测试用户名为空

/exam/login/

post

123456

用户名或密码为空

login_06

测试密码为空

/exam/login/

post

test05

用户名或密码为空

login_07

测试用户名和密码均空

/exam/login/

post

用户名或密码为空

signup.xlsx

case_id

case_name

api_path

method

arg_username

arg_password

arg_confirm

arg_name

expect_Status

expect_Result

expect_Message

check_sql

db_expect_rows

signup_01

测试注册成功

/exam/signup/

post

test06

123456

123456

测试06

1000

Success

注册成功

select count(*) from user where username='test06' and password='123456' and name='测试06'

1

signup_02

测试重复注册

/exam/signup/

post

test07

123456

123456

测试07

1003

Username test07 is taken

用户名已被占用

select count(*) from user where username='test07' and password='123456' and name='测试07'

1

signup_03

测试两次密码不一致

/exam/signup/

post

test08

123456

1234

测试08

1002

Password Not Compare

两次输入的密码不一致

select count(*) from user where username='test08' and password='123456' and name='测试08'

0

signup_04

测试账号为空

/exam/signup/

post

123456

123456

测试00

1001

Input Incomplete

输入信息不完整

select count(*) from user where username='' and password='123456' and name='测试00'

0

signup_05

测试密码为空

/exam/signup/

post

test09

123456

测试09

1001

Input Incomplete

输入信息不完整

select count(*) from user where username='test09' and password='' and name='测试09'

0

signup_06

测试确认密码为空

/exam/signup/

post

test10

123456

测试10

1001

Input Incomplete

输入信息不完整

select count(*) from user where username='test10' and password='123456' and name='测试10'

0

signup_07

测试参数均为空

/exam/signup/

post

测试00

1001

Input Incomplete

输入信息不完整

select count(*) from user where username='' and password='' and name='测试00'

0

initsqls SQL语句存放

login.sql

--登录接口,影响数据库行数:3
--测试登录成功
delete from user where username='test01'
insert into user(id,username,password,name) values(2,'test01','123456','测试01')
--测试账号错误
delete from user where username='test02'

delete from user where username='test03'
insert into user(id,username,password,name) values(3,'test03','123456','测试03')

delete from user where username='test04'

delete from user where username='test05'
insert into user(id,username,password,name) values(4,'test05','123456','测试05')

signup.sql

--注册接口
delete from user where username='test06'

delete from user where username='test07'
insert into user(id,username,password,name) values(5,'test07','123456','测试07')

delete from user where username='test08'

delete from user where username='test09'

delete from user where username='test10'

log 日志目录

report 测试报告目录

runtest 运行文件夹

runtest.py

#最后执行测试
from testcase.login import test_login
from testcase.signup import test_signup
test_login()
test_signup()

testcase 测试脚本

login.py

import pytest
from common.conf import Conf
from common.db import DB
from common.casedata import read_cases
from common.senddata import send_request, check
#测试固件
@pytest.fixture(autouse=True)
def get_host():
global url_host
a=Conf() #测试前,只获得一次host,存为全局变量
url_host=a.host
@pytest.fixture() #登录接口测试函数之前执行,需要手动指定
def init_login():
a=DB()
a.init_db('login.sql')
#pytest测试用例
cases=read_cases('login.xlsx', 'arg_', 4)
@pytest.mark.parametrize('case_id,case_name,api_path,method,args,expect', cases)
def test_login(init_login,case_id,case_name,api_path,method,args,expect):
case_info=f'{case_id}:{case_name}'
test_login.__doc__=case_info
url=url_host+api_path
res_type, actual=send_request(method, url, args)
res,msg=check(case_info, res_type, actual, expect)
assert res, msg
if __name__=='__main__':
pytest.main(['-s', '--tb=short', '--html=../report/login.html', '--self-contained-html' ,'login.py'])

signup.py

import pytest
from common.conf import Conf
from common.db import DB
from common.casedata import read_cases
from common.senddata import send_request,check
#测试固件
@pytest.fixture(autouse=True)
def get_host():
global url_host
a=Conf() #测试前,只获得一次host,存为全局变量
url_host=a.host
@pytest.fixture() #注册接口测试函数之前执行,需要手动指定
def init_signup():
a=DB()
a.init_db('signup.sql')
cases=read_cases('signup.xlsx',['arg_','expect_'],[4,5],col_type={'password':str, 'confirm':'str'})
# print(cases)
@pytest.mark.parametrize('case_id,case_name,api_path,method,args,expect,check_sql,db_expect_rows', cases)
def test_signup(init_signup,case_id,case_name,api_path,method,args,expect,check_sql,db_expect_rows): #测试注册接口的函数
case_info=f'{case_id}:{case_name}'
test_signup.__doc__=case_info
url=url_host+api_path
res_type, actual=send_request(method, url, args)
res,msg=check(case_info, res_type, actual, expect)
pytest.assume(res, msg)
res,msg=DB().check_db(case_info, args, check_sql, db_expect_rows)
assert res, msg
if __name__=='__main__':
pytest.main(['-s', '--tb=short', '--html=../report/signup.html', '--self-contained-html' ,'signup.py'])

conftest.py 系统配置文件

#先执行pytest.ini,再执行conftest.py
import pytest,platform,sys,requests,pymysql,pandas,pytest_html
from py.xml import html

#测试固件
from common.db import DB
@pytest.fixture(scope='session') #初始化所有接口的数据库数据
def init_db():
a=DB()
a.init_db()

# 测试报告名称
def pytest_html_report_title(report):
report.title = "自定义接口测试报告名称"

# Environment部分配置
def pytest_configure(config):
# 删除项
#config._metadata.pop("JAVA_HOME")
config._metadata.pop("Packages")
config._metadata.pop("Platform")
config._metadata.pop("Plugins")
config._metadata.pop("Python")

# 添加项
config._metadata["平台"] = platform.platform()
config._metadata["Python版本"] = platform.python_version()
config._metadata["包"] = f'Requests({requests.__version__}),PyMySQL({pymysql.__version__}),Pandas({pandas.__version__}),Pytest({pytest.__version__}),Pytest-html({pytest_html.__version__})'
config._metadata["项目名称"] = "自定义项目名称"
# from common.entry import Conf
# config._metadata["测试地址"] = Conf().read_server_conf()

# 在result表格中添加测试描述列
@pytest.mark.optionalhook
def pytest_html_results_table_header(cells): #添加列
cells.insert(1, html.th('测试描述')) #第2列处添加一列,列名测试描述
cells.pop()

# 修改result表格测试描述列的数据来源
@pytest.mark.optionalhook
def pytest_html_results_table_row(report, cells): #添加数据
cells.insert(1, html.td(report.description)) #第2列的数据
cells.pop()

# 修改result表格测试描述列的数据
@pytest.mark.hookwrapper
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
report.description = str(item.function.__doc__) #函数注释文档字符串

# # 测试统计部分添加测试部门和人员
# @pytest.mark.optionalhook
# def pytest_html_results_summary(prefix):
# prefix.extend([html.p("所属部门: 自动化测试部")])
# prefix.extend([html.p("测试人员: ***")])

# 解决参数化时汉字不显示问题
def pytest_collection_modifyitems(items):
#下面3行只能解决控制台中,参数化时汉字不显示问题
# for item in items:
# item.name = item.name.encode('utf-8').decode('unicode-escape')
# item._nodeid = item.nodeid.encode('utf-8').decode('unicode-escape')
#下面3行只能解决测试报告中,参数化时汉字不显示问题
outcome = yield
report = outcome.get_result()
report.nodeid = report.nodeid.encode("utf-8").decode("unicode_escape")

作者:暄总-tester