[英]How can memoized functions be tested?
我有一個簡單的memoizer,我用來節省昂貴的網絡電話的時間。 粗略地說,我的代碼看起來像這樣:
# mem.py
import functools
import time
def memoize(fn):
"""
Decorate a function so that it results are cached in memory.
>>> import random
>>> random.seed(0)
>>> f = lambda x: random.randint(0, 10)
>>> [f(1) for _ in range(10)]
[9, 8, 4, 2, 5, 4, 8, 3, 5, 6]
>>> [f(2) for _ in range(10)]
[9, 5, 3, 8, 6, 2, 10, 10, 8, 9]
>>> g = memoize(f)
>>> [g(1) for _ in range(10)]
[3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
>>> [g(2) for _ in range(10)]
[8, 8, 8, 8, 8, 8, 8, 8, 8, 8]
"""
cache = {}
@functools.wraps(fn)
def wrapped(*args, **kwargs):
key = args, tuple(sorted(kwargs))
try:
return cache[key]
except KeyError:
cache[key] = fn(*args, **kwargs)
return cache[key]
return wrapped
def network_call(user_id):
time.sleep(1)
return 1
@memoize
def search(user_id):
response = network_call(user_id)
# do stuff to response
return response
我對這段代碼進行了測試,在這里我模擬了network_call()
不同返回值,以確保我在search()
做的一些修改按預期工作。
import mock
import mem
@mock.patch('mem.network_call')
def test_search(mock_network_call):
mock_network_call.return_value = 2
assert mem.search(1) == 2
@mock.patch('mem.network_call')
def test_search_2(mock_network_call):
mock_network_call.return_value = 3
assert mem.search(1) == 3
但是,當我運行這些測試時,我得到了一個失敗,因為search()
返回一個緩存的結果。
CAESAR-BAUTISTA:~ caesarbautista$ py.test test_mem.py
============================= test session starts ==============================
platform darwin -- Python 2.7.8 -- py-1.4.26 -- pytest-2.6.4
collected 2 items
test_mem.py .F
=================================== FAILURES ===================================
________________________________ test_search_2 _________________________________
args = (<MagicMock name='network_call' id='4438999312'>,), keywargs = {}
extra_args = [<MagicMock name='network_call' id='4438999312'>]
entered_patchers = [<mock._patch object at 0x108913dd0>]
exc_info = (<class '_pytest.assertion.reinterpret.AssertionError'>, AssertionError(u'assert 2 == 3\n + where 2 = <function search at 0x10893f848>(1)\n + where <function search at 0x10893f848> = mem.search',), <traceback object at 0x1089502d8>)
patching = <mock._patch object at 0x108913dd0>
arg = <MagicMock name='network_call' id='4438999312'>
@wraps(func)
def patched(*args, **keywargs):
# don't use a with here (backwards compatability with Python 2.4)
extra_args = []
entered_patchers = []
# can't use try...except...finally because of Python 2.4
# compatibility
exc_info = tuple()
try:
try:
for patching in patched.patchings:
arg = patching.__enter__()
entered_patchers.append(patching)
if patching.attribute_name is not None:
keywargs.update(arg)
elif patching.new is DEFAULT:
extra_args.append(arg)
args += tuple(extra_args)
> return func(*args, **keywargs)
/opt/boxen/homebrew/lib/python2.7/site-packages/mock.py:1201:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
mock_network_call = <MagicMock name='network_call' id='4438999312'>
@mock.patch('mem.network_call')
def test_search_2(mock_network_call):
mock_network_call.return_value = 3
> assert mem.search(1) == 3
E assert 2 == 3
E + where 2 = <function search at 0x10893f848>(1)
E + where <function search at 0x10893f848> = mem.search
test_mem.py:15: AssertionError
====================== 1 failed, 1 passed in 0.03 seconds ======================
有沒有辦法測試記憶功能? 我考慮了一些替代方案,但它們都有缺點。
一種解決方案是模擬memoize()
。 我不願意這樣做,因為它泄漏了測試的實現細節。 從理論上講,我應該能夠在沒有系統其他部分的情況下記憶和取消默認功能,包括測試,從功能角度注意。
另一種解決方案是重寫代碼以公開修飾函數。 也就是說,我可以這樣做:
def _search(user_id):
return network_call(user_id)
search = memoize(_search)
然而,這遇到了與上面相同的問題,盡管它可能更糟,因為它不適用於遞歸函數。
是否真的需要在功能級別定義您的memoization?
這有效地使得memoized數據成為一個全局變量 (就像函數一樣,它的共享范圍)。
順便說一下,這就是你在測試時遇到困難的原因!
那么,如何將它包裝成一個對象呢?
import functools
import time
def memoize(meth):
@functools.wraps(meth)
def wrapped(self, *args, **kwargs):
# Prepare and get reference to cache
attr = "_memo_{0}".format(meth.__name__)
if not hasattr(self, attr):
setattr(self, attr, {})
cache = getattr(self, attr)
# Actual caching
key = args, tuple(sorted(kwargs))
try:
return cache[key]
except KeyError:
cache[key] = meth(self, *args, **kwargs)
return cache[key]
return wrapped
def network_call(user_id):
print "Was called with: %s" % user_id
return 1
class NetworkEngine(object):
@memoize
def search(self, user_id):
return network_call(user_id)
if __name__ == "__main__":
e = NetworkEngine()
for v in [1,1,2]:
e.search(v)
NetworkEngine().search(1)
產量:
Was called with: 1
Was called with: 2
Was called with: 1
換句話說, NetworkEngine
每個實例都有自己的緩存。 只需重用相同的一個來共享一個緩存,或者實例化一個新緩存以獲得一個新的緩存。
在您的測試代碼中,您將使用:
@mock.patch('mem.network_call')
def test_search(mock_network_call):
mock_network_call.return_value = 2
assert mem.NetworkEngine().search(1) == 2
您應該分別測試每個問題:
你已經展示了memoize
,我猜你已經測試過了。
你似乎有network_call
,所以你應該單獨測試,而不是memoized。
現在您想要將兩者結合起來,但可能這將是為了其他代碼的好處,以避免冗長的網絡延遲。 但是,如果要測試其他代碼,則它甚至不應進行1次網絡調用,因此您可能必須提供函數名稱作為參數。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.