项目背景
在面试或实际工作中,接口自动化测试能力几乎是测试工程师和开发工程师的必备技能。但很多人只停留在"会用 Postman 点点点"的阶段,缺乏工程化的框架思维。本项目基于 Python + pytest + requests 搭建了一套分层式 API 自动化测试框架,覆盖 GET / POST / PUT / DELETE 与参数化测试,目标是建立可维护、可扩展的接口自动化测试工程。
项目目标
- 掌握 Python + pytest + requests 做接口测试的完整流程
- 实践分层架构:配置层、数据层、接口封装层、用例层、工具层分离
- 使用 YAML 管理配置与测试数据
- 集成日志、异常处理、HTML 报告、Allure 报告
- 为简历提供可直接使用的项目描述
被测对象
JSONPlaceholder:免费的 REST API 练习平台,无需注册和 Token,支持标准的 CRUD 操作,非常适合接口自动化测试学习与教学。
技术栈
| 技术 | 说明 |
|---|---|
| Python 3.10+ | 编程语言 |
| pytest | 测试框架 |
| requests | HTTP 请求库 |
| PyYAML | YAML 配置与数据解析 |
| pytest-html | HTML 测试报告 |
| allure-pytest | Allure 测试报告 |
| pytest-xdist | 并行执行(可选) |
项目结构
api-automation-framework/
├── api/ # 接口封装层
│ ├── base_api.py # 基础请求封装
│ ├── users_api.py # /users 资源接口
│ └── posts_api.py # /posts 资源接口
├── testcases/ # 测试用例
│ ├── test_users.py # 用户接口 CRUD 测试
│ ├── test_posts.py # 帖子接口 CRUD 测试
│ ├── test_parametrize.py # 参数化测试
│ └── test_exceptions.py # 异常场景测试
├── config/
│ └── config.yaml # 基础 URL、超时时间等
├── data/ # YAML 测试数据
│ ├── users.yaml
│ └── posts.yaml
├── utils/ # 工具类
│ ├── config_loader.py # 配置加载
│ ├── data_loader.py # 测试数据加载
│ ├── logger.py # 日志工具
│ └── assertions.py # 断言辅助函数
├── reports/ # 报告输出目录
├── logs/ # 运行日志
├── conftest.py # pytest fixture
├── pytest.ini # pytest 配置
├── requirements.txt
├── run_tests.py # 一键执行脚本
└── README.md
分层设计思想
如果把框架比作一栋楼:
config/是地基配置(URL、超时时间)utils/是工具房(锤子、螺丝刀)api/是水电管线(接口请求)data/是建材仓库(测试数据)testcases/是房间装修(具体测试用例)
分层的好处:
- 好维护:改 URL 只改
config.yaml,不用改几十条用例。 - 好复用:用户接口和帖子接口共用
base_api.py的请求逻辑。 - 好扩展:新增资源(如 comments),只需新增
comments_api.py和test_comments.py。
核心实现
1. 配置文件 config.yaml
base_url: https://jsonplaceholder.typicode.com
timeout: 5
log_level: INFO
response_time_threshold: 5.0
所有环境相关的配置集中管理,便于切换测试环境和生产环境。
2. 配置加载器 utils/config_loader.py
from pathlib import Path
import yaml
CONFIG_PATH = Path(__file__).resolve().parent.parent / "config" / "config.yaml"
class ConfigLoader:
_cache = {}
@classmethod
def load_config(cls, key=None, default=None):
if "config" not in cls._cache:
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
cls._cache["config"] = yaml.safe_load(f)
config = cls._cache["config"]
if key is None:
return config
return config.get(key, default)
使用缓存机制避免重复读取 YAML 文件,通过 Path 定位确保无论在哪里运行测试都能找到配置文件。
3. 基础 API 封装 api/base_api.py
import requests
from requests.exceptions import RequestException
from utils.config_loader import load_config
from utils.logger import get_logger
class BaseAPI:
def __init__(self):
self.base_url = load_config("base_url")
self.timeout = load_config("timeout", 5)
self.logger = get_logger(self.__class__.__name__)
def request(self, method: str, endpoint: str, **kwargs):
url = f"{self.base_url}{endpoint}"
self.logger.info(f"[{method}] {url} 开始请求")
try:
response = requests.request(
method=method.upper(),
url=url,
timeout=self.timeout,
**kwargs,
)
self.logger.info(
f"[{method}] {url} 请求完成,状态码:{response.status_code},"
f"响应时间:{response.elapsed.total_seconds():.3f}s"
)
return response
except RequestException as exc:
self.logger.error(f"[{method}] {url} 请求异常:{exc}")
raise
def get(self, endpoint: str, **kwargs):
return self.request("GET", endpoint, **kwargs)
def post(self, endpoint: str, **kwargs):
return self.request("POST", endpoint, **kwargs)
def put(self, endpoint: str, **kwargs):
return self.request("PUT", endpoint, **kwargs)
def delete(self, endpoint: str, **kwargs):
return self.request("DELETE", endpoint, **kwargs)
BaseAPI 是所有业务接口的基类,集中处理:
- URL 拼接
- 请求日志记录
- 异常捕获与记录
- 响应时间记录
这样测试用例不需要关心底层请求细节,只关注业务断言。
4. 业务接口封装 api/users_api.py
from api.base_api import BaseAPI
class UsersAPI(BaseAPI):
def get_user(self, user_id: int):
return self.get(f"/users/{user_id}")
def list_users(self):
return self.get("/users")
def create_user(self, payload: dict):
return self.post("/users", json=payload)
def update_user(self, user_id: int, payload: dict):
return self.put(f"/users/{user_id}", json=payload)
def delete_user(self, user_id: int):
return self.delete(f"/users/{user_id}")
每个方法对应一个接口,业务语义清晰。新增资源时,只需复制一份并修改名称和路径。
5. pytest Fixture conftest.py
import pytest
from api.users_api import UsersAPI
from api.posts_api import PostsAPI
from utils.config_loader import load_config
@pytest.fixture(scope="session")
def config():
return load_config()
@pytest.fixture(scope="function")
def users_api():
return UsersAPI()
@pytest.fixture(scope="function")
def posts_api():
return PostsAPI()
@pytest.fixture(scope="session")
def response_time_threshold(config):
return config.get("response_time_threshold", 2.0)
scope="session":整个测试会话共享,如config。scope="function":每个测试函数新建实例,避免状态互相影响,如users_api。
6. 测试用例 testcases/test_users.py
import pytest
import allure
from utils.data_loader import load_data
from utils.assertions import (
assert_status_code,
assert_response_time,
assert_json_contains,
assert_field_equals,
)
@allure.feature("用户接口")
@allure.story("查询用户")
class TestGetUser:
@pytest.mark.smoke
@pytest.mark.users
@allure.title("查询单个用户")
def test_get_user_success(self, users_api, response_time_threshold):
response = users_api.get_user(1)
assert_status_code(response, 200)
assert_response_time(response, response_time_threshold)
assert_json_contains(response, "id")
assert_json_contains(response, "name")
assert_json_contains(response, "email")
assert_field_equals(response, "id", 1)
每个用例基本都会断言三个维度:
- 状态码:200、201、404 等
- 响应时间:接口不能慢得离谱
- 关键字段:确认返回了预期的数据
7. 参数化测试 testcases/test_parametrize.py
import pytest
from utils.data_loader import load_data
from utils.assertions import assert_status_code, assert_field_equals
@pytest.mark.parametrize(
"case",
load_data("users.yaml", "parametrize_users"),
ids=[f"user_{c['user_id']}" for c in load_data("users.yaml", "parametrize_users")],
)
def test_get_user_with_parametrize(self, users_api, response_time_threshold, case):
response = users_api.get_user(case["user_id"])
assert_status_code(response, 200)
assert_field_equals(response, "name", case["expected_name"])
assert_field_equals(response, "username", case["expected_username"])
@pytest.mark.parametrize 会自动把 YAML 中的多组数据跑一遍。YAML 里有 3 组数据,这条用例就会生成 3 个测试。
8. 异常场景测试 testcases/test_exceptions.py
import pytest
from requests.exceptions import Timeout
from utils.data_loader import load_data
from utils.assertions import assert_status_code
def test_get_nonexistent_user(self, users_api):
invalid_id = load_data("users.yaml", "invalid_user_id")
response = users_api.get_user(invalid_id)
assert_status_code(response, 404)
def test_request_timeout(self, users_api):
users_api.timeout = 0.001
with pytest.raises(Timeout):
users_api.get_user(1)
异常场景测试验证了接口的容错能力和框架的异常处理机制。
9. YAML 测试数据 data/users.yaml
create_user:
name: Alice Chen
username: alice_dev
email: alice.dev@example.com
phone: 010-12345678
website: https://alice.dev
parametrize_users:
- user_id: 1
expected_name: Leanne Graham
expected_username: Bret
- user_id: 2
expected_name: Ervin Howell
expected_username: Antonette
测试数据与代码分离的好处:
- 改数据不用改代码
- 非技术同事也能看懂和修改
- 便于参数化和数据驱动
10. 日志工具 utils/logger.py
import logging
from pathlib import Path
from utils.config_loader import load_config
LOG_FILE = Path(__file__).resolve().parent.parent / "logs" / "api_test.log"
def get_logger(name: str = "api_test") -> logging.Logger:
logger = logging.getLogger(name)
if logger.handlers:
return logger
level = load_config("log_level", "INFO")
logger.setLevel(getattr(logging, level.upper(), logging.INFO))
formatter = logging.Formatter(
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
file_handler = logging.FileHandler(LOG_FILE, encoding="utf-8")
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger
日志同时输出到控制台和 logs/api_test.log 文件,便于排查问题和追溯历史。
11. 断言工具 utils/assertions.py
def assert_status_code(response, expected: int):
assert response.status_code == expected, (
f"状态码断言失败:期望 {expected},实际 {response.status_code}"
)
def assert_response_time(response, threshold: float):
elapsed = response.elapsed.total_seconds()
assert elapsed < threshold, (
f"响应时间断言失败:期望小于 {threshold}s,实际 {elapsed:.3f}s"
)
def assert_json_contains(response, field: str):
data = response.json()
assert field in data, f"响应 JSON 中缺少字段:{field}"
def assert_field_equals(response, field: str, expected):
data = response.json()
assert data.get(field) == expected, (
f"字段 {field} 断言失败:期望 {expected},实际 {data.get(field)}"
)
断言失败时给出清晰的中文错误信息,方便快速定位问题。
运行方式
运行全部测试
pytest
按标记运行
# 只跑冒烟用例
pytest -m smoke
# 只跑用户接口
pytest -m users
# 只跑帖子接口
pytest -m posts
生成 Allure 报告
pytest --alluredir=reports/allure
allure generate reports/allure -o reports/allure/html --clean
一键执行
python run_tests.py
该脚本会自动清空旧报告、运行全部用例、生成 HTML 和 Allure 报告。
测试覆盖
| 接口类型 | 覆盖内容 |
|---|---|
| GET | 查询单个用户、查询用户列表、查询单个帖子、查询帖子列表 |
| POST | 创建用户、创建帖子 |
| PUT | 更新用户、更新帖子 |
| DELETE | 删除用户、删除帖子 |
| 参数化测试 | 多组用户 ID / 帖子 ID 数据验证 |
| 异常处理 | 查询不存在资源返回 404、网络超时异常捕获 |
项目成果
- 建立了完整的分层式接口自动化测试框架。
- 覆盖 JSONPlaceholder 的 users/posts 资源 CRUD 操作。
- 实现配置、数据、接口、用例、工具完全解耦。
- 集成日志、异常处理、响应时间断言、HTML/Allure 报告。
- 支持按 smoke/users/posts 等自定义标记运行测试。
简历写法参考
API 接口自动化测试框架
- 基于 Python + pytest + requests 搭建分层式接口自动化测试框架,覆盖 JSONPlaceholder 的 users/posts 资源。
- 使用 YAML 管理接口配置与测试数据,通过 conftest.py 统一管理 fixture,实现用例与数据解耦。
- 封装 BaseAPI 统一处理 HTTP 请求、日志记录与异常捕获,提供状态码、响应时间、关键字段等多维度断言。
- 集成 pytest-html 与 Allure 生成可视化测试报告,支持冒烟、用户、帖子、参数化等自定义标记运行。
写在最后
这个项目虽小,但五脏俱全。它覆盖了接口自动化测试的核心能力:接口请求、数据管理、断言设计、日志记录、异常处理、报告输出。掌握这套框架后,完全可以自信地在简历中写"具备接口自动化测试框架搭建能力",并在面试中从容展开。