---
title: "Python 测试替身为什么没生效：在调用方查找的名字上打补丁"
date: 2026-11-13
slug: python-mock-patch-lookup
canonical: https://kernelranch.com/blog/2026/11/python-mock-patch-lookup
markdown_url: https://kernelranch.com/blog/2026/11/python-mock-patch-lookup.md
tags: ["python", "testing", "unittest"]
---被测模块执行 `from gateway import charge` 后，会在自己的命名空间查找 `charge`。此时替换 `gateway.charge`，已经导入的绑定仍然指向原函数。测试要把补丁打在调用时查找的名字上，再用 `autospec=True` 让调用参数遵守真实签名。

## 1. 本机复现

保存为 `mock_lab.py`，运行 `python mock_lab.py`。为便于复现，脚本用 `ModuleType` 构造两个模块，模拟两个文件之间的导入。网关函数只返回固定字符串。

```python
from types import ModuleType
from unittest.mock import patch

gateway = ModuleType("lab_gateway")
checkout = ModuleType("lab_checkout")
exec(
    "def charge(amount, *, currency):\n"
    "    return f'real:{amount}:{currency}'\n",
    gateway.__dict__,
)

with patch.dict("sys.modules", {"lab_gateway": gateway, "lab_checkout": checkout}):
    exec(
        "from lab_gateway import charge\n"
        "def pay(amount):\n"
        "    return charge(amount, currency='CNY')\n",
        checkout.__dict__,
    )

    with patch("lab_gateway.charge", autospec=True, return_value="stub") as wrong:
        result = checkout.pay(25)
        assert result == "real:25:CNY"
        wrong.assert_not_called()
        print("patch definition:", result)

    with patch("lab_checkout.charge", autospec=True, return_value="stub") as stub:
        result = checkout.pay(25)
        assert result == "stub"
        stub.assert_called_once_with(25, currency="CNY")
        print("patch lookup:", result)

        try:
            checkout.charge(25)
        except TypeError:
            print("missing keyword: rejected by autospec")
        else:
            raise AssertionError("accepted invalid signature")

    assert checkout.pay(25) == "real:25:CNY"
    print("after patch:", checkout.pay(25))
```

本地实测输出如下。

```text
patch definition: real:25:CNY
patch lookup: stub
missing keyword: rejected by autospec
after patch: real:25:CNY
```

## 2. 沿着调用表达式定位补丁

| 被测模块的调用方式 | 通常的补丁位置 |
|---|---|
| `from gateway import charge`，随后调用 `charge(...)` | 被测模块的 `charge` |
| `from gateway import charge as send`，随后调用 `send(...)` | 被测模块的 `send` |
| `import gateway`，随后调用 `gateway.charge(...)` | `gateway` 模块对象上的 `charge` |
| 依赖已绑定为默认参数或保存在实例里 | 调用时注入测试替身，或替换实际持有的实例依赖 |

如果断言显示 mock 从未调用，检查导入形式、别名和绑定时机。正确替换后，既要断言业务结果，也要断言替身收到的参数。`autospec` 可以捕获签名不符的调用。远端返回结构、超时和实际连接行为仍需要有明确输入输出的契约测试或集成测试。

## 小结

根据执行时的查找路径定位补丁。用 autospec 检查调用形状，用断言验证替身确实进入执行路径。

## 数据来源与检索时间

检索时间 2026-11-13。

| 来源 | URL | 对应内容 |
|---|---|---|
| Python unittest.mock Where to patch | https://docs.python.org/3.11/library/unittest.mock.html#where-to-patch | 在使用方查找的命名空间替换绑定 |
| Python unittest.mock Autospeccing | https://docs.python.org/3.11/library/unittest.mock.html#autospeccing | 基于真实对象创建带签名约束的替身 |

## 核验说明

- Windows、Python 3.11.9 验证了补丁目标选错、正确拦截、缺少关键字参数时抛出 TypeError，以及离开补丁作用域后恢复原绑定。
