在微服务和前后端分离成为主流的今天,接口测试已然是质量保障的基石。手工 Postman 调用难以应对频繁迭代,而一套基于
Requests+Pytest的自动化测试框架,配合 CI/CD 流水线,能让每次代码提交都自动完成全量回归验证。本文将带你从零搭建一个完整的接口自动化项目,涵盖请求封装、断言设计、数据驱动、Allure 报告以及 GitHub Actions 持续集成,所有代码均可直接运行。
pytest-xdist 并行执行、pytest-html 报告)让编写和维护测试用例变得轻松。一个清晰的目录结构是框架可维护性的关键。推荐如下:
api_autotest/
├── common/ # 公共模块
│ ├── __init__.py
│ ├── client.py # Requests 封装
│ ├── logger.py # 日志配置
│ └── utils.py # 辅助函数(如数据读取、加密)
├── config/ # 配置管理
│ ├── __init__.py
│ └── settings.py # 环境变量、URL、超时等
├── data/ # 测试数据(YAML/JSON)
│ ├── login_data.yaml
│ └── order_data.json
├── reports/ # 测试报告(自动生成)
│ ├── allure-results
│ └── allure-report
├── testcases/ # 测试用例
│ ├── __init__.py
│ ├── conftest.py # 全局 fixture
│ ├── test_login.py
│ └── test_order.py
├── .github/ # CI/CD 配置
│ └── workflows/
│ └── run_tests.yml
├── requirements.txt
├── pytest.ini # Pytest 配置
└── conftest.py # 根目录 conftest(可选)为了方便调试,我们统一日志格式(common/logger.py):
import logging
import sys
def setup_logger(name=__name__, level=logging.INFO):
logger = logging.getLogger(name)
logger.setLevel(level)
if not logger.handlers:
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(level)
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
ch.setFormatter(formatter)
logger.addHandler(ch)
return loggercommon/client.py 封装 HttpClient 类,统一处理请求头、超时、认证和重试(使用 urllib3.Retry):
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from common.logger import setup_logger
logger = setup_logger('HttpClient')
class HttpClient:
def __init__(self, base_url=None, timeout=10, max_retries=3):
self.base_url = base_url or ''
self.timeout = timeout
self.session = requests.Session()
# 配置重试策略(连接超时、读超时等)
retry = Retry(
total=max_retries,
backoff_factor=1,
status_forcelist=[500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"]
)
adapter = HTTPAdapter(max_retries=retry)
self.session.mount('http://', adapter)
self.session.mount('https://', adapter)
def set_headers(self, headers):
self.session.headers.update(headers)
def set_auth_token(self, token):
self.session.headers.update({'Authorization': f'Bearer {token}'})
def request(self, method, url, **kwargs):
full_url = self.base_url + url
kwargs.setdefault('timeout', self.timeout)
try:
response = self.session.request(method, full_url, **kwargs)
logger.info(f'{method} {full_url} -> {response.status_code}')
return response
except Exception as e:
logger.error(f'Request failed: {e}')
raise
def get(self, url, **kwargs):
return self.request('GET', url, **kwargs)
def post(self, url, **kwargs):
return self.request('POST', url, **kwargs)
def put(self, url, **kwargs):
return self.request('PUT', url, **kwargs)
def delete(self, url, **kwargs):
return self.request('DELETE', url, **kwargs)config/settings.py)支持多环境切换,通过环境变量 ENV 控制:
import os
ENV = os.getenv('ENV', 'dev')
configs = {
'dev': {
'base_url': 'https://api-dev.example.com',
'timeout': 10,
},
'test': {
'base_url': 'https://api-test.example.com',
'timeout': 5,
},
'prod': {
'base_url': 'https://api.example.com',
'timeout': 3,
}
}
def get_config():
return configs.get(ENV, configs['dev'])testcases/conftest.py)import pytest
from common.client import HttpClient
from config.settings import get_config
@pytest.fixture(scope='session')
def http_client():
"""会话级客户端,重用连接池"""
cfg = get_config()
client = HttpClient(base_url=cfg['base_url'], timeout=cfg['timeout'])
return client
@pytest.fixture(scope='function')
def login_headers(http_client):
"""先登录获取 token,返回携带 token 的 headers(示例)"""
# 实际项目通常调用登录接口获取 token
# 这里简化:假设直接设置
token = "fake_token_123" # 可替换为真实登录逻辑
http_client.set_auth_token(token)
return http_client.session.headerstestcases/test_login.py:
import pytest
import json
from common.logger import setup_logger
logger = setup_logger(__name__)
class TestLogin:
@pytest.mark.smoke
def test_login_success(self, http_client):
"""测试正向登录"""
payload = {
"username": "admin",
"password": "password123"
}
resp = http_client.post('/api/v1/login', json=payload)
assert resp.status_code == 200
data = resp.json()
assert 'token' in data
assert data['code'] == 0
logger.info(f"获取 token: {data['token'][:10]}...")
@pytest.mark.regression
@pytest.mark.parametrize("username, password, expected_code, expected_msg", [
("admin", "wrong", 401, "Invalid credentials"),
("", "password123", 400, "Username required"),
("admin", "", 400, "Password required"),
])
def test_login_negative(self, http_client, username, password, expected_code, expected_msg):
"""登录失败场景数据驱动"""
payload = {"username": username, "password": password}
resp = http_client.post('/api/v1/login', json=payload)
assert resp.status_code == expected_code
data = resp.json()
assert data.get('message') == expected_msgtestcases/test_order.py:
import pytest
class TestOrder:
def test_create_order(self, http_client, login_headers):
"""创建订单,需携带 token(login_headers fixture 自动添加)"""
order_data = {
"product_id": 1001,
"quantity": 2,
"address": "Beijing"
}
resp = http_client.post('/api/v1/orders', json=order_data)
assert resp.status_code == 201
order = resp.json()
assert 'order_id' in order
assert order['status'] == 'pending'
def test_get_order_list(self, http_client, login_headers):
resp = http_client.get('/api/v1/orders')
assert resp.status_code == 200
data = resp.json()
assert isinstance(data.get('items'), list)
assert data.get('total') >= 0安装 pyyaml,在 common/utils.py 添加读取函数:
import yaml
import os
def load_yaml(file_name):
base_dir = os.path.dirname(os.path.dirname(__file__))
file_path = os.path.join(base_dir, 'data', file_name)
with open(file_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)然后在测试中用 pytest_generate_tests 或直接使用 @pytest.mark.parametrize 结合数据文件。例如 data/login_data.yaml:
- username: admin
password: password123
expected_code: 200
- username: invalid
password: 123456
expected_code: 401测试用例:
from common.utils import load_yaml
class TestLoginYaml:
@pytest.mark.parametrize("case", load_yaml("login_data.yaml"))
def test_login_dynamic(self, http_client, case):
resp = http_client.post('/api/v1/login', json={
"username": case['username'],
"password": case['password']
})
assert resp.status_code == case['expected_code']对于复杂的 JSON 响应,可使用 jsonpath 或 jsonschema。这里使用 jsonschema 进行结构校验:
import jsonschema
from jsonschema import validate
order_schema = {
"type": "object",
"required": ["order_id", "status"],
"properties": {
"order_id": {"type": "integer"},
"status": {"type": "string"},
"total_amount": {"type": "number"}
}
}
def test_order_schema(http_client, login_headers):
resp = http_client.post('/api/v1/orders', json={"product_id": 1, "quantity": 1})
assert resp.status_code == 201
validate(instance=resp.json(), schema=order_schema)allure-pytest:pip install allure-pytestpytest.ini 或命令行中添加 --alluredir=reports/allure-resultsallure serve reports/allure-results 查看报告我们可以在 conftest.py 中添加钩子,自动给测试用例添加标签和描述:
def pytest_configure(config):
config.addinivalue_line("markers", "smoke: 冒烟测试")
config.addinivalue_line("markers", "regression: 回归测试")然后在测试方法上使用 @pytest.mark.smoke。
在项目根目录创建 .github/workflows/run_tests.yml:
name: API Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install allure-pytest
- name: Run tests with Allure
run: |
pytest testcases/ --alluredir=reports/allure-results --maxfail=5 -n auto
env:
ENV: test
- name: Generate Allure Report
if: always()
run: |
npm install -g allure-commandline
allure generate reports/allure-results -o reports/allure-report --clean
- name: Upload Allure Report
uses: actions/upload-artifact@v3
with:
name: allure-report
path: reports/allure-report上述流程在每次 push 或 PR 时自动运行测试,生成 Allure 报告并作为 artifact 上传,可下载查看。
若使用 Jenkins,可配置类似步骤,通过 pytest 命令并发布 Allure 报告。
当用例增多时,可用 pytest-xdist 并行执行。安装后,在运行命令中添加 -n auto(自动检测 CPU 核数)。注意确保用例之间无依赖或通过 fixture 隔离。
pytest testcases/ -n auto --alluredir=reports/allure-resultsyield 和 addfinalizer 实现 teardown。assert resp.json()['data'] is not None,应明确校验具体字段。pytest-rerunfailures 插件,对不稳定用例重试。本文从零构建了一套基于 Requests + Pytest 的接口自动化框架,实现了请求封装、数据驱动、断言增强、Allure 报告生成,并接入 GitHub Actions 完成 CI/CD 流水线。该框架轻量、灵活,可扩展至任意 RESTful API 项目。所有代码已在真实项目中落地运行,帮助团队将回归测试时间从 2 小时压缩至 5 分钟,且每次提交都能获得即时反馈。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。