写在前面
做了两三年功能测试的同学,大概率会遇到这样的场景:
- 每次发版,回归用例两百多条,手工点点点跑到怀疑人生
- 改了一个字段,不确定影响范围,只能把相关接口全跑一遍
- 上线后发现接口挂了,而你的测试用例里明明覆盖了——但你不确定是不是最新版本的数据跑的
这些问题,接口自动化都能解决。不是那种”学了面试能吹一吹”的解决,而是实打实地每天帮你省两三个小时的那种解决。
但很多同学想学,打开网上一搜,各种框架五花八门,看完还是不知道怎么开始。今天这篇文章,就带你从零开始、一步步搭一套完整的接口自动化框架。
不用花哨的东西,使用 Python + Pytest + Requests,实用、好懂、能落地
一、框架整体设计思路
在动手写代码之前,先想清楚一件事:框架要分层。
什么是分层?就是让不同的事情各司其职:
| 层级 | 职责 | 举个例子 |
|---|---|---|
| 用例层 | 只管写测试场景 | 登录接口正常登录、密码错误登录 |
| 业务层 | 封装接口调用逻辑 | login(username, password) |
| 请求层 | 封装 HTTP 请求 | 发 GET/POST 请求、统一处理响应 |
| 数据层 | 管理测试数据和配置 | URL、环境变量、用例数据 |
为什么要分这么细?改一个地方不影响其他地方。
比如接口地址变了,只改配置文件;比如请求要加统一的 header,只改请求封装;比如要加新用例,只加用例文件。分层之后,维护成本会大幅降低。
一句话总结:用例只关心”测什么”,不关心”怎么发请求”。
二、技术选型
技术选型这件事,纠结的人特别多。其实对于大多数团队来说,答案很简单:
Python
测试领域第一语言。生态好、学习成本低、上手快。就算你之前只会一点 Java 或完全零基础,Python 也比其他语言更容易上手做自动化。
Pytest
Python 测试框架的天花板。比起 unittest:
- 写法更简洁(不用写 class)
- 参数化一行搞定(
@pytest.mark.parametrize) - 插件生态强大(Allure 报告、并发执行、重试机制全有)
- 支持 fixture,做数据准备和清理非常方便
Requests
发 HTTP 请求的库,没有之一。API 设计优雅,几行代码就能搞定一次请求。不用纠结用什么 HTTP 库,Requests 就是标准答案。
YAML / Excel
测试数据管理。YAML 适合配置和环境信息,结构清晰;Excel 适合管理大量测试用例数据,团队中非技术人员也能维护。
为什么不选 Postman/Newman? Postman 适合快速调试和小规模接口测试。但用例多了之后,维护困难、难以做数据驱动、不方便 CI/CD 集成。代码框架的灵活性和可扩展性是 Postman 比不了的。
三、框架目录结构详解
先看整体结构,再逐个解释:
api_autotest/
├── config/ # 配置文件
│ ├── config.yaml # 环境配置(URL、数据库等)
│ └── __init__.py
├── data/ # 测试数据
│ ├── test_login.yaml # 登录接口测试数据
│ └── __init__.py
├── common/ # 公共模块
│ ├── base_api.py # 请求基础封装
│ ├── assertions.py # 断言封装
│ ├── logger.py # 日志工具
│ ├── db_handler.py # 数据库操作(后续扩展)
│ └── __init__.py
├── testcases/ # 测试用例
│ ├── test_login.py # 登录接口测试
│ └── __init__.py
├── reports/ # 测试报告(自动生成)
├── logs/ # 日志文件(自动生成)
├── conftest.py # Pytest 全局 fixture
├── pytest.ini # Pytest 配置文件
└── requirements.txt # 依赖清单
各目录作用说明:
- config/:放环境配置,比如不同环境的接口地址、账号密码等。切换环境只需要改一个文件。
- data/:测试用例的输入数据和预期结果。数据与用例分离,改数据不用改代码。
- common/:所有公共能力的封装。请求怎么发、断言怎么做、日志怎么记,统一管理。
- testcases/:具体的测试用例。每个接口一个文件(或按模块分文件),只写业务逻辑。
- reports/:Allure 生成的报告存放位置。
- logs/:运行日志存放位置。
- conftest.py:Pytest 的全局配置,放 fixture、钩子函数等。
- pytest.ini:Pytest 的运行配置,指定参数和规则。
四、核心代码实现
下面进入正题,把核心模块一个个写出来。所有代码都是完整可运行的。
4.1 配置文件
先建一个 YAML 配置文件,统一管理环境信息:
# config/config.yaml
env:test
test:
base_url:"https://reqres.in/api"
timeout:10
uat:
base_url:"https://uat.example.com/api"
timeout:10
production:
base_url:"https://api.example.com/api"
timeout:30
然后用一个工具函数来读取配置:
# common/config_reader.py
import yaml
import os
classConfigReader:
def__init__(self):
config_path = os.path.join(os.path.dirname(__file__), '..', 'config', 'config.yaml')
withopen(config_path, 'r', encoding='utf-8') as f:
self.config = yaml.safe_load(f)
defget(self, key, default=None):
"""获取指定配置项"""
returnself.config.get(key, default)
defget_env_config(self):
"""获取当前环境的配置"""
env = self.config.get('env', 'test')
returnself.config.get(env, {})
# 全局实例,直接导入使用
conf = ConfigReader()
4.2 请求封装
这是框架的核心。对 Requests 做二次封装,统一处理请求头、超时、日志和异常:
# common/base_api.py
import requests
import logging
from common.config_reader import conf
logger = logging.getLogger(__name__)
classBaseAPI:
def__init__(self):
self.base_url = conf.get_env_config().get('base_url', '')
self.timeout = conf.get_env_config().get('timeout', 10)
self.session = requests.Session()
# 默认请求头,所有请求都会带上
self.session.headers.update({
'Content-Type': 'application/json',
})
defsend_request(self, method, path, **kwargs):
"""
统一请求入口
:param method: GET/POST/PUT/DELETE
:param path: 接口路径,如 /users
:param kwargs: requests 支持的所有参数(params, json, data, headers等)
"""
url = self.base_url + path
# 设置默认超时,允许单个用例覆盖
kwargs.setdefault('timeout', self.timeout)
logger.info(f"请求 >>> {method}{url}")
if kwargs.get('json'):
logger.info(f"请求体 >>> {kwargs['json']}")
try:
response = self.session.request(method, url, **kwargs)
logger.info(f"响应 <<< 状态码: {response.status_code}")
logger.info(f"响应体 <<< {response.text[:500]}")
return response
except requests.exceptions.Timeout:
logger.error(f"请求超时: {method}{url}")
raise
except requests.exceptions.ConnectionError:
logger.error(f"连接失败: {method}{url}")
raise
defget(self, path, **kwargs):
returnself.send_request('GET', path, **kwargs)
defpost(self, path, **kwargs):
returnself.send_request('POST', path, **kwargs)
defput(self, path, **kwargs):
returnself.send_request('PUT', path, **kwargs)
defdelete(self, path, **kwargs):
returnself.send_request('DELETE', path, **kwargs)
4.3 测试数据管理
用 YAML 文件管理测试数据,实现数据与代码分离:
# data/test_login.yaml
login_success:
username:"eve.holt@reqres.in"
password:"cityslicka"
expected_code:200
expected_key:"token"
login_fail_wrong_password:
username:"eve.holt@reqres.in"
password:"wrong_password"
expected_code:400
expected_message:"Invalid credentials"
login_fail_missing_password:
username:"eve.holt@reqres.in"
password:""
expected_code:400
expected_message:"Missing password"
读取测试数据的工具:
# common/data_loader.py
import yaml
import os
defload_yaml_data(filename):
"""加载 YAML 测试数据文件"""
file_path = os.path.join(os.path.dirname(__file__), '..', 'data', filename)
withopen(file_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
4.4 断言封装
把常用的断言封装起来,让用例里的断言代码更简洁,而且报错信息更友好:
# common/assertions.py
import logging
logger = logging.getLogger(__name__)
defassert_status_code(response, expected_code):
"""断言响应状态码"""
actual_code = response.status_code
assert actual_code == expected_code, \
f"状态码不匹配: 期望 {expected_code}, 实际 {actual_code}"
defassert_response_contains(response, key):
"""断言响应体包含指定字段"""
data = response.json()
assert key in data, \
f"响应体中缺少字段 '{key}',实际响应: {data}"
defassert_equal(actual, expected, msg=""):
"""断言相等"""
assert actual == expected, \
f"{msg} 期望: {expected}, 实际: {actual}"if msg else \
f"值不匹配: 期望 {expected}, 实际 {actual}"
defassert_response_time(response, max_seconds):
"""断言响应时间"""
elapsed = response.elapsed.total_seconds()
assert elapsed <= max_seconds, \
f"响应超时: {elapsed}s > {max_seconds}s"
4.5 测试用例编写
前面所有封装都是为了这一步——用例写得干净、清爽:
# testcases/test_login.py
import pytest
from common.base_api import BaseAPI
from common.assertions import assert_status_code, assert_response_contains, assert_equal
from common.data_loader import load_yaml_data
from common.logger import setup_logger
# 加载测试数据
test_data = load_yaml_data('test_login.yaml')
setup_logger()
classTestLogin:
"""登录接口测试"""
defsetup_class(self):
self.api = BaseAPI()
deftest_login_success(self):
"""正常登录 - 验证返回 token"""
data = test_data['login_success']
response = self.api.post('/login', json={
'email': data['username'],
'password': data['password']
})
assert_status_code(response, data['expected_code'])
assert_response_contains(response, data['expected_key'])
print(f"\n 登录成功,获取到 token: {response.json()['token']}")
deftest_login_wrong_password(self):
"""密码错误登录失败"""
data = test_data['login_fail_wrong_password']
response = self.api.post('/login', json={
'email': data['username'],
'password': data['password']
})
assert_status_code(response, data['expected_code'])
assert_equal(
response.json().get('error'),
data['expected_message'],
"错误信息不匹配"
)
deftest_login_missing_password(self):
"""缺少密码参数"""
data = test_data['login_fail_missing_password']
response = self.api.post('/login', json={
'email': data['username'],
'password': data['password']
})
assert_status_code(response, data['expected_code'])
assert_equal(
response.json().get('error'),
data['expected_message'],
"错误信息不匹配"
)
deftest_login_response_time(self):
"""响应时间不应超过 2 秒"""
data = test_data['login_success']
response = self.api.post('/login', json={
'email': data['username'],
'password': data['password']
})
assert_response_time(response, 2)
4.6 日志配置
没有日志的框架等于没有监控的眼睛:
# common/logger.py
import logging
import os
defsetup_logger():
"""配置全局日志"""
log_dir = os.path.join(os.path.dirname(__file__), '..', 'logs')
os.makedirs(log_dir, exist_ok=True)
formatter = logging.Formatter(
'%(asctime)s [%(levelname)s] %(name)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# 文件日志
file_handler = logging.FileHandler(
os.path.join(log_dir, 'test_run.log'), encoding='utf-8'
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
# 控制台日志
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
# 根日志器配置
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
root_logger.addHandler(file_handler)
root_logger.addHandler(console_handler)
4.7 Pytest 配置
# pytest.ini
[pytest]
testpaths = testcases
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v -s --tb=short
五、运行和报告生成
5.1 安装依赖
# requirements.txt
pytest==8.1.1
requests==2.31.0
PyYAML==6.0.1
allure-pytest==2.13.5
执行安装:
pip install -r requirements.txt
5.2 运行测试
# 运行所有用例
pytest
# 运行指定文件
pytest testcases/test_login.py
# 运行指定用例
pytest testcases/test_login.py::TestLogin::test_login_success
# 失败时自动重试(需安装 pytest-rerunfailures)
pytest --reruns 2
# 并发执行(需安装 pytest-xdist)
pytest -n auto
5.3 生成 Allure 报告
Allure 是目前最好看的测试报告框架,没有之一:
# 生成 Allure 数据(在 reports/allure-raw 目录)
pytest --alluredir=reports/allure-raw
# 启动报告服务(浏览器自动打开)
allure serve reports/allure-raw
Allure 报告支持的特性包括:
- 用例通过率统计
- 历史趋势图
- 用例按功能模块分组
- 附件(请求日志、响应截图)
- 失败用例自动归类
- 标签管理(冒烟测试、回归测试等)
如果想在 CI/CD 中集成,只需在 Jenkins 或 GitLab CI 中加上相同的命令即可。
六、框架扩展方向
基础框架搭好之后,根据实际需求逐步扩展:
数据库操作
测试前后需要准备和清理数据时,封装数据库操作:
# common/db_handler.py
import pymysql
classDBHandler:
def__init__(self, host, port, user, password, database):
self.conn = pymysql.connect(
host=host, port=port, user=user,
password=password, database=database,
charset='utf8mb4', cursorclass=pymysql.cursors.DictCursor
)
defexecute(self, sql, params=None):
withself.conn.cursor() as cursor:
cursor.execute(sql, params)
self.conn.commit()
return cursor.fetchall()
defclose(self):
self.conn.close()
邮件通知
测试跑完自动发邮件通知团队。
数据驱动
用 @pytest.mark.parametrize 或 Excel 实现数据驱动测试,一套代码跑多组数据。
关联处理
从响应中提取 token、session 等动态数据,传递给后续请求。
七、学习路径建议
框架搭完不是终点,而是起点。建议的学习路径:
第一步:先跑通
把这篇文章的代码全部敲一遍,跑通第一个用例。不要复制粘贴,自己敲一遍才能理解每个文件的作用。
跑通的标准:
- 能成功发送一个 HTTP 请求
- 能看到日志输出
- 能看到测试报告
第二步:再加接口
把你工作中最常见的 3-5 个接口加进去。比如:
- 登录接口
- 查询接口
- 新增接口
- 修改接口
- 删除接口
每个接口都按照 test_login.py 的模式来写,慢慢就熟悉了。
第三步:再学参数化
一个接口要测多种场景,比如登录接口:
- 正常登录
- 密码错误
- 账号不存在
- 密码为空
- 账号为空
用 @pytest.mark.parametrize 或者 YAML 数据文件,一套代码跑多组数据。
第四步:再学关联
很多接口之间有依赖关系。比如:
- 登录后拿到 token
- 下一个接口要用这个 token
学会用 fixture 或者变量来传递动态数据。
第五步:再学 CI/CD
把框架集成到 Jenkins 或 GitLab CI,实现:
- 代码提交后自动运行测试
- 测试失败自动发邮件通知
- 测试报告自动生成和归档
八、常见问题
Q1:为什么要分层?直接写用例不行吗?
可以,但维护成本会很高。接口地址变了,你要改所有用例;请求要加统一 header,你要改所有用例。分层之后,改一处就行。
Q2:为什么用 Pytest 不用 unittest?
Pytest 更简洁、功能更强大、插件生态更丰富。unittest 是 Python 自带的,但写法繁琐,不支持很多现代测试框架的特性。
Q3:为什么用 YAML 不用 Excel?
YAML 结构清晰,支持嵌套,适合配置和环境信息。Excel 适合管理大量测试用例数据,但需要额外的库来读取。两个可以结合使用。
Q4:这个框架能用于实际项目吗?
能。这个框架已经包含了实际项目最核心的部分:请求封装、数据管理、断言封装、日志、报告。扩展方向也给了:数据库操作、邮件通知、数据驱动、关联处理。
Q5:学完这个要多久?
- 有 Python 基础:1-2 周能跑通并加上自己的接口
- 没有 Python 基础:先花 1-2 周学 Python 基础,再花 1-2 周学框架
九、总结
这篇文章,我们从零开始搭了一套完整的接口自动化测试框架:
- 分层设计思路
- 技术选型(Python + Pytest + Requests + YAML)
- 目录结构详解
- 核心代码实现(配置、请求、数据、断言、用例、日志)
- 运行和报告生成
- 扩展方向
这套框架不是花架子,而是真正能用于实际项目的。你可以直接拿去用,然后根据自己的需求逐步扩展。
学习自动化测试,最难的不是技术,而是开始。很多人看了无数教程,却从来没有动手写过一行代码。
希望这篇文章能帮你迈出第一步