利用 Python 脚本执行 Excel 中的接口测试(Windows & macOS + VS Code 全流程)
目标:用 Excel 维护接口用例(方法、路径、断言、提取等),用 pytest 执行,用 HTML 报告查看是否成功、是否达到预期值。
环境:Windows 10/11 或 macOS、Python 3.10+、VS Code(安装 Python 插件)。
目录
- 准备与安装
- 创建项目结构
- 创建虚拟环境并安装依赖(Windows 与 macOS)
- 把代码与表格放到项目里
test_from_excel.py(当前版本,含注释)conftest.py(当前版本,含注释)requirements.txt- (可选)
pytest.ini - (可选)
apis.xlsx表头与示例
- 运行测试与生成 HTML 报告
- VS Code 配置与一键运行
- 设置环境变量(Windows & macOS)
- 常见问题与排错
- 下一步:扩展断言/提取与并行执行
准备与安装
-
Python 3.10+
- Windows:到 python.org 下载并安装,勾选 “Add Python to PATH”。
验证:py --version python --version - macOS:推荐用 Homebrew 安装:
brew install python # 未安装 Homebrew: https://brew.sh python3 --version
- Windows:到 python.org 下载并安装,勾选 “Add Python to PATH”。
-
VS Code + 扩展:
- Microsoft Python 扩展(ms-python.python)
- (可选)Pylance、Python Test Explorer
创建项目结构
在任意目录(Windows 例:D:\work;macOS 例:~/work)新建项目 excel-api-test:
excel-api-test/
├─ conftest.py
├─ test_from_excel.py
├─ apis.xlsx # Excel 用例表(工作表名:cases)
├─ requirements.txt
└─ pytest.ini # 可选:固定开启 HTML 报告等
也可先只放
*.py与requirements.txt,第 5 步前再放apis.xlsx。
创建虚拟环境并安装依赖(Windows 与 macOS)
Windows(PowerShell):
cd D:\work\excel-api-test
# 1) 创建并激活虚拟环境
py -3 -m venv .venv
.\.venv\Scripts\activate
# 2) 升级 pip(建议)
python -m pip install -U pip
# 3) 安装依赖
pip install -r requirements.txt
macOS(zsh/bash):
cd ~/work/excel-api-test
# 1) 创建并激活虚拟环境
python3 -m venv .venv
source .venv/bin/activate
# 2) 升级 pip(建议)
python -m pip install -U pip
# 3) 安装依赖
pip install -r requirements.txt
激活成功后,命令行前缀会显示
(.venv);退出虚拟环境:deactivate。
把代码与表格放到项目里
test_from_excel.py
import json # 把单元格中的 JSON 字符串转成 Python 对象
import math # 用来判断 NaN(空单元格时 pandas 可能给出 NaN)
import requests # 发送 HTTP 请求
import pytest # 做参数化
import pandas as pd # 读取 Excel
from jsonpath_ng import parse as jp # JSONPath 解析(从 JSON 响应里取字段)
# 约定的 Excel 文件名和工作表名
EXCEL_FILE = "apis.xlsx"
SHEET_NAME = "cases"
def _parse_cell(s):
"""
把 Excel 单元格里的内容转成“合适的 Python 类型”:
- 空/NaN/空白 -> None
- 否则优先按 JSON 解析(如 '{"cid":"294"}' -> dict)
- 如果不是合法 JSON,就当作普通字符串返回(后面还能做 {变量} 替换)
"""
# 判空:None、NaN、空字符串都当 None
if s is None or (isinstance(s, float) and math.isnan(s)) or str(s).strip() == "":
return None
# 统一成字符串并去空白,尝试按 JSON 解析
s = str(s).strip()
try:
return json.loads(s) # 成功:得到 dict/list/数值/布尔 等
except Exception:
return s # 不是合法 JSON:作为普通字符串返回
def _extract_to_ctx(ctx, resp_json, extract_spec):
"""
按 extract 规则,从响应 JSON 里提取字段 -> 存到变量背包 ctx
- extract_spec 形如:{"aid":"$.data.datas[0].id"}
- 用 JSONPath 找到第一个匹配值,保存为 ctx["aid"]
"""
for var_name, jpath in extract_spec.items():
matches = list(jp(jpath).find(resp_json)) # 在 resp_json 里执行 JSONPath 查询
if matches:
ctx[var_name] = matches[0].value # 取第一个匹配结果的值存入 ctx
# 若取不到,可按需要改为强校验:assert matches, f"{var_name} not found by {jpath}"
def _check_jsonpath(resp_json, path_expr, expected, ctx):
"""
做 JSON 字段断言:
- path_expr:JSONPath 表达式(例如 $.errorCode)
- expected :期望值;可以是具体值/字符串(支持 {变量} 占位)/特殊标记
* "!null" -> 断言“能取到且不为 null/None”
* "!exists?false" -> 断言“允许不存在,或存在但值为 False”
"""
matches = list(jp(path_expr).find(resp_json)) # 执行 JSONPath,拿到匹配项列表
if expected == "!null":
# 要求:有匹配项,且值不是 None
assert matches and matches[0].value is not None, f"{path_expr} is null or missing"
return
if expected == "!exists?false":
# 允许:没有这个字段;或有这个字段但为 False(空/0/False)
assert (matches and bool(matches[0].value) is False) or not matches, f"{path_expr} exists and truthy"
return
# 如果期望是字符串,允许里面写 {变量}(例如 "{aid}");用 ctx.apply 替换
if isinstance(expected, str):
expected = ctx.apply(expected)
actual = matches[0].value if matches else None # 实际取到的值(取不到就当 None)
assert actual == expected, f"jsonpath {path_expr}: expected {expected}, got {actual}"
def load_cases():
"""
从 Excel 读取所有用例 -> 生成“用例字典列表”
同时把 'times'(执行次数)展开:例如某行 times=3 -> 复制成 3 条用例
"""
df = pd.read_excel(EXCEL_FILE, sheet_name=SHEET_NAME) # 读 Excel 的 cases 工作表
cases = []
for _, row in df.iterrows(): # 遍历每一行
case = {
"case": row.get("case"), # 用例名
"method": str(row.get("method", "GET")).upper(), # 方法(默认 GET,转大写)
"path": row.get("path"), # 路径(与 base_url 拼接)
"headers": _parse_cell(row.get("headers")), # 请求头(JSON 或空)
"query": _parse_cell(row.get("query")), # 查询参数(JSON 或空)
"body": _parse_cell(row.get("body")), # 请求体(JSON 或空)
"expected_status": int(row.get("expected_status", 200)), # 期望 HTTP 状态码(默认 200)
"expect_jsonpath": row.get("expect_jsonpath"), # JSONPath 断言字段
"expect_value": _parse_cell(row.get("expect_value")), # 断言的期望值
"extract": _parse_cell(row.get("extract")) or {}, # 需要从响应里提取的变量
"times": int(row.get("times", 1)), # 重复执行次数(默认 1)
}
cases.append(case) # 收集
# 把 times>1 的用例展开成多条(名字后加 #1/#2/... 方便在报告里区分)
expanded = []
for c in cases:
for i in range(c["times"]):
tag = f'{c["case"]}#{i+1}' if c["times"] > 1 else c["case"]
expanded.append({**c, "case": tag})
return expanded
# 模块加载时就把所有用例读出来,供参数化使用
_ALL_CASES = load_cases()
# 参数化:_ALL_CASES 有多少条,就会生成多少个独立的测试用例
# ids 指定每个用例在报告里显示的名字(这里用 Excel 里的 case 字段)
@pytest.mark.parametrize("c", _ALL_CASES, ids=lambda c: c["case"])
def test_by_excel(c, base_url, ctx):
"""
核心测试函数:
- 拼 URL(base_url + path)
- 在发请求前,对 headers/query/body 做 {变量} 替换
- 发送请求并断言 HTTP 状态码
- 若配置了 JSON 断言或提取,则解析 JSON 并执行对应逻辑
"""
# 拼接完整 URL:base_url(来自 conftest.py 的 fixture) + path(来自 Excel)
url = base_url + c["path"]
# 在真正发请求前,先把 headers/query/body 里的 {变量} 都替换成真实值
headers = ctx.apply(c["headers"]) if isinstance(c["headers"], (dict, list, str)) else c["headers"]
query = ctx.apply(c["query"]) if isinstance(c["query"], (dict, list, str)) else c["query"]
body = ctx.apply(c["body"]) if isinstance(c["body"], (dict, list, str)) else c["body"]
# 发送 HTTP 请求:
# - 只有当 headers/query/body 是 dict 时才传入(避免把字符串误传)
# - 这里把 body 当 JSON 发送(入门版这样就够了)
resp = requests.request(
method=c["method"],
url=url,
headers=headers if isinstance(headers, dict) else None,
params=query if isinstance(query, dict) else None,
json=body if isinstance(body, dict) else None,
timeout=15
)
# 断言 HTTP 状态码(例如 200);失败时把响应前 300 字符打印出来便于定位
assert resp.status_code == c["expected_status"], (
f"{c['case']} -> {resp.status_code}\n{resp.text[:300]}"
)
# 只有在需要 JSON 断言或需要提取变量时,才把响应当 JSON 解析(避免无意义解析)
if c["expect_jsonpath"] or c["extract"]:
data = resp.json()
# 如果配置了 JSONPath 断言,就检查取到的值是否等于期望值(或满足 !null 等规则)
if c["expect_jsonpath"]:
_check_jsonpath(data, c["expect_jsonpath"], c["expect_value"], ctx)
# 如果配置了 extract,就把响应里的值提取到 ctx 里供后续用例使用
if c["extract"]:
_extract_to_ctx(ctx, data, c["extract"])
conftest.py
import os # 读取环境变量用(例如 BASE_URL、TEST_username 等)
import pytest # pytest 的夹具(fixture)机制要用到
from collections import UserDict # 可扩展的字典基类,和 dict 类似但更方便自定义
# 从环境变量里拿 BASE_URL;如果没有,就用默认的 WanAndroid 域名(官方已全面 https)
BASE_URL = os.getenv("BASE_URL", "https://www.wanandroid.com")
class Ctx(UserDict):
"""
变量上下文(像一个“变量背包”):
- 本质上就是一个字典:可以存 {"token": "...", "dev": "..."} 等变量
- 提供 apply() 方法:把对象里出现的 {变量名} 替换成背包里同名变量的值
例如:字符串 "Bearer {token}" -> 如果 self["token"]="abc",则替换为 "Bearer abc"
"""
def apply(self, obj):
# 如果传进来的是 dict,就把 key、value 都递归替换(因为 key 里也可能出现 {变量})
if isinstance(obj, dict):
return {self.apply(k): self.apply(v) for k, v in obj.items()}
# 如果是 list,就对列表里的每一个元素递归替换
if isinstance(obj, list):
return [self.apply(x) for x in obj]
# 如果是字符串,就用 Python 的 str.format(**self) 做占位符替换
# 比如:obj="Hello {name}",而 self={"name":"Edy"} -> "Hello Edy"
if isinstance(obj, str):
return obj.format(**self)
# 其它类型(int/bool/None…)保持原样返回
return obj
@pytest.fixture(scope="session")
def ctx():
"""
提供一个“会话级”的上下文对象(变量背包):
- scope=session 表示整个测试会话(一次 pytest 运行)只创建一次
- 支持把环境变量 TEST_xxx 注入为 ctx['xxx']
例如:$env:TEST_dev="abc123" -> Excel 可用 {dev}
"""
c = Ctx()
for k, v in os.environ.items():
if k.startswith("TEST_"):
# 去掉 TEST_ 前缀,统一存小写
c[k[5:].lower()] = v
return c
@pytest.fixture(scope="session")
def base_url():
"""把统一的接口前缀暴露给测试使用(可被 env 覆盖)"""
return BASE_URL
requirements.txt
pytest
requests
pandas
openpyxl
jsonpath-ng
pytest-html
pytest-xdist
pytest.ini (可选)
固定每次都输出 HTML 报告:
[pytest]
addopts = -v --html=report.html --self-contained-html
apis.xlsx 表头与示例
工作表名:cases。常用列:
| case | method | path | expected_status | expect_jsonpath | expect_value | headers | query | body | extract | times |
|---|
示例(WanAndroid 常用 GET 接口):
| case | method | path | expected_status | expect_jsonpath | expect_value |
|---|---|---|---|---|---|
| 首页文章列表-第0页 | GET | /article/list/0/json | 200 | $.errorCode | 0 |
| 首页banner | GET | /banner/json | 200 | $.errorCode | 0 |
| 常用网站 | GET | /friend/json | 200 | $.errorCode | 0 |
如果需要多次运行同一条,把
times填 3 就会展开成 3 条用例。
运行测试与生成 HTML 报告
Windows(PowerShell):
# 基本运行(若已在 pytest.ini 中配置 HTML)
pytest -v
# 显式生成 HTML 报告
pytest -v --html=report.html --self-contained-html
# 想在报告中也看到 print 输出:
pytest -v -s --capture=tee-sys --html=report.html --self-contained-html
# 跑完直接用默认浏览器打开
start report.html
macOS(zsh/bash):
# 基本运行(若已在 pytest.ini 中配置 HTML)
pytest -v
# 显式生成 HTML 报告
pytest -v --html=report.html --self-contained-html
# 想在报告中也看到 print 输出:
pytest -v -s --capture=tee-sys --html=report.html --self-contained-html
# 打开报告
open report.html
只跑某些用例:
pytest -v -k banner
并行跑(更快):
pytest -v -n auto
VS Code 配置与一键运行
- 选择解释器:
Ctrl+Shift+P→ “Python: Select Interpreter”- Windows 选:
.\.venv\Scripts\python.exe - macOS 选:
./.venv/bin/python
- Windows 选:
- 打开 Testing 面板(左侧小烧杯),VS Code 会自动发现
pytest,点 ▶️ 运行即可。 - (可选)
.vscode/settings.json:
{
"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false,
"python.testing.pytestArgs": [
"-v",
"--html=report.html",
"--self-contained-html",
"--capture=tee-sys",
"-s"
]
}
设置环境变量(Windows & macOS)
如果 Excel 用到了占位符(在 headers/query/body/expect_value 里写了 {username} 之类),就需要给 ctx 提供变量。
Windows(PowerShell)—临时:
$env:BASE_URL = "https://www.wanandroid.com"
$env:TEST_username = "your_user"
$env:TEST_password = "your_pass"
pytest -v
Windows(永久,用户变量):
setx BASE_URL "https://www.wanandroid.com"
setx TEST_username "your_user"
setx TEST_password "your_pass"
# 重新打开一个新终端生效
macOS(zsh)—临时:
export BASE_URL="https://www.wanandroid.com"
export TEST_username="your_user"
export TEST_password="your_pass"
pytest -v
macOS(永久): 将上述 export 写进 ~/.zshrc,然后 source ~/.zshrc。
常见问题与排错
-
ModuleNotFoundError: ...
没在虚拟环境里:先激活.venv再安装依赖。 -
ValueError: Excel file format cannot be determined
确认文件是.xlsx,且工作表名是cases。Apple Numbers 请导出为 Excel。 -
状态码通过,但业务失败
在 Excel 填上:expect_jsonpath=$.errorCode,expect_value=0,就能断业务成功。 -
看不到 print 输出
用-s或--capture=tee-sys:
pytest -v -s --capture=tee-sys --html=report.html --self-contained-html。 -
需要严格:提取不到也判失败
把_extract_to_ctx里加assert matches, ...(上面代码注释已给出)。 -
代理/证书问题
Windows:setx HTTPS_PROXY "http://proxy:port";macOS:export HTTPS_PROXY="http://proxy:port"。
下一步:扩展断言/提取与并行执行
- 更多断言:当前支持 1 条 JSONPath 断言;若需多断言,可扩展表头与代码。
- 链路依赖:用
extract从上一条响应取值(如aid),下一条在path/body/expect_value用{aid}。 - 并行:
pytest -n auto;大量用例时显著提速。 - CI 定时跑:把同一套命令放进 GitHub Actions、Jenkins 等。
到这里,你就能在 Windows 或 macOS + VS Code 下,从 0 到 1 跑起“Excel 数据驱动的接口自动化”,并把结果产出为一个可追溯的 HTML 报告。