Pytest27--完善exam项目

结构介绍

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")

posted @ 2021-11-22 20:19  暄总-tester  阅读(154)  评论(0)    收藏  举报