Python接口测试框架搭建实战(上)
本节内容概要
- 测试数据分离 - 从Excel文件读取测试数据
- 日志记录功能实现
- 测试报告邮件发送
- 配置文件管理
- 框架目录结构优化

一、测试数据分离 - 从Excel读取数据
在前面的测试代码中,测试数据直接硬编码在Python文件中,这样的方式不利于数据的维护和修改。我们将测试数据存储在Excel文件中,实现代码与数据的分离。
创建Excel文件`api_test_data.xlsx`,包含两个工作表`LoginTest`和`RegisterTest`,并放置在项目根目录。

Excel数据表结构
在Excel中增加headers列,用于存储请求头信息,格式为JSON。
LoginTest工作表:
| case_name | url | method | headers | data | expect_res |
|---|---|---|---|---|---|
| test_login_success | http://115.28.108.130:5000/api/user/login/ | POST | {} | {"name":"zhangsan","password":"123456"} | <h1>登录成功</h1> |
| test_login_password_error | http://115.28.108.130:5000/api/user/login/ | POST | {} | {"name":"zhangsan","password":"1234567"} | <h1>失败,用户名或密码错误</h1> |
RegisterTest工作表:
| case_name | url | method | headers | data | expect_res |
|---|---|---|---|---|---|
| test_register_success | http://115.28.108.130:5000/api/user/reg/ | POST | {} | {"name":"lisi","password":"123456"} | {"code":"100000","msg":"成功","data": |
| test_register_already_exists | http://115.28.108.130:5000/api/user/reg/ | POST | {} | {"name":"wangwu","password":"123456"} | {"code":"100001","msg":"失败,用户已存在","data":{"name":"wangwu","password":"e10adc3949ba59abbe56e057f20f883e"}} |
Excel读取实现
使用Python的xlrd库来读取Excel文件。
pip install xlrd
import xlrd
# 打开Excel文件
workbook = xlrd.open_workbook("api_test_data.xlsx")
# 通过工作表名称获取 sheet 对象
sheet = workbook.sheet_by_name("LoginTest")
# 获取行列数
print(f"数据行数:{sheet.nrows}")
print(f"数据列数:{sheet.ncols}")
# 获取单个单元格内容
print(f"第一行第一列:{sheet.cell(0, 0).value}")
# 获取整行数据
print(f"第一行数据:{sheet.row_values(0)}")
# 合并标题与数据行为字典
first_row = sheet.row_values(0)
second_row = sheet.row_values(1)
print(dict(zip(first_row, second_row)))
# 遍历所有数据行
for i in range(sheet.nrows):
print(f"第{i}行数据:{sheet.row_values(i)}")
执行结果:
数据行数:3
数据列数:5
第一行第一列:case_name
第一行数据:['case_name', 'url', 'method', 'data', 'expect_res']
{'case_name': 'test_login_success', 'url': 'http://115.28.108.130:5000/api/user/login/', 'method': 'POST', 'data': '{"name":"zhangsan","password":"123456"}', 'expect_res': '<h1>登录成功</h1>'}
第0行数据:['case_name', 'url', 'method', 'data', 'expect_res']
第1行数据:['test_login_success', 'http://115.28.108.130:5000/api/user/login/', 'POST', '{"name":"zhangsan","password":"123456"}', '<h1>登录成功</h1>']
第2行数据:['test_login_password_error', 'http://115.28.108.130:5000/api/user/login/', 'POST', '{"name":"zhangsan","password":"1234567"}', '<h1>失败,用户不存在</h1>']
封装Excel读取工具类
新建`data_helper.py`文件,封装两个函数:
- `load_excel_data(file_name, sheet_name)`:一次性读取指定工作表的所有数据
- `find_test_case(all_data, case_name)`:根据用例名称查找对应数据
import xlrd
def load_excel_data(file_name, sheet_name):
"""
读取Excel文件中指定工作表的所有数据
返回:列表形式,每个元素为一条用例数据的字典
"""
result_data = []
workbook = xlrd.open_workbook(file_name)
sheet = workbook.sheet_by_name(sheet_name)
# 获取标题行
header_row = sheet.row_values(0)
# 跳过标题行,从第二行开始遍历
for i in range(1, sheet.nrows):
row_data = sheet.row_values(i)
case_dict = dict(zip(header_row, row_data))
result_data.append(case_dict)
return result_data
def find_test_case(data_list, case_name):
"""
从数据列表中查找指定用例名的数据
返回:找到返回用例数据字典,未找到返回None
"""
for item in data_list:
if case_name == item.get('case_name'):
return item
return None
if __name__ == '__main__':
# 测试代码
all_cases = load_excel_data("api_test_data.xlsx", "LoginTest")
case_info = find_test_case(all_cases, 'test_login_success')
print(case_info)
输出结果:
{'case_name': 'test_login_success', 'url': 'http://115.28.108.130:5000/api/user/login/', 'method': 'POST', 'data': '{"name":"zhangsan","password":"123456"}', 'expect_res': '<h1>登录成功</h1>'}
测试用例中应用数据
登录测试用例 - test_login.py
import unittest
import requests
from data_helper import load_excel_data, find_test_case
import json
class TestUserLogin(unittest.TestCase):
@classmethod
def setUpClass(cls):
# 读取该测试类的所有用例数据,整个类只执行一次
cls.case_data_list = load_excel_data("api_test_data.xlsx", "LoginTest")
def test_login_success(self):
# 从数据列表中查找对应用例数据
case_data = find_test_case(self.case_data_list, 'test_login_success')
if not case_data:
print("未找到用例数据")
return
url = case_data.get('url')
data = case_data.get('data')
expect_res = case_data.get('expect_res')
# 发送POST请求,data参数需要转换为字典
response = requests.post(url=url, data=json.loads(data))
# 断言验证
self.assertEqual(response.text, expect_res)
if __name__ == '__main__':
unittest.main(verbosity=2)
注册测试用例 - test_register.py
import unittest
import requests
from db_helper import query_db, execute_db
from data_helper import load_excel_data, find_test_case
import json
class TestUserRegister(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.case_data_list = load_excel_data("api_test_data.xlsx", "RegisterTest")
def test_register_success(self):
case_data = find_test_case(self.case_data_list, 'test_register_success')
if not case_data:
print("未找到用例数据")
return
url = case_data.get('url')
data_dict = json.loads(case_data.get('data'))
expect_dict = json.loads(case_data.get('expect_res'))
user_name = data_dict.get("name")
# 环境准备:清理已存在的用户
if query_db(f"SELECT * FROM users WHERE name='{user_name}'"):
execute_db(f"DELETE FROM users WHERE name='{user_name}'")
# 发送注册请求
response = requests.post(url=url, json=data_dict)
# 响应结果断言
self.assertDictEqual(response.json(), expect_dict)
# 数据库数据验证
result = query_db(f"SELECT * FROM users WHERE name='{user_name}'")
self.assertTrue(len(result) > 0, "用户未成功注册到数据库")
# 清理测试数据
execute_db(f"DELETE FROM users WHERE name='{user_name}'")
if __name__ == '__main__':
unittest.main(verbosity=2)
二、日志功能实现
创建`logger.py`配置文件,配置日志输出格式和级别。
import logging
# 日志基础配置
logging.basicConfig(
level=logging.DEBUG, # 日志级别
format='[%(asctime)s] %(levelname)s [%(funcName)s:%(filename)s, %(lineno)d] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
filename='run_log.txt',
filemode='a'
)
if __name__ == '__main__':
logging.info("测试日志输出")
运行后会在当前目录生成`run_log.txt`,内容示例:
[2018-09-11 18:08:17] INFO [<module>: logger.py, 38] 测试日志输出
日志级别说明
- CRITICAL - 严重错误,导致程序停止运行
- ERROR - 错误信息,影响功能运行
- WARNING - 警告信息,提示潜在问题
- INFO - 一般信息,记录正常流程
- DEBUG - 调试信息,详细执行过程
级别优先级:CRITICAL > ERROR > WARNING > INFO > DEBUG
设置level=DEBUG时,所有级别都会输出;设置level=ERROR时,仅输出ERROR和CRITICAL。
日志格式化参数
- %(levelno)s - 日志级别数值
- %(levelname)s - 日志级别名称
- %(pathname)s - 执行程序完整路径
- %(filename)s - 执行程序文件名
- %(funcName)s - 所在函数名
- %(lineno)d - 所在行号
- %(asctime)s - 时间戳
- %(thread)d - 线程ID
- %(process)d - 进程ID
- %(message)s - 日志内容
项目中集成日志
修改`db_helper.py`,添加日志记录
import pymysql
from logger import *
def query_db(sql):
conn = get_db_connection()
cursor = conn.cursor()
logging.debug(f"执行SQL查询:{sql}")
cursor.execute(sql)
conn.commit()
query_result = cursor.fetchall()
logging.debug(f"查询结果:{query_result}")
cursor.close()
conn.close()
return query_result
def execute_db(sql):
conn = get_db_connection()
cursor = conn.cursor()
logging.debug(f"执行SQL语句:{sql}")
try:
cursor.execute(sql)
conn.commit()
except Exception as error:
conn.rollback()
logging.error(f"SQL执行失败:{str(error)}")
finally:
cursor.close()
conn.close()
在测试用例中使用日志
import unittest
import requests
from data_helper import load_excel_data, find_test_case
import json
from logger import logging
class TestUserLogin(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.case_data_list = load_excel_data("api_test_data.xlsx", "LoginTest")
def test_login_success(self):
case_data = find_test_case(self.case_data_list, 'test_login_success')
if not case_data:
logging.error("用例数据不存在")
return
url = case_data.get('url')
data = case_data.get('data')
expect_res = case_data.get('expect_res')
response = requests.post(url=url, data=json.loads(data))
# 输出详细日志信息
logging.info(f"测试用例:test_login_success")
logging.info(f"请求地址:{url}")
logging.info(f"请求参数:{data}")
logging.info(f"预期结果:{expect_res}")
logging.info(f"实际结果:{response.text}")
self.assertEqual(response.text, expect_res)
if __name__ == '__main__':
unittest.main(verbosity=2)
生成的日志文件内容:
[2018-09-13 10:34:49] INFO [test_login_success: test_login.py, 20] 测试用例:test_login_success
[2018-09-13 10:34:49] INFO [test_login_success: test_login.py, 21] 请求地址:http://115.28.108.130:5000/api/user/login/
[2018-09-13 10:34:49] INFO [test_login_success: test_login.py, 22] 请求参数:{"name":"zhangsan","password":"123456"}
[2018-09-13 10:34:49] INFO [test_login_success: test_login.py, 23] 预期结果:<h1>登录成功</h1>
[2018-09-13 10:34:49] INFO [test_login_success: test_login.py, 24] 实际结果:<h1>登录成功</h1>
封装日志输出函数
由于每个测试用例都需要记录大量日志信息,创建`logger_helper.py`统一处理。
from logger import logging
import json
def record_test_log(case_name, url, request_data, expected, actual_response):
"""
统一记录测试用例的日志信息
"""
if isinstance(request_data, dict):
request_data = json.dumps(request_data, ensure_ascii=False)
logging.info(f"用例名称:{case_name}")
logging.info(f"请求地址:{url}")
logging.info(f"请求参数:{request_data}")
logging.info(f"预期结果:{expected}")
logging.info(f"实际结果:{actual_response}")
简化后的测试用例
import unittest
import requests
from data_helper import load_excel_data, find_test_case
import json
from logger import logging
from logger_helper import record_test_log
class TestUserLogin(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.case_data_list = load_excel_data("api_test_data.xlsx", "LoginTest")
def test_login_success(self):
case_data = find_test_case(self.case_data_list, 'test_login_success')
if not case_data:
logging.error("用例数据不存在")
return
url = case_data.get('url')
data = case_data.get('data')
expect_res = case_data.get('expect_res')
response = requests.post(url=url, data=json.loads(data))
# 使用封装好的日志函数
record_test_log('test_login_success', url, data, expect_res, response.text)
self.assertEqual(response.text, expect_res)
if __name__ == '__main__':
unittest.main(verbosity=2)
三、测试报告邮件发送
测试完成后,需要自动将测试报告发送到指定邮箱。Python发送邮件通过SMTP服务实现。
前提条件:发送邮件的邮箱需要开启SMTP服务
邮件发送流程
- 编写邮件内容(MIME格式)
- 组装邮件头(发件人、收件人、主题)
- 连接SMTP服务器并发送
基础邮件发送
import smtplib
from email.mime.text import MIMEText
# 1. 编写邮件内容
msg = MIMEText('这是一封测试邮件', 'plain', 'utf-8')
# 2. 组装邮件头
msg['From'] = 'test_sender@sina.com'
msg['To'] = 'receiver@example.com'
msg['Subject'] = '接口测试报告'
# 3. 连接SMTP服务器并发送
smtp = smtplib.SMTP_SSL('smtp.sina.com')
smtp.login('your_email', 'your_password')
smtp.sendmail('sender@example.com', 'receiver@example.com', msg.as_string())
smtp.quit()
发送HTML邮件及附件
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
# 读取HTML报告内容
with open('test_report.html', 'r', encoding='utf-8') as f:
email_content = f.read()
# 创建混合格式邮件
email_msg = MIMEMultipart()
email_msg.attach(MIMEText(email_content, 'html', 'utf-8'))
# 设置邮件头
email_msg['From'] = 'test_sender@sina.com'
email_msg['To'] = 'receiver@example.com'
email_msg['Subject'] = Header('接口测试报告', 'utf-8')
# 添加附件
attachment = MIMEText(open('test_report.html', 'rb').read(), 'base64', 'utf-8')
attachment["Content-Type"] = 'application/octet-stream'
attachment["Content-Disposition"] = 'attachment; filename="test_report.html"'
email_msg.attach(attachment)
# 发送邮件
smtp = smtplib.SMTP_SSL('smtp.sina.com')
smtp.login('test_sender@sina.com', 'password123')
smtp.sendmail('test_sender@sina.com', 'receiver@example.com', email_msg.as_string())
smtp.quit()
封装邮件发送函数
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
from logger import logging
def send_test_report(report_file_path):
"""
发送测试报告邮件
"""
email_msg = MIMEMultipart()
# 添加邮件正文
with open(report_file_path, 'r', encoding='utf-8') as f:
email_content = f.read()
email_msg.attach(MIMEText(email_content, 'html', 'utf-8'))
# 设置邮件头
email_msg['From'] = 'test_sender@sina.com'
email_msg['To'] = 'receiver@example.com'
email_msg['Subject'] = Header('接口测试报告', 'utf-8')
# 添加附件
attachment = MIMEText(open(report_file_path, 'rb').read(), 'base64', 'utf-8')
attachment["Content-Type"] = 'application/octet-stream'
attachment["Content-Disposition"] = f'attachment; filename="{report_file_path}"'
email_msg.attach(attachment)
try:
smtp = smtplib.SMTP_SSL('smtp.sina.com')
smtp.login('test_sender@sina.com', 'password123')
smtp.sendmail('test_sender@sina.com', 'receiver@example.com', email_msg.as_string())
logging.info("邮件发送成功!")
except Exception as e:
logging.error(f"邮件发送失败:{str(e)}")
finally:
smtp.quit()
测试执行并发送邮件
import unittest
from HTMLTestReportCN import HTMLTestRunner
from logger import logging
from mail_helper import send_test_report
logging.info("=================== 测试开始 ===================")
# 发现并执行所有测试用例
test_suite = unittest.defaultTestLoader.discover("./")
# 生成测试报告
with open("test_report.html", 'wb') as f:
runner = HTMLTestRunner(stream=f, title="API测试报告", description="接口测试执行结果")
runner.run(test_suite)
# 发送邮件
send_test_report('test_report.html')
logging.info("=================== 测试结束 ===================")

四、配置文件管理
将数据库配置、邮件配置、日志配置等统一放到`settings.py`文件中管理。
import logging
import os
# 项目根目录
project_root = os.path.dirname(os.path.abspath(__file__))
# 路径配置
data_dir = project_root
test_case_dir = project_root
log_file = os.path.join(project_root, 'log.txt')
report_file = os.path.join(project_root, 'report.html')
# 日志配置
logging.basicConfig(
level=logging.DEBUG,
format='[%(asctime)s] %(levelname)s [%(funcName)s:%(filename)s, %(lineno)d] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
filename=log_file,
filemode='a'
)
# 数据库配置
db_host = '127.0.0.1'
db_port = 3306
db_user = 'test_user'
db_password = 'test123456'
db_name = 'api_test'
# 邮件配置
smtp_server = 'smtp.sina.com'
smtp_user = 'test_sender@sina.com'
smtp_password = 'password123'
email_from = smtp_user
email_to = 'receiver@example.com'
email_subject = '接口测试报告'
修改相关文件引用配置
数据库连接 - db_helper.py
import pymysql
from settings import *
def get_db_connection():
return pymysql.connect(
host=db_host,
port=db_port,
user=db_user,
passwd=db_password,
db=db_name,
charset='utf8'
)
邮件发送 - mail_helper.py
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
from settings import *
from logger import logging
def send_test_report(report_file_path):
email_msg = MIMEMultipart()
email_msg.attach(MIMEText(open(report_file_path, 'r', encoding='utf-8').read(), 'html', 'utf-8'))
email_msg['From'] = email_from
email_msg['To'] = email_to
email_msg['Subject'] = Header(email_subject, 'utf-8')
attachment = MIMEText(open(report_file_path, 'rb').read(), 'base64', 'utf-8')
attachment["Content-Type"] = 'application/octet-stream'
attachment["Content-Disposition"] = f'attachment; filename="{os.path.basename(report_file_path)}"'
email_msg.attach(attachment)
try:
smtp = smtplib.SMTP_SSL(smtp_server)
smtp.login(smtp_user, smtp_password)
smtp.sendmail(email_from, email_to, email_msg.as_string())
logging.info("测试报告邮件已发送")
except Exception as error:
logging.error(f"邮件发送失败:{str(error)}")
finally:
smtp.quit()
测试执行 - run_all.py
import unittest
from HTMLTestReportCN import HTMLTestRunner
from settings import logging
from mail_helper import send_test_report
logging.info("=================== 测试开始 ===================")
test_suite = unittest.defaultTestLoader.discover(test_case_dir)
with open(report_file, 'wb') as f:
runner = HTMLTestRunner(stream=f, title="API测试报告", description="接口测试结果")
runner.run(test_suite)
send_test_report(report_file)
logging.info("=================== 测试结束 ===================")
五、框架目录结构优化
随着项目规模扩大,需要对文件进行分类管理,建立清晰的目录结构。

创建目录结构
- config/ - 项目配置文件
- data/ - 测试数据文件
- libs/ - 公共方法库
- logs/ - 日志文件
- reports/ - 测试报告
- test_cases/ - 测试用例
- user/ - 用户模块用例(需包含__init__.py)

修改配置路径
修改`config/settings.py`中的路径配置:
import logging
import os
# 项目根目录(config目录的上一级)
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 路径配置
data_dir = os.path.join(project_root, 'data')
test_case_dir = os.path.join(project_root, 'test_cases')
log_file = os.path.join(project_root, 'logs', 'run_log.txt')
report_file = os.path.join(project_root, 'reports', 'test_report.html')
处理导入路径问题
由于目录结构变化,需要调整导入路径。使用sys.path添加项目根目录到搜索路径。
libs/db_helper.py
import pymysql
import sys
sys.path.append('..') # 提升到项目根目录
from config.settings import *
test_cases/user/test_login.py
import unittest
import requests
import json
import os
import sys
sys.path.append('../..') # 提升两级到项目根目录
from config.settings import logging
from libs.data_helper import load_excel_data, find_test_case
from libs.logger_helper import record_test_log
class TestUserLogin(unittest.TestCase):
@classmethod
def setUpClass(cls):
excel_path = os.path.join(data_dir, "api_test_data.xlsx")
cls.case_data_list = load_excel_data(excel_path, "LoginTest")
def test_login_success(self):
case_data = find_test_case(self.case_data_list, 'test_login_success')
if not case_data:
logging.error("用例数据不存在")
return
url = case_data.get('url')
data = case_data.get('data')
expect_res = case_data.get('expect_res')
response = requests.post(url=url, data=json.loads(data))
record_test_log('test_login_success', url, data, expect_res, response.text)
self.assertEqual(response.text, expect_res)
run_all.py(项目根目录)
import unittest
from libs.HTMLTestReportCN import HTMLTestRunner
from config.settings import logging
from libs.mail_helper import send_test_report
logging.info("=================== 测试开始 ===================")
test_suite = unittest.defaultTestLoader.discover(test_case_dir)
with open(report_file, 'wb') as f:
runner = HTMLTestRunner(stream=f, title="API测试报告", description="接口测试执行")
runner.run(test_suite)
send_test_report(report_file)
logging.info("=================== 测试结束 ===================")
注意事项
- 同一目录下模块相互导入也需要从项目根目录导入
- run_all.py位于项目根目录,无需修改sys.path
- 运行测试后根据日志和报告调试,确保所有用例通过
本节内容到此结束,下一节将继续完善框架功能,包括用例基类设计、用例标签管理和失败用例重试机制。