Python 测试与调试
pytest + breakpoint() + logging 三件套。写完代码顺手配好这些,比出事后再 print 强一个量级。环境配置见Python 环境与包管理。
pytest:assert 就是测试
# test_math.py —— 文件和函数都以 test_ 开头,pytest 自动发现
def add(a, b):
return a + b
def test_add():
assert add(1, 2) == 3
def test_add_negative():
assert add(-1, -1) == -2
def test_add_type_error():
import pytest
with pytest.raises(TypeError):
add(1, "2")
pytest # 跑当前目录全部测试
pytest test_math.py # 指定文件
pytest -k "add and not negative" # 按名字过滤
pytest -x # 第一个失败就停
pytest -v # 显示每个用例
pytest --tb=short # 精简报错回溯
pytest -s # 显示 print 输出
失败时 pytest 会把 assert 两边的实际值打印出来(assert 重写机制),比裸 assert 友好得多。
fixture:测试的依赖注入
import pytest
@pytest.fixture
def sample_data():
# 每个用到它的测试函数前执行一次(setup), yield 后是 teardown
data = {"users": [{"id": 1, "name": "Ada"}]}
yield data
data.clear() # 清理
def test_find_user(sample_data): # 参数名 = fixture 名,自动注入
assert sample_data["users"][0]["name"] == "Ada"
# 作用域:@pytest.fixture(scope="module") 整个文件一份
# scope="session" 整场测试一份(数据库连接等)
# 共享 fixture 放 conftest.py,同目录测试自动可见,无需 import
参数化:一个用例测一组数据
import pytest
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
])
def test_add_many(a, b, expected):
assert add(a, b) == expected
monkeypatch:替换依赖
import requests
def get_status(url: str) -> int:
return requests.get(url).status_code
def test_get_status(monkeypatch):
class FakeResp:
status_code = 200
monkeypatch.setattr(requests, "get", lambda url: FakeResp()) # 不发真请求
assert get_status("http://whatever") == 200
更复杂的替身用 unittest.mock.Mock/patch——原则一样:测试里永远不发真网络请求、不碰真数据库。
项目结构约定
myproject/
├─ src/myproject/ # 源码
├─ tests/
│ ├─ conftest.py # 共享 fixture
│ ├─ test_core.py
│ └─ test_api.py
└─ pyproject.toml # dev 依赖: pytest, ruff, mypy
测的是"行为"不是"实现":测 parse() 返回正确结果,而不是测它内部调了哪个私有函数。
调试:breakpoint() 与 pdb
def suspicious(data):
result = transform(data)
breakpoint() # 3.7+,执行到这里进入调试器(PDB 1.0 自动带颜色)
return result
(Pdb) p result 打印变量
(Pdb) pp data pretty-print
(Pdb) l / ll 看代码 / 长列表
(Pdb) n 下一行(不进函数)
(Pdb) s 步入函数
(Pdb) c 继续运行到下一个断点
(Pdb) w 调用栈
(Pdb) u / d 栈帧上移 / 下移
(Pdb) q 退出
python -m pdb script.py # 从头调试
pytest --pdb # 测试失败瞬间掉进调试器(post-mortem,现场变量都在)
pytest -x --pdb # 第一个失败就停
VS Code / PyCharm 里打断点本质也是这个,图形界面更省事;终端环境(服务器、SSH)pdb 是救命稻草。
logging:把 print 升级掉
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
logger = logging.getLogger(__name__) # 每模块一个,名字自动带模块路径
logger.debug("细节 %s", value) # 占位符惰性格式化,别用 f-string
logger.info("started")
logger.warning("deprecated call")
logger.error("failed: %s", err, exc_info=True) # 带堆栈
好处:级别可调(生产 INFO、排查时 DEBUG)、可路由(文件/轮转/syslog)、可关停。写库代码用 logger 别用 print;一次性脚本 print 无妨。
# 文件 + 控制台双输出(简单场景够用)
file_handler = logging.FileHandler("app.log", encoding="utf-8")
logging.basicConfig(level=logging.DEBUG,
handlers=[logging.StreamHandler(), file_handler])
静态检查:让机器替你 code review
ruff check . # lint(替代 flake8+isort 等,快几百倍)
ruff format . # 格式化(替代 black)
mypy src/ # 类型检查;pyright/Pylance 是 VS Code 默认
pyproject.toml 里统一配置工具(示例见环境与包管理),进 pre-commit 或 CI。
习惯清单
- 新功能先写一个会失败的测试(红),实现让它变绿——不用教条 TDD,但回归测试必须有;
- 修 bug 前先写一个复现该 bug 的测试,修完它转绿,这个 bug 永远不会再回来;
breakpoint()用完删掉,别提交(ruff 的 TID/debugger检查能拦);- 测试跑得快才有人跑:慢 IO 全 mock,数据库用 scope="function" 的事务回滚。