Python 现代开发学习笔记

(十二)用替身稳定复现难触发的失败

1. 在文件替换处制造一次错误

原子替换最需要确认的是:os.replace 失败时,旧文件仍然完整,临时文件被清理。平时反复运行通常会成功,无法稳定检查失败分支。monkeypatch 可以在本次测试中临时替换一个属性,结束后恢复:

# 项目文件:quality_project/tests/integration/test_atomic.py
# 替换真正查找 replace 的模块属性,让写好临时文件之后稳定发生 OSError。
import pytest

from bookdata import atomic


def test_failed_replace_keeps_old_file(tmp_path, monkeypatch):
    target = tmp_path / "books.json"
    target.write_text("旧快照", encoding="utf-8")

    def fail_replace(source, destination):
        assert source.parent == target.parent
        assert source.read_text(encoding="utf-8") == "新快照"
        assert destination == target
        raise OSError("模拟替换失败")

    monkeypatch.setattr(atomic.os, "replace", fail_replace)
    with pytest.raises(OSError, match="模拟替换失败"):
        atomic.write_text_atomic(target, "新快照")
    assert target.read_text(encoding="utf-8") == "旧快照"
    assert list(tmp_path.iterdir()) == [target]

这次仍然创建真实临时文件、写入并关闭,只有切换名字的动作被替换。于是检查既能观察完整写入过程,又能稳定走到清理路径。

临时文件已经写好、替换抛错、旧文件保留与临时名字清理的状态变化

monkeypatch.setattr(对象, 属性名, 替身) 改的是程序执行时实际读取的位置。当前 atomic.py 使用 os.replace,因此替换 atomic.os.replace;若模块写成 from os import replace,就应替换那个模块自己的 replace。补丁持续期间,同一个 os 模块对象上的属性也会变化,所以把调用集中在这次测试中。monkeypatch

2. 按要观察的行为选择替换位置

稍后还会让一个内存对象替代 Library,检查服务层传入了哪份书目。这类替身保留方法接口,把外部资源换成可观察的对象。需要验证 SQL 与回滚时仍使用真实 SQLite;把连接的所有方法都替换掉,就看不到真实事务结果。

测试如果靠睡眠等待“刚好失败”、依赖当前日期或共享用户环境,就很难重复。第五章的日志方法显式接收 date,测试直接传入固定日期;配置测试用 monkeypatch.setenv 和 delenv 临时控制环境变量。先把会变化的输入放到明确的位置,再安排稳定数据。

笔记正文与全部题解 · Ctrl / ⌘ K