本文同步平台:blog.csdn.net/qq_58221901…
前言:
- github仓库链接:后续补充链接
- 适用于学习的开放API链接:wanandroid.com/blog/show/2
- 注册/登录:——
- 开放API接口(jmx文件/psotman文件):——
- 其他项目参考地址:GitHub - yushaoqi/PytestAutoApi: 本框架主要是基于 Python + pytest + allure + log + yaml + mysql + 钉钉通知 + Jenkins 实现的接口自动化框架,本框架优势在于易维护,功能丰富,测试人员只需要维护测试用例,零基础小白也可以快速上手,框架支持多环境、多角色任意切换,支持接口响应断言以及数据库断言。 · GitHub
为什么选Pytest
自动化框架有以下几种:
1.pytest(需要安装, 高扩展性,简单)
2.unittest(不需要安装,标准库,需要写类,格式规范)
3.Robot Framework
1. 创造编程环境(AI编程):
下载Cursor,一个类似Vscode的代码编译器(自带AI),新账号有免费额度:cursor.com/cn/download
(或者选择下载trae,也有免费额度 : TRAE - Collaborate with Intelligence )
(或者在Vscode 安装 Condex插件)
1.安装
2.注册账号
3.登录
4.换成高亮;(个人习惯)
5.有编码模式/ai agent 模式,选择编码模式
6.新建项目
2. 创造python环境/cursor终端环境
(1)cursor终端环境:
安装python插件 :
打开python terminal :
cursor选择终端:
cursor选择环境:
(2)python环境
(创建虚拟环境隔离项目、避免不同项目使用不同版本的依赖导致的环境冲突的问题)
这里创建conda环境(可以直接在cmd中创建和激活,然后到cursor终端选择环境) :
创建环境:conda create -n cursor_pytest_autotest python==3.11
激活环境:conda activate cursor_pytest_autotest
(3)配置python环境,安装部分依赖
pip install pytest
pip install https
pip install requests
pip install python-dotenv
3. 手动创建目录,创建文件夹/文件的指令:
midir -p .../file_path // 创建文件夹 -p 父目录
touch .../file_path.txt // 创建文件
cat file_path1.txt file_path2.txt // 查看文件1、 文件2
cat file_path1.txt > file_path2.txt // 1内容输出到2
cat file_path1.txt >> file_path2.txt // 1内容追加到2
4. pytest框架知识点:
pytest相关文件的命名规范:
文件名必须以test_开头或者_test结尾;
测试类必须以test开头,并且不能有__init__方法;
测试方法必须以test开头
pytest 的装饰器(decorator)
@pytest.fixture
fixture的scope级别:
#准备测试需要的东西
@pytest.fixture(scope="session") #fixture 的初始化/生命周期是 session 级别
def logged_in_client(api_client: ApiClient):
...
'''
把 logged_in_client() 定义成一个 fixture,并且这个 fixture 在整个 pytest 测试会话期间只创建/执行一次。
'''
@pytest.mark.parametrize
#让同一个测试用不同数据跑多次
@pytest.mark.parametrize(
"username,password",
[
("user1", "123456"),
("user2", "123456"),
("user3", "123456"),
]
)
def test_login(username, password):
print(username, password)
'''
pytest 会自动把它变成 3 次测试:
test_login[user1-123456]
test_login[user2-123456]
test_login[user3-123456]
只写了一个函数,但是pytest执行了三次测试
'''
5. 核心代码示例
api_client.py
import httpx
class ApiClient:
"""
API 客户端封装类。
用于统一管理 API 请求,包括:
- HTTP GET/POST 请求
- 请求 Header
- Bearer Token(如果需要token登录的话)
- Cookie(如果是Cookie登录的话)
- HTTP Client 的生命周期
"""
def __init__(self, base_url: str, timeout: float = 30.0):
self._client = httpx.Client(
base_url=base_url,
timeout=timeout,
)
def set_header(self, name: str, value: str) -> None:
self._client.headers[name] = value
def set_cookie(
self,
name: str,
value: str,
domain: str | None = None,
path: str = "/",
) -> None:
self._client.cookies.set(
name,
value,
domain=domain,
path=path,
)
def get(self, path: str, params: dict | None = None) -> httpx.Response:
return self._client.get(path, params=params)
def post(
self,
path: str,
json: dict | None = None,
data: dict | None = None,
) -> httpx.Response:
return self._client.post(path, json=json, data=data)
def close(self):
self._client.close()
conftest.py
# base_url配置到环境变量里或从配置文件获取
@pytest.fixture(scope="session")
def api_client():
base_url = os.environ.get("API_BASE_URL")
if not base_url:
raise RuntimeError("Missing env var: API_BASE_URL")
client = ApiClient(base_url=base_url)
yield client
client.close()
# 直接用从网站上获取的cookie : f12 Application / Storage / cookie / url: ... ...
# 配置到环境变量里,从环境变量里面获取
@pytest.fixture(scope="session")
def logged_in_client(api_client: ApiClient):
login_username = os.environ.get("LOGINUSERNAME")
token_pass = os.environ.get("TOKENPASS")
jsessionid = os.environ.get("JSESSIONID")
domain = os.environ.get("DOMAIN")
if not all([
login_username,
token_pass,
jsessionid,
domain,
]):
pytest.skip(
"Cookie configuration not found in .env"
)
api_client.set_cookie(
"loginUserName",
login_username,
domain=domain,
path="/",
)
api_client.set_cookie(
"token_pass",
token_pass,
domain=domain,
path="/",
)
api_client.set_cookie(
"JSESSIONID",
jsessionid,
domain=domain,
path="/",
)
return api_client
assertions.py
from typing import Any
def key_exists_anywhere(obj: Any, target_key: str) -> bool:
'''
递归查找对象(dict/list 混合)中是否存在指定 key。
'''
if isinstance(obj, dict):
if target_key in obj:
return True
return any(key_exists_anywhere(v, target_key) for v in obj.values())
if isinstance(obj, list):
return any(key_exists_anywhere(x, target_key) for x in obj)
return False
def get_by_path(obj: Any, path: str) -> Any:
"""
按点分路径取值,支持数组索引。例如:data.datas.0.title,缺失安全返回 `None`。
"""
cur = obj
for part in path.split("."):
if cur is None:
return None
if isinstance(cur, dict):
cur = cur.get(part)
elif isinstance(cur, list) and part.isdigit():
idx = int(part)
cur = cur[idx] if 0 <= idx < len(cur) else None
else:
return None
return cur
def assert_wan_response(
resp,
expected_status: int = 200,
expected_error_code: int | None = 0,
data_keys: list[str] | None = None,
path_equals: dict[str, Any] | None = None,
) -> dict:
"""
针对 wanandroid 统一响应结构做断言:
{ "data": ..., "errorCode": 0, "errorMsg": "" }
1. HTTP 状态码 = 200
2. body 必含 `errorCode`、`errorMsg`
3. `errorCode` == 0(可设 `None` 关闭)
4. `data_keys` 中每个 key 都能在响应任意层级找到(递归)
5. `path_equals` 按点分路径做等值断言(支持数组索引,如 `data.datas.0.title`)
"""
assert resp.status_code == expected_status, (
f"HTTP status mismatch: expected {expected_status}, got {resp.status_code}. "
f"body: {resp.text[:500]}"
)
body = resp.json()
assert "errorCode" in body, f"Missing errorCode in response: {body}"
assert "errorMsg" in body, f"Missing errorMsg in response: {body}"
if expected_error_code is not None:
assert body["errorCode"] == expected_error_code, (
f"errorCode mismatch: expected {expected_error_code}, "
f"got {body['errorCode']}. errorMsg: {body.get('errorMsg')}"
)
if data_keys:
for k in data_keys:
assert key_exists_anywhere(body, k), (
f"Expected key '{k}' not found anywhere in response body"
)
if path_equals:
for path, expected in path_equals.items():
actual = get_by_path(body, path)
assert actual == expected, (
f"path '{path}' mismatch: expected {expected!r}, got {actual!r}"
)
return body
utils.py
如果是json文件数据驱动的话,以下代码用于加载案例
import json
from pathlib import Path
from typing import Any
def load_cases(json_filename: str) -> list[dict[str, Any]]:
"""
从 tests/data/<json_filename> 读取用例列表
"""
project_root = Path(__file__).resolve().parents[3] # 根据实际情况调整根目录获取方式
path = project_root / "tests" / "data" / json_filename
return json.loads(path.read_text(encoding="utf-8"))
test.py 示例
class TestAuth:
def test_login_without_credentials_returns_error(self, api_client: ApiClient):
resp = api_client.post(
"/user/login",
data={"username": "", "password": ""},
)
body = resp.json()
assert resp.status_code == 200
assert body["errorCode"] != 0
assert "errorMsg" in body
运行代码指令
pytest