from utils import sum 然後 patch('utils.sum'),測試死活 mock 不到。
改成 patch('helloworld.sum') 就好了。
這是 Python mock 最常見的誤區,搞懂原因之後就不會再踩。
問題重現
utils.py:
1
2
| def sum(a, b):
return a + b
|
helloworld.py:
1
2
3
4
| from utils import sum
def main():
return sum(1 + 2)
|
寫測試,想 mock 掉 sum:
1
2
3
4
5
6
| from unittest.mock import patch
def test_main():
with patch('utils.sum') as mocked:
mocked.return_value = 5
assert main() == 5 # 失敗!assert 3 != 5
|
main() 回傳的是 3,不是 5。mock 沒有生效。
為什麼
from utils import sum 這行做了一件事:把 utils.sum 這個 function object 複製一份 reference 到 helloworld 模組的命名空間裡。
之後 helloworld 呼叫的 sum,用的是 helloworld.sum 這個 binding,不是 utils.sum。
patch('utils.sum') 確實替換了 utils 模組裡的 sum,但 helloworld.sum 還是指著原本的 function object,完全沒被動到。
畫成圖:
1
2
3
4
| patch('utils.sum') 之後:
utils.sum ──────→ MockObject ← patch 改的是這裡
helloworld.sum ──→ <原始 sum> ← 這個沒動,main() 用這個
|
正確寫法
patch 要指向使用這個函式的模組,也就是被測模組:
1
2
3
4
| def test_main():
with patch('helloworld.sum') as mocked: # 改這裡
mocked.return_value = 5
assert main() == 5 # 通過
|
patch('helloworld.sum') 替換的是 helloworld 命名空間裡的 sum,main() 呼叫的就是被替換過的版本,mock 生效。
原則:patch where it’s used
這個規則很好記:
patch 要寫使用這個名稱的地方,不是定義這個名稱的地方
| import 方式 | patch 目標 |
|---|
from utils import sum | patch('helloworld.sum') |
import utils | patch('utils.sum') |
第二種情況,import utils 之後呼叫 utils.sum(...),helloworld 裡沒有建立獨立的 binding,每次呼叫都是透過 utils 模組去查,所以 patch('utils.sum') 就夠了。
完整範例
1
2
3
| # utils.py
def sum(a, b):
return a + b
|
1
2
3
4
5
| # helloworld.py
from utils import sum
def main():
return sum(1, 2)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # test_helloworld.py
from unittest.mock import patch
from helloworld import main
def test_main_with_wrong_patch():
# 錯誤:patch 打在原始模組,helloworld.sum 沒被替換
with patch('utils.sum') as mocked:
mocked.return_value = 5
result = main()
assert result == 5 # AssertionError: 3 != 5
def test_main_with_correct_patch():
# 正確:patch 打在使用的模組
with patch('helloworld.sum') as mocked:
mocked.return_value = 5
result = main()
assert result == 5 # 通過
|
延伸:同一個模組內的函式
如果 main 和 sum 在同一個模組:
1
2
3
4
5
6
| # app.py
def sum(a, b):
return a + b
def main():
return sum(1, 2)
|
1
2
3
4
| # test_app.py
with patch('app.sum') as mocked:
mocked.return_value = 5
assert main() == 5 # 通過
|
同理,patch 目標是 app.sum,也就是 main 呼叫時查找的那個命名空間。
小結
Python 的 import 機制會在被匯入的模組建立獨立的 binding。from X import Y 之後,模組裡就有自己的 Y,跟 X.Y 是各自獨立的 reference。
patch 的目標永遠是呼叫發生的地方,不是函式定義的地方。記住這條規則,mock 不到的問題基本上就不會再出現。
參考資源