python mock 遇到的一个坑

修改一个项目的 Unit Test 代码时,遇到一个关于 mock 的问题点,花了我很久时间才,找到解决方案。特此记录一下,提醒自己,提示他人。Binder

<- 吐槽开始

参与开源项目,最令人头疼的东西,不是他们的代码有多么先进(当然有些项目的代码确实很让人震惊,使用了一些从未听过的库、特性和设计方案:比如 TensorFlow),真正让人震惊的是:开源软件对软件工程的实践。我相信很多企业开发人员和我一样,虽然公司已经比较大型,开发人员众多,公司业务已经达到日均千万次,软件开发人员的素质也很高,个人能力也属于业内能力数一数二的,但是从软件开发过程的角度来说,由于种种内部和外界的因素,导致我们的软件工程实践长期停留在 “家庭作坊式”,但是当我们参与一个大中型 (大约 1K+ star) 开源软件时,我们才意识到外国开发者对于软件工程的理解和使用确实是超出中国十万光年啊 (毕竟软件工程就是歪果仁发明的,多数时候,我们只是照猫画虎)。

吐槽结束 ->

我给一个开源软件贡献 PR 时,遇到一个大的问题是我总是要给我的代码写单元测试,会出现一些奇怪的问题(我以为的,实际最后往往证明是我蠢 :( ,本例就是)

经过简化,最小可复现问题代码集如下:

文件 dependency.py

1
2
def some_funny_func():
return 'funny'

文件 module.py

1
2
3
4
from dependency import some_funny_func

def call_func():
return some_funny_func()

文件 tester.py

1
2
3
4
5
6
7
8
9
10
11
12
from unittest.mock import patch

from module import call_func

def test_call_func():
def mocked_funny_func():
return "not funny at all"

with patch("dependency.some_funny_func", mocked_funny_func):
return_value = call_func()

assert return_value == "not funny at all"

一切看似合情合理(有些高手,可能已经发现问题了,但我当时没有看出来问题),但是就是通过不了测试。

这里的错误是,没有深入理解 patch 的工作原理,patch 通过修改 module 属性的方式工作。这里 from module import call_func 执行的时候已经经 dependency.some_funny_func 导入了 module, 换言之:module.some_funny_func 已经指向了 dependency.some_funny_func, 此时通过 patch("dependency.some_funny_func", mocked_funny_func) 只是修改了 dependency.some_funny_func 至新的 mocked_funny_func. 但不能修改 module.some_funny_func, 因为这个是修改前赋值的,它现在依旧指向原来的函数。

将上述改写成 python 代码,原理大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 模拟 dependency.some_funny_func
dependency = {}
dependency["some_funny_func"] = "some_value"

# 模拟 module.some_funny_func
module = {}
module["some_funny_func"] = dependency["some_funny_func"]

# 模拟 mock
dependency["some_funny_func"] = "some_other_value"

# 查看结果
print(dependency["some_funny_func"])
print(module["some_funny_func"])

在线演示

在线演示地址