[英]How to test if a function calls range in python?
我是 Python 講師,我想給我的學生一個任務:編寫一個 function,使用 for 循環計算列表的平均值,范圍為 object。
我想對他們的 function 進行測試,看看它是否實際使用范圍 object。我該怎么做?
它應該是這樣的:
def avg(L):
Pass
def test_range(avg):
...
如果avg
包含range
,那么test_range
應該返回True
。
我嘗試了利用func_code
的解決方案,但顯然range
沒有。
您可以使用 Python 的unittest.mock
模塊從builtins
模塊環繞range
function,然后讓您的測試斷言環繞的range
確實被調用了。
例如,使用 Python 的unittest
框架編寫測試:
import builtins
import unittest
from unittest.mock import patch
# I don't know what L is supposed to be, and I know that there
# are better ways to compute average of a list, but the code
# for calculating the average is not important for this question.
def avg(L):
total = 0
for index in range(len(L)):
total += L[index]
return total / len(L)
class TestAverage(unittest.TestCase):
def test_avg(self):
with patch("builtins.range", wraps=builtins.range) as wrapped_patch:
expected = 47
actual = avg([1,49,91])
self.assertEqual(expected, actual)
wrapped_patch.assert_called()
if __name__ == '__main__':
unittest.main()
$ python -m unittest -v main.py
test_avg (main.TestAverage) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
它使用unittest.mock
的針對 builtins.range builtins.range
的patch
。通常, patch
用於測試以替換目標的行為和/或返回值,但在這種情況下,您可以傳遞wraps=builtins.range
(這被傳遞給底層的Mock
對象),這意味着“我只想監視調用,但不想修改它的行為”:
wraps :用於模擬 object 包裝的項目。 如果wraps不是
None
那么調用 Mock 會將調用傳遞給包裝的 object(返回真實結果)。
通過將其包裝在Mock
object 中,您可以使用Mock
的任何斷言函數來檢查對range
的調用,例如assert_called
檢查目標是否至少被調用一次。 您可以通過斷言調用range
的次數來更具體:
self.assertTrue(wrapped_patch.call_count == 1)
如果根本不調用斷言,斷言就會失敗:
# Here, `range` wasn't used at all.
def avg(L):
return sum(L) / len(L)
class TestAverage(unittest.TestCase):
# same as the code above
$ python -m unittest -v main.py
test_avg (main.TestAverage) ... FAIL
======================================================================
FAIL: test_avg (main.TestAverage)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/path/to/main.py", line 15, in test_avg
wrapped_patch.assert_called()
File "/usr/local/Cellar/python@3.10/3.10.8/Frameworks/Python.framework/Versions/3.10/lib/python3.10/unittest/mock.py", line 888, in assert_called
raise AssertionError(msg)
AssertionError: Expected 'range' to have been called.
使用patch
時要注意的最重要的事情是要確切地知道在哪里打補丁。 在這種情況下,您可以查看文檔或使用__module__
來了解range
的模塊:
>>> range
<class 'range'>
>>> range.__module__
'builtins'
但是測試有點幼稚,因為即使avg
沒有真正使用range
,它仍然可以通過:
def avg(L):
range(len(L)) # Called but really unused. Sneaky!
return sum(L) / len(L)
class TestAverage(unittest.TestCase):
# same as the code above
$ python -m unittest -v main.py
test_avg (main.TestAverage) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
一個稍微令人困惑的解決方法是“打破” range
的測試,如果 function真的使用range
,那么它將不再起作用:
def avg(L):
range(len(L)) # Called but really unused. Sneaky!
return sum(L) / len(L)
class TestAverage(unittest.TestCase):
def test_avg(self):
# same as above
def test_avg_is_really_using_range(self):
L = [10,20,90]
# Is it returning the correct result?
self.assertEqual(avg(L), 40)
# OK, but did it really use `range`?
# Let's try breaking `range` so it always yields 0,
# so we expect the return value to be *different*
with patch("builtins.range", return_value=[0,0,0]):
self.assertNotEqual(avg(L), 40)
因此,如果avg
偷偷調用但沒有真正使用range
,那么test_avg_is_really_using_range
現在會失敗,因為即使range
被破壞, avg
仍然會產生正確的值:
$ python -m unittest -v main.py
test_avg (main.TestAverage) ... ok
test_avg_really_using_range (main.TestAverage) ... FAIL
======================================================================
FAIL: test_avg_really_using_range (main.TestAverage)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/path/to/main.py", line 19, in test_avg_really_using_range
self.assertNotEqual(avg(L), 40)
AssertionError: 40.0 == 40
最后,作為旁注,我在所有示例中都使用了assertEqual
,因為返回值的測試不是重點,但請務必閱讀斷言可能的浮點值的正確方法,例如。 如何對浮點輸出執行單元測試? - python
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.