做Python测试这块的朋友应该都经历过从unittest迁移到pytest的过程。我刚转pytest那会儿心里只有一个想法为什么不早点换。断言不用再记一堆assertEqual、assertTrue、assertIn一个原生的assert就能搞定所有场景fixture机制把前置条件和数据清理安排得明明白白参数化让一条用例能覆盖十几种组合配合插件体系报告、重试、并行执行都是装个包的事。这篇就按我实际项目里从零搭建pytest测试体系的完整经验来写从安装、断言、fixture、参数化到接口自动化项目组织、报告输出和常见坑位一次说透。无论你是刚入行想给代码加测试的Python开发者还是已经在unittest里挣扎了挺久的测试工程师这篇文章应该都能给你一些可以直接落地的思路。1. 为什么是pytest——从unittest迁移的真实感受1.1 断言方式带来的体验差异有多明显先看一组最直观的对比。用unittest写断言的时候需要记忆一整套APIimport unittest class TestLogin(unittest.TestCase): def test_login_success(self): resp do_login(admin, 123456) self.assertEqual(resp[code], 0) self.assertIn(token, resp[data]) self.assertTrue(resp[data][token])而pytest直接用Python原生断言def test_login_success(): resp do_login(admin, 123456) assert resp[code] 0 assert token in resp[data] assert resp[data][token]如果你只是把断言写法从self.assertEqual(a, b)换成assert a b那确实只是省了两行字。但pytest真正厉害的地方在于它对断言语句做了AST改写。当断言失败的时候它会告诉你具体差异而不是冷冰冰地抛出一个AssertionError。比如这样一段代码def test_response(): resp {code: 404, msg: user not found} assert resp[code] 200pytest实际打印出来的信息是这样的 assert resp[code] 200 E assert 404 200左边404是实际的响应码右边200是期望值。如果对比的是字典、列表这种复杂结构pytest甚至会递归展开把不相等的部分一行行列出来。在排查接口测试失败原因时这个能力省掉了我大量打断点、加日志的时间。1.2 用例发现机制与组织形式更灵活unittest的规矩比较多测试类要继承unittest.TestCase用例方法要以test开头运行起来要自己拼TestSuite或者靠unittest.main()去发现。当然unittest也支持简单的函数式测试写法但整体组织逻辑仍然倾向于类。pytest的约定则简化了很多测试文件命名规则test_*.py或者*_test.py测试函数以test_开头测试类以Test开头且类中不需要继承任何基类测试方法以test_开头我刚才说不需要继承任何基类这一点对写测试的人来说体验差异非常大。因为不强制继承我就完全可以根据业务语义去组织代码。有时候一连串接口测试逻辑上就是一组函数我用函数写就很简洁当某个模块确实需要共享一套类级别的数据时我再改成Test类。这个自由度的提升会让测试代码的整体设计更贴合被测系统的实际结构。另外pytest的用例发现是递归的默认从当前目录往下扫描所有符合规则的文件省了手工维护测试集。1.3 插件生态这是pytest能成为事实标准的核心原因pytest的插件体系成熟到什么程度我常用的几个插件名作用pytest-html生成HTML测试报告pytest-xdist多进程并行执行用例pytest-rerunfailures失败用例自动重试pytest-cov统计代码覆盖率pytest-order自定义用例执行顺序pytest-sugar彩色进度条失败信息更友好pytest-mock集成mock替代unittest.mock的繁琐写法allure-pytest生成Allure报告这些插件全部通过pip install安装在pytest.ini或命令行里指定参数即可生效基本不需要改测试代码。对比unittest那套相对封闭的生态维护成本完全不在一个量级。所以我的结论很直接如果你的新项目还没有选定测试框架或者你正在用unittest而觉得别扭直接切pytest就行。迁移成本很低我当时的做法是把self.assertEqual(a, b)这种方式机械地改成assert a b跑一遍再把类继承去掉改成pytest风格一上午就完成了。2. 装上就能跑环境准备与第一个用例2.1 安装与基本运行pytest的安装没有坑正常Python环境都支持pip install pytest pytest --version我习惯在虚拟环境里操作避免系统Python环境被污染。这个不多说了属于常规操作。运行执行文件的命令也很简单pytest # 递归查找当前目录下所有符合条件的用例 pytest test_demo.py # 执行指定文件 pytest test_demo_py -k add # 按名称关键字筛选 pytest test_demo_py -m slow # 按mark标记筛选 pytest -x # 遇到第一个失败即停止 pytest --maxfail3 # 最多允许3个失败 pytest -q # 静默输出只显示最终结果 pytest -v # 详细模式显示每个用例及其结果 pytest -s # 显示print输出 pytest --lf # 只重跑上次失败的用例从实际体验来说最常用的组合是pytest -v -s --lf。尤其是--lf在修复bug之后想快速验证失败的用例是否通过非常高效。2.2 第一个用例长什么样创建一个test_demo.pydef test_add(): assert 1 1 2 def test_string_upper(): assert hello.upper() HELLO class TestOrder: def test_create_order(self): order_id create_order() assert order_id is not None执行pytest test_demo.py -v输出结果中会有每个用例的执行状态。三个用例全部通过PASSED显示绿色。如果你故意让其中一个断言失败比如把assert 1 1 2改成assert 1 1 3运行时的信息就会展示详细差异test_demo.py:1: in test_add assert 1 1 2 E assert (1 1) 31 1的结果被直接求出来跟3做对比。这种直观的失败信息能帮你快速定位问题尤其在处理复杂表达式的时候不用自己去算。2.3 目录结构约定pytest的自动发现机制是基于目录递归的。实际项目的测试目录结构我一般这样组织project/ ├── src/ # 被测代码 │ └── my_app/ ├── tests/ # 测试代码 │ ├── conftest.py # 公共fixture │ ├── test_user.py │ ├── test_order.py │ └── test_api/ │ ├── test_login.py │ └── test_register.py ├── pytest.ini # 配置文件 ├── requirements.txt └── README.md注意pytest的目录组织比unittest更宽松你可以在tests目录下建子目录只要文件名符合test_*.py规则即可。不过实际工作中我建议保持被测应用与测试代码的目录比例一致这样查找用例和管理数据都方便。在pytest.ini里还可以指定路径、测试发现规则等[pytest] testpaths tests python_files test_*.py *_test.py python_functions test_*配置文件的好处是如果你的测试文件命名不是标准的test开头通过python_files可以声明自定义规则。团队协作时统一用配置文件能少很多争议。2.4 我在探索阶段的三个小心得刚开始用pytest最容易忽略的是-s参数。pytest默认会捕获print输出用例通过的时候看不到任何print内容这会让人误以为代码没执行。我建议刚上手阶段直接打开-s看一下print输出是否正常摸排完再关掉。日志也一样配合--log-cli-levelDEBUG可以直接在终端看到日志输出调试定位问题非常方便。还有一点pytest的断言失败信息并非对所有表达式都完美。比如assert a b这种简单等值判断没问题但如果你写的是assert any(...)它的错误信息可能只有True/False不够直观。这种情况下我通常会拆成多个断言或加注释别指望pytest智能到能解析所有复杂表达式。3. fixture机制前置条件与数据清理的正确姿势3.1 一个典型的场景登录态复用写过接口自动化的人都知道大部分接口都要求有登录态。如果每个测试函数里都去调一次登录接口接口要被打爆不说用例还会变慢。fixture的典型场景就是干这个把公共的前置条件统一管理起来。import pytest import requests BASE_URL https://api.example.com pytest.fixture def login_token(): resp requests.post(f{BASE_URL}/login, json{ username: admin, password: 123456, }) assert resp.status_code 200 return resp.json()[token] def test_get_user_info(login_token): headers {Authorization: fBearer {login_token}} resp requests.get(f{BASE_URL}/user/info, headersheaders) assert resp.status_code 200在这个示例中login_token这个fixture返回了登录接口的tokentest_get_user_info函数的参数名login_token与fixture函数名一致pytest会自动完成依赖注入。这就是fixture最核心的价值把准备数据和测试逻辑解耦。测试函数自己只关心业务断言前置条件的组装交给fixture去处理。3.2 yield兼顾前置清理与后置操作有些前置条件不仅需要创建还需要清理。比如创建了一个临时数据库测试完要删掉创建了一张测试订单测完要取消。这时候就用yield写法pytest.fixture def temp_order(): order create_order() yield order cancel_order(order[id])fixture执行到yield的时候把order返回给测试函数。测试函数执行完之后fixture会继续往下走到cancel_order完成清理。这种写法逻辑非常清晰对比unittest的setUp和tearDownpytest的fixture把资源创建和资源释放放在同一个函数里阅读代码的时候不用来回跳。特别是在嵌套多个模块的时候yield能保证释放顺序是与创建顺序相反的后进先出和上下文管理器的行为一致。3.3 scope控制fixture的生命周期fixture默认每个函数执行一次称为function作用域。但有些资源的创建开销很大比如登录态、数据库连接、读取大文件希望在整个测试阶段复用。pytest提供了几个scope选项scope值生命周期function每个测试函数执行一次默认值class每个测试类执行一次module每个测试模块执行一次session整个测试会话只执行一次我通常在接口自动化项目里把登录token的fixture设成sessionpytest.fixture(scopesession) def login_token(): resp requests.post(f{BASE_URL}/login, json{...}) return resp.json()[token]这样整个测试过程只登录一次后续所有用例都复用同一个token执行效率提升很明显。但也要注意如果token有效期很短或session级别的token被某一个用例修改了后续用例就会踩坑。我后面会专门展开讲这一类问题。3.4 conftest.py共享fixture的载体fixture单独放在某个测试文件里其他文件用不了。要全局共享需要把fixture放到conftest.py中。conftest.py是pytest的特色文件pytest会自动发现它并把其中定义的fixture注入到同目录及子目录的所有测试模块中。注意我没有说需要import这是conftest.py最方便的地方只要文件在目录树中pytest就能找到它测试函数直接把它当作参数使用即可。举个实际项目中的例子我的conftest.py里一般放login_tokensession级所有接口用例都要用load_test_data读取测试数据文件client封装好的请求客户端delete_test_user测试结束之后删除脏数据3.5 autouse自动应用但要慎用autouseTrue参数可以让fixture不经过参数声明自动应用到所有符合scope条件的用例上。pytest.fixture(autouseTrue) def setup_environment(): # 每个用例执行前的公共初始化 setup_env() yield teardown_env()这个功能很方便适合做全局的日志、临时目录、mock环境等。但我的建议是非必要不使用autouse。原因很现实它把隐式依赖引入了测试代码——其他同事看到用例定义时根本不知道有个fixture在执行这会增加排查问题成本。如果某个用例依赖了autouse fixture修改的全局状态用例失败时你得翻遍所有conftest才能找到原因。如果只是少数几个用例需要统一前置我更倾向于显式传参代码可读性高很多。4. 参数化一条用例跑出N种场景4.1 基础参数化的写法最典型的场景就是登录接口不同用户名、密码组合期望得到不同结果。如果不用参数化得写一大串重复的测试函数维护成本极高。用pytest.mark.parametrize就能在一个函数里覆盖多种组合import pytest def do_login(username, password): # 模拟登录逻辑 if username admin and password 123456: return {code: 0} return {code: 400} pytest.mark.parametrize(username,password,expected_code, [ (admin, 123456, 0), (admin, wrong, 400), (guest, 123456, 400), (, , 400), ]) def test_login(username, password, expected_code): resp do_login(username, password) assert resp[code] expected_code执行的时候pytest会生成4条独立的用例记录。哪一组参数挂了报告里能直接看到是哪个组合。4.2 用ids参数让报告不再是一堆编号默认情况下参数化用例的ID是参数值的序列化结果比如test_login[admin-123456-0]。当参数比较复杂时这一串东西在报告里非常难看。用ids参数自定义用例名pytest.mark.parametrize(username,password,expected_code, [ (admin, 123456, 0), (admin, wrong, 400), ], ids[正确账号密码, 错误密码]) def test_login(username, password, expected_code): ...执行输出会显示test_login[正确账号密码]语义清晰很多。尤其是向非技术同事展示报告时这一步很重要。4.3 参数化与fixture组合indirect的巧妙用法如果参数化里的值不是普通数据而是需要从fixture动态获取的对象可以直接配indirectTruepytest.fixture def user(request): username request.param return create_user(username) pytest.mark.parametrize(user, [admin, guest], indirectTrue) def test_user_role(user): assert user.role in [admin, guest]这样user参数会被当作fixture来调用request.param接收参数化中传入的值fixture返回的user对象再传给测试函数。这种写法适合复杂场景参数化提供某种输入fixture负责把这些输入转换成测试需要的数据形态业务逻辑不出现在测试函数中。4.4 从外部数据文件读取参数化数据接口自动化做久了测试数据经常需要增改。把数据写死在代码里每次改数据都要动测试文件不利于协作。我一般把测试数据和代码分离放到YAML或JSON文件里。读取方式用一个fixture来封装import json pytest.fixture(scopesession) def load_cases(): def _load(file, key): with open(fdata/{file}.json, r, encodingutf-8) as f: data json.load(f) return data[key] return _load然后在测试文件中import pytest with open(data/login_cases.json, r, encodingutf-8) as f: login_cases json.load(f) pytest.mark.parametrize(case, login_cases, idslambda c: c[desc]) def test_login_from_file(case): resp do_login(case[username], case[password]) assert resp[code] case[expected_code]这里我用with open直接读取文件来定义参数而不是在fixture里读取。原因很简单pytest.mark.parametrize在收集用例阶段就要确定参数列表fixture是到执行阶段才运行的没法替代收集阶段的数据加载。所以参数数据本身必须要在模块导入时准备好。这一点比较绕新手容易搞混。我自己的经验是外部数据文件读取放在模块顶部导入时执行一次然后用parametrize引用数据量大的时候比较吃内存但接口自动化场景一般规模可控。如果想优化可以在conftest.py里定义session级fixture缓存文件内容避免每个模块都重复读文件。5. 接口自动化实战pytestrequests搭建一套能落地的项目5.1 项目结构设计做接口自动化最难的不是单个用例怎么写而是项目结构怎么组织。结构组织得好用例可维护性才会高。下面是我在实际项目中沉淀下来的一套结构仅供参考api_test_project/ ├── pytest.ini ├── requirements.txt ├── config.py # 全局配置base_url等 ├── utils/ │ └── client.py # 请求客户端封装 ├── data/ │ ├── login_cases.yaml │ └── order_cases.yaml ├── testcases/ │ ├── conftest.py # API测试相关的fixture │ ├── test_login.py │ └── test_order.py └── reports/ # 测试报告输出目录 └── ...config.py里放的是环境相关的配置# config.py BASE_URL https://api.example.com USERNAME admin PASSWORD 123456 TIMEOUT 105.2 封装统一的请求客户端为什么一定要封装requests因为真实项目的接口请求存在大量公共逻辑统一的base_url拼接、统一的请求头、token自动注入、登录态过期自动刷新、请求日志输出。如果每个用例里直接requests.get这些逻辑会散落在上百个测试函数里后期的维护成本是灾难性的。一个简版的客户端# utils/client.py import requests from config import BASE_URL, TIMEOUT class ApiClient: def __init__(self, base_urlBASE_URL, timeoutTIMEOUT): self.base_url base_url.rstrip(/) self.timeout timeout self.token None self.session requests.Session() def set_token(self, token): self.token token def request(self, method, path, **kwargs): url f{self.base_url}/{path.lstrip(/)} headers kwargs.pop(headers, {}) if self.token: headers[Authorization] fBearer {self.token} headers[Content-Type] application/json kwargs[headers] headers kwargs[timeout] self.timeout return self.session.request(method, url, **kwargs) def get(self, path, **kwargs): return self.request(GET, path, **kwargs) def post(self, path, **kwargs): return self.request(POST, path, **kwargs)这里我用requests.Session()而不是直接requests.get好处是会话内可以复用TCP连接显著提升大量请求时的速度。封装完成后每个用例的代码就可以很简洁def test_get_user_info(client): resp client.get(/user/info) assert resp.status_code 200 assert resp.json()[code] 0这样测试函数只关心业务断言底层请求细节全部由client负责。5.3 用fixture管理client与登录态这里我定义一个clientfixture并做成session级复用# testcases/conftest.py import pytest from utils.client import ApiClient from config import BASE_URL, USERNAME, PASSWORD pytest.fixture(scopesession) def client(): client ApiClient(BASE_URL) resp client.post(/login, json{ username: USERNAME, password: PASSWORD, }) token resp.json()[data][token] client.set_token(token) return client这个fixture第一次被调用时会实例化ApiClient并调用登录接口获取token后续所有测试函数在请求参数中写上client就能拿到同一个带token的客户端实例。这种方式比每个用例写一段登录逻辑要清爽得多执行速度和可维护性都是质变。5.4 数据驱动把测试数据和执行逻辑分离接口测试的本质是输入-输出的校验天然适合数据驱动。我把每条用例的参数和期望值放在YAML文件中# data/order_cases.yaml test_create_order: - desc: 正常创建订单 payload: product_id: 1001 quantity: 2 expected: code: 0 amount: 200.0 - desc: 商品数量不能为0 payload: product_id: 1001 quantity: 0 expected: code: 40001在测试文件中通过一个辅助函数读取import pytest import yaml def load_yaml(file_path): with open(file_path, r, encodingutf-8) as f: return yaml.safe_load(f) order_cases load_yaml(data/order_cases.yaml)[test_create_order] pytest.mark.parametrize(case, order_cases, idslambda c: c[desc]) def test_create_order(client, case): resp client.post(/order/create, jsoncase[payload]) assert resp.status_code 200 body resp.json() assert body[code] case[expected][code] if amount in case[expected]: assert body[data][amount] case[expected][amount]这样新增一条测试用例只需要在YAML里加一条数据测试代码完全不用动。对于业务迭代频繁的项目这种数据驱动模式能让测试跟上需求变化的速度。5.5 断言设计的几个细节接口自动化里断言不只是HTTP状态码等于200这么简单我一般会分层断言断言层级检查内容作用网络层状态码200/401/500检查接口可达性业务层code字段是否符合预期检查业务逻辑正确与否数据层data里的关键字段值确认细节数据正确边界层响应时间、空值、空数组等识别接口潜在稳定性问题我的经验是每一条用例至少要覆盖网络层和业务层断言数据层断言根据具体业务来加。太细的断言会让用例脆弱太粗的断言又抓不住问题这个度需要根据项目实际情况来把控。6. 报告、重试与CI集成测试结果要有说服力6.1 pytest-html五分钟出报告跑测试不是终点把结果展示出来才是。团队里最常用的是pytest-htmlpip install pytest-html pytest --htmlreports/report.html --self-contained-html--self-contained-html参数会把CSS样式内嵌进HTML文件这样报告可以单文件发送给任何人不需要附带其他资源。pytest-html生成的报告包含用例总数、通过/失败/跳过数量、耗时、每个用例的执行状态和日志。用来做日常回归测试的交付物完全够用。6.2 allure当报告需要展示更丰富的信息时当你的测试报告需要展示更丰富的信息时我推荐用allure。allure把报告做成一个带有历史趋势、功能分组、用例步骤、附件信息的可视化页面更适合周期性展示给项目组。使用需要两步# 安装库 pip install allure-pytest # 需要额外安装allure命令行工具需要Java环境 allure --version执行测试时生成结果数据pytest --alluredirreports/allure-results然后预览报告allure serve reports/allure-resultsallure的优势在于它支持在用例上打标签按模块、功能、严重程度做分类统计。例如import allure allure.feature(登录模块) allure.story(正常登录) allure.severity(allure.severity_level.CRITICAL) def test_login_success(client): ...这样在allure报告页面上就能按feature/story/severity维度去筛选用例对于几十上百条用例的接口测试项目来说这种归类会带来很多便利。6.3 失败重试与并行执行接口测试最怕的不是逻辑挂而是偶发超时、网络抖动造成的假失败。加了重试机制后报告质量会稳定很多。pip install pytest-rerunfailures pytest-xdist pytest --reruns 2 --reruns-delay 5这段命令的意思是失败后等5秒重试最多重试2次。如果要并行执行多个测试文件pytest -n 4-n 4表示4个并行worker。在接口测试场景下并发能将整套用例执行时间从几十分钟压到几分钟。但要特别提醒并行引入了一个隐藏问题——如果你的测试里有共享状态比如某个fixture会往一个文件里写数据多个worker同时写会互相覆盖导致用例断言失败。所以启用-n之前先确认测试代码里没有共享文件、共享全局变量、共享数据库连接等问题。否则并行不但不能提速反而会让失败的用例数量飙升。6.4 接入CI让测试在每次代码变更后自动执行有了pytest之后把它接入CI其实只是写命令的问题。在GitLab CI中一个简单的job配置可以是test: stage: test script: - pip install -r requirements.txt - pytest --htmlreports/report.html --self-contained-html -n 4 artifacts: paths: - reports/ when: always在Jenkins中则是配置一个执行shell的构建步骤安装依赖后执行pytest命令然后把报告目录赋给HTML Publisher插件。接入CI的意义在于测试不再依赖某个人的手动回归每次代码merge之前自动化跑一遍提前发现破坏性变更这比事后发现要省太多时间。7. 避坑指南我在pytest项目里踩过的那些坑7.1 fixture与参数名撞车pytest通过函数的参数名来匹配fixture这是一个魔法机制它方便同时也带来了隐患如果一个函数参数名字恰好和某个fixture重名pytest不会报错而是直接把fixture返回的值传给参数。我之前遇到过一个案例测试函数里定义了user参数用于接收一个字典数据但conftest.py里恰好有个userfixture。结果函数拿到的不是预期字典而是一个用户对象导致运行时报AttributeError。排查了十几分钟才意识到是fixture冲突。解决办法很简单fixture命名尽量有辨识度不要用user、data这种过于通用、容易和业务变量重名的词或者用一个统一的前缀比如fixture_user、api_client。如果你发现某个用例总是不对劲第一时间检查fixture名和参数名是否有冲突。7.2 session级fixture被用例修改的连锁反应session级fixture返回一个可变的list或dict时如果某个用例在测试过程中修改了这个对象那之后的用例都会拿到被修改后的值这种bug非常隐蔽。我真实遇到过的场景一个session级的字典型fixture保存了部分配置某个用例在测试中往这个字典里加了一个字段后续用例断言时发现多了字段但排查不到来源。解决这类问题的办法有三个fixture里返回深拷贝副本避免共享引用在fixture中定义成返回不可变对象如tuple将可变部分拆到每个用例独立创建的fixture中核心思路是不要随便在测试中修改来自共享fixture的数据。7.3 用例之间的顺序依赖有段时间我遇到过一种写法用例B依赖用例A的运行结果def test_a(): global order_id order_id create_order() assert order_id is not None def test_b(): assert order_id is not None resp get_order(order_id) assert resp.status_code 200这种写法在pytest里执行顺序刚好是先A后B时能过但一旦修改了用例顺序、或者用-n并行执行、或者只单独运行test_b就会挂掉。正确做法是把公共操作放到fixture里pytest.fixture def order(): order_id create_order() yield order_id # 可选清理 cancel_order(order_id) def test_create_order_success(order): assert order is not None def test_get_order(order): resp get_order(order) assert resp.status_code 200这样每个用例都独立执行顺序随意变换也不会互相影响。7.4 浮点数断言时直接用会翻车Python的浮点数存储会带来精度误差直接用比较常常得到Falsedef test_price(): price 0.1 0.2 assert price 0.3 # 会失败处理方式是用pytest.approximport pytest def test_price(): price 0.1 0.2 assert price pytest.approx(0.3)这在金额计算、费率相关的接口测试中经常遇到。我早期第一次遇到时困惑了很久后来养成了凡是浮点数断言先问自己这个值是否经过运算的习惯顺手就加上pytest.approx。7.5 断言表达式里不要写复杂逻辑我见过有人为了节省代码在assert里写一堆逻辑判断assert resp.json()[data][list][0][status] 1这种写法的问题在于一旦失败pytest只能告诉你整个表达式的值为False但到底是resp.json()有问题还是[data]不存在还是[list][0]为空还是[status]不等于1完全看不出来。更好的写法是拆步并配合中间变量body resp.json() assert body[code] 0 data body.get(data, {}) assert len(data.get(list, [])) 0 assert data[list][0][status] 1这样执行失败时定位到具体是哪一行断言出的问题就清楚了。调试效率会高很多。7.6 全局变量污染pytest的用例收集和执行不在同一个进程里其实不是它是在同一个进程内执行的只是收集阶段和执行阶段分离。这就意味着如果你在模块顶层定义了一个可变全局变量多个用例修改它会导致用例之间发生状态漂移。我之前踩过的坑两个测试模块都需要往同一个全局list里追加记录结果一个模块测试完另一个模块里的数据就多了很多脏结构断言全挂。现在我的规则很简单测试代码里尽量避免修改全局可变状态需要共享的东西全部通过fixture来提供。如果实在需要跨模块共享数据用临时文件或临时数据库来承载比全局变量安全得多。最后聊一个小体会。pytest的门槛确实低但它绝对不是把assert从assertEquals换掉这么简单。真正能体现pytest价值的地方是fixture对资源的精细控制、参数化对场景的全面覆盖、以及插件生态对整个测试流程的支撑。把这几个核心机制用透测试代码的维护成本会直线下降用例的执行效率也会上一个台阶。我个人建议新项目直接上pytest老项目如果有机会迁移成本也不高。如果你正从unittest迁移过来别急着把所有代码一次性改完优先把断言和fixture这两个最核心的机制用起来跑通之后就会明显感受到差别。测试这件事框架选对了路会好走很多。