
前言github仓库链接后续补充链接适用于学习的开放API链接https://wanandroid.com/blog/show/2注册/登录——开放API接口jmx文件/psotman文件后续补充jmx文件其他项目参考地址GitHub - yushaoqi/PytestAutoApi: 本框架主要是基于 Python pytest allure log yaml mysql 钉钉通知 Jenkins 实现的接口自动化框架本框架优势在于易维护功能丰富测试人员只需要维护测试用例零基础小白也可以快速上手框架支持多环境、多角色任意切换支持接口响应断言以及数据库断言。 · GitHub为什么选Pytest自动化框架有以下几种1.pytest需要安装 高扩展性简单2.unittest不需要安装标准库需要写类格式规范3.Robot Framework1. 创造编程环境AI编程下载Cursor一个类似Vscode的代码编译器自带AI新账号有免费额度 https://cursor.com/cn/download或者选择下载trae也有免费额度TRAE - Collaborate with Intelligence或者在Vscode 安装 Condex插件1.安装2.注册账号3.登录4.换成高亮;个人习惯5.有编码模式/ai agent 模式选择编码模式6.新建项目2. 创造python环境/cursor终端环境1cursor终端环境安装python插件 打开python terminal cursor选择终端cursor选择环境2python环境创建虚拟环境隔离项目、避免不同项目使用不同版本的依赖导致的环境冲突的问题这里创建conda环境可以直接在cmd中创建和激活然后到cursor终端选择环境 :创建环境conda create -n cursor_pytest_autotest python3.11激活环境conda activate cursor_pytest_autotest3配置python环境安装部分依赖pip install pytest pip install https pip install requests pip install python-dotenv3. 手动创建目录创建文件夹/文件的指令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内容追加到24. pytest框架知识点pytest相关文件的命名规范文件名必须以test_开头或者_test结尾测试类必须以test开头并且不能有__init__方法测试方法必须以test开头pytest 的装饰器decoratorpytest.fixturefixture的scope级别#准备测试需要的东西 pytest.fixture(scopesession) #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.pyimport 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_urlbase_url, timeouttimeout, ) 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, domaindomain, pathpath, ) def get(self, path: str, params: dict | None None) - httpx.Response: return self._client.get(path, paramsparams) def post( self, path: str, json: dict | None None, data: dict | None None, ) - httpx.Response: return self._client.post(path, jsonjson, datadata) def close(self): self._client.close()conftest.py# base_url配置到环境变量里或从配置文件获取 pytest.fixture(scopesession) 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_urlbase_url) yield client client.close() # 直接用从网站上获取的cookie f12 Application / Storage / cookie / url: ... ... # 配置到环境变量里从环境变量里面获取 pytest.fixture(scopesession) 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, domaindomain, path/, ) api_client.set_cookie( token_pass, token_pass, domaindomain, path/, ) api_client.set_cookie( JSESSIONID, jsessionid, domaindomain, path/, ) return api_clientassertions.pyfrom 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, ( fHTTP status mismatch: expected {expected_status}, got {resp.status_code}. fbody: {resp.text[:500]} ) body resp.json() assert errorCode in body, fMissing errorCode in response: {body} assert errorMsg in body, fMissing errorMsg in response: {body} if expected_error_code is not None: assert body[errorCode] expected_error_code, ( ferrorCode mismatch: expected {expected_error_code}, fgot {body[errorCode]}. errorMsg: {body.get(errorMsg)} ) if data_keys: for k in data_keys: assert key_exists_anywhere(body, k), ( fExpected 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, ( fpath {path} mismatch: expected {expected!r}, got {actual!r} ) return bodyutils.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(encodingutf-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