测试、调试与代码质量
学习目标
- 用断言和 pytest 编写清晰的小测试。
- 区分单元测试、参数化、异常断言和 fixture。
- 使用 traceback、logging、pdb 和 lint/format 工具缩短定位时间。
核心知识点
测试验证可观察行为,不应只验证实现细节。单元测试快速隔离一个函数;fixture 管理共享准备;参数化覆盖多个输入。调试先复现,再看最小堆栈和局部状态,最后补回归测试。
常用用法
assert
def add(left: int, right: int) -> int:
return left + right
result = add(2, 3)
assert result == 5
print(result)
# 输出:5简单脚本可用 assert 表达不变量;正式测试通常让 pytest 收集并报告失败上下文。
pytest 基础测试
def add(left: int, right: int) -> int:
return left + right
def test_add_returns_sum() -> None:
assert add(2, 3) == 5
test_add_returns_sum()
print("test body passed")
# 输出:test body passed文件通常命名为 test_*.py 或 *_test.py,测试函数以 test_ 开头。
python -m pytest -qpython -m pytest -qpytest 参数化
cases = [(1, 2, 3), (0, 4, 4), (-1, 1, 0)]
results = [left + right == expected for left, right, expected in cases]
print(all(results))
# 输出:True真实 pytest 写法使用 @pytest.mark.parametrize,每组输入都会显示为独立案例。
import pytest
@pytest.mark.parametrize(
("left", "right", "expected"),
[(1, 2, 3), (0, 4, 4)],
)
def test_add_cases(left: int, right: int, expected: int) -> None:
assert left + right == expected
print("parameterized test defined")
# 输出:parameterized test definedpytest.raises
import pytest
def parse_port(text: str) -> int:
value = int(text)
if not 1 <= value <= 65535:
raise ValueError("port out of range")
return value
with pytest.raises(ValueError, match="range"):
parse_port("70000")
print("error contract passed")
# 输出:error contract passed异常测试应检查异常类型和必要的消息片段,不要把完整自然语言消息当成稳定 API。
fixture
import pytest
@pytest.fixture
def sample_user() -> dict[str, str]:
return {"name": "Ada"}
def test_user_name(sample_user: dict[str, str]) -> None:
assert sample_user["name"] == "Ada"
print(sample_user.__name__)
# 输出:sample_userfixture 负责准备和清理,测试函数通过参数声明依赖;不要在多个测试中复制相同初始化代码。
logging 调试信息
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(message)s")
logger = logging.getLogger("demo")
logger.info("value=%s", 3)
print(logger.name)
# 输出:demo日志参数用占位符传入,避免先拼接昂贵字符串;库代码不要随意调用 basicConfig。
breakpoint()
import sys
debugger_name = sys.breakpointhook.__name__
print(debugger_name)
# 输出:breakpointhook交互调试时可在代码中临时放 breakpoint(),提交前应移除或由条件控制;生产排障优先使用日志和可观测性。
不常用但需要知道
unittest.mock.Mock
from unittest.mock import Mock
service = Mock()
service.fetch.return_value = "ok"
result = service.fetch("id-1")
print(result, service.fetch.call_count)
# 输出:ok 1mock 适合隔离慢或外部依赖,但过度 mock 会让测试只验证调用细节而失去行为价值。
unittest.mock.patch
from unittest.mock import patch
class Service:
def fetch(self, key: str) -> str:
return f"real:{key}"
service = Service()
with patch.object(service, "fetch", return_value="mocked"):
value = service.fetch("id-1")
print(value)
# 输出:mockedpatch 应该作用于“被测模块查找依赖的路径”,而不是盲目 patch 依赖定义的原始路径。
traceback.format_exc()
import traceback
try:
1 / 0
except ZeroDivisionError:
summary = traceback.format_exc().splitlines()[-1]
print(summary)
# 输出:ZeroDivisionError: division by zero记录完整 traceback 有助于定位;对用户返回时应隐藏内部路径和敏感数据。
compileall
import compileall
print(callable(compileall.compile_dir))
# 输出:True可以用 python -m compileall 做语法级预检,但它不能替代类型检查、测试和运行时覆盖。
python -m compileall -q src tests易混点
- 测试文件被 pytest 收集不代表测试真的覆盖了失败分支。
- fixture 的作用域越大,状态泄漏风险越高;默认函数级最安全。
- mock 的路径应是被测模块实际使用的名字。
breakpoint()是调试工具,不应留在生产路径。- 格式化、lint、类型检查和测试解决不同问题,应组合使用。
课后小问
为什么异常测试要同时检查类型和消息片段? 答案:类型确认错误分类,消息片段确认关键原因;完整消息可留给实现细节。 解析:只检查“抛了异常”可能误收错误类型,只匹配完整文本又容易因措辞变化产生脆弱测试。
mock 外部服务时,为什么 patch 路径容易写错? 答案:应 patch 被测模块中查找依赖的名字,而不是依赖最初定义的模块路径。 解析:
from client import fetch后,被测模块使用的是自己的fetch绑定,patch 原模块不会替换它。
本节小结
先写能复现行为的测试,再用最小日志和 traceback 定位;pytest 管理测试结构,mock 隔离边界,格式化/类型检查/编译预检共同提升质量。
快速回顾
- 基本断言:
assert - pytest:收集、参数化、
raises、fixture - 调试:
logging、breakpoint、traceback - 隔离:
Mock、patch - 预检:
python -m compileall
阿源的知识库