本项目技术选型主要包括:Python3.7+Requests+Pytest+Excel+Allure 。通过Requests 实现二次封装,来发送和处理HTTP协议的请求接口;使用 Pytest 单元测试框架作为测试执行器;使用 Excel表来管理接口的测试数据;最后使用 Allure 来生成测试报告。

项目目录结构:

第一步:准备数据

1、创建配置文件setting.ini,放在config目录下

[host]
# 服务器域名
BASE_URL = http://127.0.0.1:8888

# mysql数据库配置
[mysql]
# 主机名
HOST = 127.0.0.1
# 端口
PORT = 3306
# 用户名
USER = root
# 密码
PASSWD = 123456
# 数据库
DB = test


第二步:编写工具类,统一放在common目录下

1、读取配置文件工具类:read_file_data.py



#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
@Author:Administrator
@Time:2021/06/30
"""

from configparser import ConfigParser
from common.logger import logger


# 重写ConfigParser中的 optionxform 函数,解决.ini文件中的键option自动转为小写的问题
class MyConfigParser(ConfigParser):
def __init__(self, defaults=None):
ConfigParser.__init__(self, defaults=defaults)

def optionxform(self, optionstr: str) -> str:
return optionstr


class ReadFileData:
# 读取.INI配置文件信息
def read_ini(self, file_path):
logger.info("加载 {} 文件......".format(file_path))
config = MyConfigParser()
config.read(file_path, encoding="utf-8")
data = dict(config._sections)
return data


file_data = ReadFileData()


2、日志工具类:logger.py



#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
@Author:Administrator
@Time:2021/06/30
"""
import logging
import os
import time

# 获取当前文件上一级的上一级目录
BASE_PATH = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
# 定义日志文件路径
log_file_path = os.path.join(BASE_PATH, "log")
# 如果log文件夹不存在,则自动创建
if not os.path.exists(log_file_path):
os.mkdir(log_file_path)


class Logger:
def __init__(self):
# 根据日期生成日志文件名
self.log_name = os.path.join(log_file_path, "{}.log".format(time.strftime("%Y-%m-%d")))
# 创建一个logger日志对象
self.logger = logging.getLogger("log")
# 设置日志级别,默认级别为:WARNING 级别排序:CRITICAL > ERROR > WARNING > INFO > DEBUG
self.logger.setLevel(logging.DEBUG)
# 这个类配置了日志的格式,在里面自定义设置日期和时间,输出日志的时候将会按照设置的格式显示内容。
# %(acstime)s 时间
# %(filename)s 日志文件名
# %(funcName)s 调用日志的函数名
# %(levelname)s 日志的级别
# %(module)s 调用日志的模块名
# %(message)s 日志信息
# %(name)s logger的name,不写的话默认是root
self.formatter = logging.Formatter('%(asctime)s-%(filename)s[line:%(lineno)d]-%(levelname)s: %(message)s')

# 创建StreamHandler对象
self.console = logging.StreamHandler()
# StreamHandler对象自定义日志级别(控制台输出)
self.console.setLevel(logging.DEBUG)

# 创建FileHandler对象
self.file_logger = logging.FileHandler(self.log_name, mode='a', encoding='utf-8')
# 自定义日志级别(日志文件输出)
self.file_logger.setLevel(logging.INFO)
self.file_logger.setFormatter(self.formatter)
# 增加addHandler
self.logger.addHandler(self.file_logger)
self.logger.addHandler(self.console)


logger = Logger().logger


3、操作mysql数据库工具类:operation_mysql.py



#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
@Author:Administrator
@Time:2021/06/30
"""
import os
import pymysql

from common.logger import logger
from common.read_file_data import file_data

# 获取当前文件上一级的上一级目录
BASE_PATH = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
# 获取setting.ini配置文件
data_file_path = os.path.join(BASE_PATH, "config", "setting.ini")
# 读取setting.ini配置文件中mysql下面的值
mysql_data = file_data.read_ini(data_file_path)["mysql"]
DB_data = {
"host": mysql_data["HOST"],
"port": int(mysql_data["PORT"]),
"user": mysql_data["USER"],
"passwd": mysql_data["PASSWD"],
"db": mysql_data["DB"]
}


class MysqlDB:
def __init__(self, db=None):
if db is None:
db = DB_data
# 通过字典拆包传递配置信息,建立数据库连接
self.conn = pymysql.connect(**db, autocommit=True)
# 创建游标对象,并以字典格式输出查询结果
self.cur = self.conn.cursor(cursor=pymysql.cursors.DictCursor)

# 对象资源被释放时触发,在对象即将被删除时的最后操作
def __del__(self):
self.cur.close()
self.conn.close()

def select_db(self, sql):
"""查询数据操作"""
# 检查连接是否断开,如果断开就进行重连
self.conn.ping(reconnect=True)
self.cur.execute(sql)
data = self.cur.fetchall()
return data

def execute_db(self, sql):
"""增/删/改操作"""
try:
self.conn.ping(reconnect=True)
self.cur.execute(sql)
self.conn.commit()
except Exception as e:
logger.info("操作数据库时异常,原因:{}".format(e))
self.conn.rollback()


db_tool = MysqlDB(DB_data)


4、操作excel表工具类:operation_excel.py



#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
@Author:Administrator
@Time:2021/07/01
"""

import openpyxl
from openpyxl.styles import PatternFill


# 表字段数据管理
class ExcelVar:
case_id = '用例编号'
case_module = '用例模块'
case_name = '用例名称'
case_url = '请求地址'
case_method = '请求方式'
case_type = '请求类型'
case_param = '请求参数'
case_header = '请求头'
case_conditions = '预置条件'
case_expect_code = '期望code'
case_expect_msg = '期望msg'
case_test_result = '测试结果'


class ExcelUtils:
def __init__(self, excel_file_path, sheet_name='Sheet1'):
self.excel_file_path = excel_file_path
self.sheet_name = sheet_name

# 返回工作表名
def get_sheet(self):
wb = openpyxl.load_workbook(self.excel_file_path)
sheet = wb[self.sheet_name]
return sheet

# 读取表中数据
def get_excel_data(self):
# 获取工作表名
sheet = self.get_sheet()
data_list = []
# 遍历工作表中所有数据
for i in range(2, sheet.max_row + 1):
data_dict = {
ExcelVar.case_id: sheet.cell(i, 1).value, ExcelVar.case_module: sheet.cell(i, 2).value,
ExcelVar.case_name: sheet.cell(i, 3).value, ExcelVar.case_url: sheet.cell(i, 4).value,
ExcelVar.case_method: sheet.cell(i, 5).value, ExcelVar.case_type: sheet.cell(i, 6).value,
ExcelVar.case_param: sheet.cell(i, 7).value, ExcelVar.case_header: sheet.cell(i, 8).value,
ExcelVar.case_conditions: sheet.cell(i, 9).value, ExcelVar.case_expect_code: sheet.cell(i, 10).value,
ExcelVar.case_expect_msg: sheet.cell(i, 11).value, ExcelVar.case_test_result: sheet.cell(i, 12).value
}
data_list.append(data_dict)
return data_list

# 保存测试结果到EXCEL表中
def write_data_to_excel(self, case_id, result, column_index):
wb = openpyxl.load_workbook(self.excel_file_path)
sheet = wb[self.sheet_name]
# 测试通过则填充绿色
green_fill = PatternFill(fill_type='solid', fgColor='00FF00')
# 测试不通过则填充红色
red_fill = PatternFill(fill_type='solid', fgColor='FF0000')
if result == 'PASSED':
sheet.cell(case_id+1, column_index).fill = green_fill
elif result == 'FAILED':
sheet.cell(case_id+1, column_index).fill = red_fill
sheet.cell(case_id+1, column_index).value= result
wb.save(self.excel_file_path)


 

第三步:封装requests,放在core目录下

request_client.py



#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
@Author:Administrator
@Time:2021/07/01
"""
import requests
import os
import json as cjson

from common.logger import logger
from common.read_file_data import file_data

BASE_PATH = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
ini_file_path = os.path.join(BASE_PATH, "config", 'setting.ini')
# 从setting.ini配置文件中获取BASE_URL
base_url = file_data.read_ini(ini_file_path)["host"]["BASE_URL"]


class RequestClient:
def __init__(self, base_url):
self.base_url = base_url
self.session = requests.session()

# 封装GET请求
def get(self, url, **kwargs):
return self.request(url, "GET", **kwargs)

# 封装POST请求
def post(self, url, data=None, json=None, **kwargs):
return self.request(url, "POST", data, json, **kwargs)

def request(self, url, method, data=None, json=None, **kwargs):
# 拼写url全路径
url = self.base_url + url
headers = dict(**kwargs).get("headers")
params = dict(**kwargs).get("params")
self.print_log(url, method, data, json, params, headers)
if method == 'GET':
return self.session.get(url, **kwargs)
if method == 'POST':
return self.session.post(url, data, json, **kwargs)

# 输出日志信息
def print_log(self, url, method, data=None, json=None, params=None, headers=None, **kwargs):
logger.info("接口请求地址:{}".format(url))
logger.info("接口请求方法:{}".format(method))
# Python3中,json在做dumps操作时,会将中文转换成unicode编码,因此设置 ensure_ascii=False
# indent=4 格式化输出
logger.info("接口请求头:{}".format(cjson.dumps(headers, indent=4, ensure_ascii=False)))
logger.info("接口请求体 data参数:{}".format(cjson.dumps(data, indent=4, ensure_ascii=False)))
logger.info("接口请求体 json参数:{}".format(cjson.dumps(json, indent=4, ensure_ascii=False)))
logger.info("接口请求体 params参数:{}".format(cjson.dumps(params, indent=4, ensure_ascii=False)))


request = RequestClient(base_url)