微软生成式 AI 入门课的测试跑不过:先看 conftest 与 fixture
先把话说在前面:下面这些结论全部是从 generative-ai-for-beginners 仓库的文件本身读出来的结构性事实,我们没有执行过这套测试,所以文中不会出现任何关于耗时、通过率之类的描述。这篇要解决的是另一类问题——你看到报错以后,去仓库里的哪几个文件对照,就能判断是哪一环出了岔子。
现象:pytest tests/ 的三种不同失败面貌
仓库根目录下确实有 tests/ 目录,里面是 conftest.py、test_api_utils.py、test_env_utils.py、test_input_validation.py。.github/workflows/code-quality.yml 里有一个名为 Python Tests 的 job,最后一步逐字写的是:
pytest tests/
但同一个仓库的 AGENTS.md 里有一节标题就叫 “No Automated Tests”,正文写着这是一个教学仓库、没有单元测试或集成测试可跑,验证方式是手工跑示例、Markdown 校验和社区评审。这两处是明摆着对不上的:CI 在跑 pytest,贡献指引说没有自动化测试。知道这一点很有用——你如果照着 AGENTS.md 找测试说明,是找不到的,得去 workflow 文件和 pyproject.toml 里找。
常见的失败形态大致分三种:命令本身找不到;收集阶段就报 import 错误;报 shared 这个包找不到。这三种对应的根因完全不同,别混着治。
第一处要看的:依赖清单里有没有 pytest 和 requests
课程的本地环境步骤在 00-course-setup/02-setup-local.md,它给的安装命令只有一条:
pip install -r requirements.txt
而根目录的 requirements.txt 列的是 ipywidgets、numpy、matplotlib、pandas、tqdm、python-dotenv、openai、tiktoken、azure-ai-inference、scikit-learn。这份清单里既没有 pytest,也没有直接列出 requests。
再看另外两处。pyproject.toml 的 [project] dependencies 里写了 requests,[project.optional-dependencies] 下有一组名为 dev 的依赖,里面列了 pytest 与 pytest-cov。而 CI 那个 job 安装依赖的一步逐字是:
python -m pip install pytest openai requests python-dotenv
三份清单各说各的。CI 单独把这四个装了一遍,说明按 requirements.txt 装出来的环境和跑测试需要的环境不是同一个集合。至于 pip install -e ".[dev]" 这类安装 dev 组的具体命令,仓库文档里我们没有找到。
怎么确认是这一环:在激活好的虚拟环境里看已安装包里有没有 pytest 和 requests(pip list 是通用做法,不是仓库文档里的内容)。判定依据更可靠的是看报错落在哪:test_api_utils.py 文件开头的 import 段里有一句 from requests.exceptions import RequestException,这是模块顶层的无条件 import,缺了它整个文件在收集阶段就会出错;而 test_env_utils.py 与 test_input_validation.py 只 import 了 pytest 和被测模块,不带任何第三方依赖。
处置:按 CI 那一行原样装。验证:再执行 pytest tests/。
这里有个不对称,值得单独记一下。test_api_utils.py 里三个和客户端相关的用例,每个开头都有一行 pytest.importorskip("openai");而 TestMakeSafeRequest 那两个用例没有这一行,requests 也是文件顶层导入的。也就是说,缺 openai 会让那几个用例变成 skip,缺 requests 会让这个文件根本收集不起来。看到大片 skipped 别急着当失败,那是 importorskip 写在代码里的行为。
第二处:conftest.py 里其实一个 fixture 都没有注册
很多人一看到 conftest.py 就默认里面有 fixture、有钩子。这个仓库的 tests/conftest.py 全文很短,做的事只有一件:把仓库根目录插到 sys.path 的最前面。
REPO_ROOT = Path(__file__).resolve().parent.parent
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
文件开头的 docstring 是仓库自述的理由:确保仓库根目录可导入,这样测试无论从哪个工作目录运行,shared.python 这个包都能解析到。
配套的一个事实是:shared/ 目录下没有 __init__.py,只有 shared/python/__init__.py。三个测试文件的导入写法统一是 from shared.python.api_utils import ... 这种从仓库根算起的完整路径。所以一旦 sys.path 里没有仓库根,报的就是 shared 找不到,而不是缺第三方包——这是区分第一处和第二处的判据。
pyproject.toml 里还有一节 [tool.pytest.ini_options],写明 testpaths = ["tests"]、python_files = ["test_*.py", "*_test.py"]、addopts = "-v --tb=short"。这解释了两件事:在仓库根直接执行 pytest 不带路径,收集范围也还是 tests;以及输出天然是 verbose 的,不用你自己加参数。需要提醒的是,pytest 以「含配置节的那个文件所在目录」为 rootdir 是 pytest 自身的通用行为,不是这个仓库特有的设定。
什么情况说明不是 conftest 的问题:如果你能正常 import 到 shared.python,报错却出在 requests 或 pytest 本身,那和 sys.path 无关,回到第一处;如果报错是某个断言不成立,那是被测代码和用例期望对不上,也和 conftest 无关。
第三处:这套测试对环境变量的依赖,比你以为的少
这是最容易被误判的一环。很多人一看到测试里出现 OPENAI_API_KEY、AZURE_OPENAI_ENDPOINT,就以为得先把 .env 配好才能跑。从代码上看不是这样。
test_api_utils.py 与 test_env_utils.py 里凡是碰环境变量的地方,用的都是 pytest 内建的 monkeypatch fixture,方向恰恰是反的:
TestCreateOpenAIClient里是monkeypatch.delenv("OPENAI_API_KEY", raising=False)——主动把它删掉,然后断言create_openai_client()抛出ValueError且消息里含 “API key”。TestCreateAzureOpenAIClient的两个用例一个删AZURE_OPENAI_ENDPOINT同时设AZURE_OPENAI_API_KEY为"test-key"(这是用例里的示例值,不是真实密钥),另一个反过来。test_env_utils.py里全是monkeypatch.setenv("MY_VAR", "hello")这类临时变量名,和你的.env没有交集。
换句话说,这几个用例测的是「缺配置时报不报错、报的错文案对不对」,不是「配置对不对」。monkeypatch 会在用例结束时还原它改过的键,这是 pytest 提供的能力。
还有一处容易忽略:tests/conftest.py 里没有调用 load_dotenv,shared/python/env_utils.py 和 shared/python/api_utils.py 里也都只有 os.getenv,没有任何 dotenv 相关的调用。所以你根目录那个 .env 文件对这套测试是不生效的——配了没用,没配也不影响。
什么情况说明不是环境变量的问题:如果报的是 401 或 429,那基本可以排除 tests/ 这套,因为它们不打真实接口(下一节说结构);出现这类错误说明你跑的是课程各章 python/ 目录下的示例脚本,那些确实要凭据,排错请看 00-course-setup/02-setup-local.md 第 5 节的表格,里面 OpenAI 401 / 429 errors 那一行给的处置是检查 OPENAI_API_KEY 的值与请求速率限制。
顺带说一句变量名:.env.copy 里 Microsoft Foundry Models 那一段的注释逐字写明,它取代的是将于 2026 年 7 月底退役的 GitHub Models。所以课程里那些 githubmodels- 前缀的示例文件,前缀名和实际要接的目标已经脱节了,真正要配的是 AZURE_INFERENCE_ENDPOINT 与 AZURE_INFERENCE_CREDENTIAL。同一个文件里还有一条注释写明 gpt-5-mini 属于 reasoning 模型,不支持 temperature / top_p,且用 max_output_tokens 而不是 max_tokens。这两条都和 tests/ 无关,但排查配置时会撞到。
第四处:断网环境下这套测试的结构
把 tests/ 里所有可能出网的点数一遍:
TestMakeSafeRequest 的两个用例都有这么一行——
monkeypatch.setattr("shared.python.api_utils.requests.request", fake_request)
真正的请求函数被替换成了本地定义的 fake_request,一个返回假响应对象,一个直接抛 RequestException。三个客户端用例走的都是「缺 endpoint 或缺 key 就抛 ValueError」的分支——对照 shared/python/api_utils.py 可以看到,create_openai_client 里 OpenAI(api_key=key) 那一行排在 key 检查之后,检查不过就抛出去了,构造调用根本轮不到。test_env_utils.py 和 test_input_validation.py 里全是纯函数校验。
所以从代码结构上看,tests/ 目录里没有对外发起真实请求的路径。另外,shared/python/api_utils.py 里的 download_image 会真正下载文件,但 tests/ 里没有它的用例。
这里再补一个对照着读才看得出来的细节:test_returns_response_on_success 里的 fake 函数体第一行是 assert timeout == 30,硬编码了 30;而 make_safe_request 的签名是 timeout: int = 30, retries: int = 3。这是仓库当前代码里的默认值,随版本可能变动——真要动这个默认值,测试里那个断言得跟着改,不然它会先炸给你看。
Windows 侧的两点差异
00-course-setup/02-setup-local.md 第 2 步给的虚拟环境激活命令是分开写的,macOS / Linux 用 source .venv/bin/activate,Windows PowerShell 用 .\.venv\Scripts\activate。没激活就装依赖,是「装了但 pytest 找不到」这类现象最常见的来源之一。
同一文档第 5 节的排错表里有一行是 Windows 专属的:pip 在 Windows 上无法构建 wheel 时,处置是 pip install --upgrade pip setuptools wheel 之后重试。
以上命令均照抄自仓库文件,未经实测,以仓库最新内容与 --help 的实际输出为准。
顺手记一个不一致
test_input_validation.py 从 shared.python.input_validation 直接导入了 validate_email 与 validate_url,这两个函数在该模块里确实定义了。但 shared/python/__init__.py 的 from .input_validation import 只引入了 sanitize_prompt_input、validate_number_input、validate_text_input 三个,__all__ 里也没有前面那两个。测试走的是子模块完整路径,所以不受影响;但你若照着包级导出去写 from shared.python import validate_email,路径是不通的。这两处放在一起就是这个关系,至于是有意还是遗漏,仓库里没有找到相关说明。
本文依据 github.com/microsoft/generative-ai-for-beginners 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码,
因此不涉及运行结果、耗时与生成质量的任何描述。
该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。
文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。