Ho un semplice memoizer che sto usando per risparmiare un po 'di tempo per le chiamate di rete costose. Approssimativamente, il mio codice simile a questo:Come possono essere testate le funzioni memoizzate?
# 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
E ho le prove per questo codice, dove ho un mock di valori di ritorno di diversi network_call()
per assicurarsi che alcune modifiche che faccio in search()
lavoro come previsto.
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
Tuttavia, quando ho eseguito questi test, ottengo un fallimento, perché search()
restituisce un risultato in cache.
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 ======================
C'è un modo per testare le funzioni memorizzate? Ho preso in considerazione alcune alternative, ma ognuna ha degli svantaggi.
Una soluzione è prendere in giro memoize()
. Sono riluttante a farlo perché fa filtrare i dettagli di implementazione ai test. In teoria, dovrei essere in grado di memoizzare e disimitare le funzioni senza il resto del sistema, compresi i test, rilevando da un punto di vista funzionale.
Un'altra soluzione è riscrivere il codice per esporre la funzione decorata. Cioè, ho potuto fare qualcosa di simile:
def _search(user_id):
return network_call(user_id)
search = memoize(_search)
Tuttavia, questo corre negli stessi problemi di cui sopra, anche se è forse peggio, perché non funziona per funzioni ricorsive.
Non sono sicuro di aver capito. Perché hai due test che testano valori di ritorno diversi? Se è corretto per la funzione memorizzata restituire un valore "obsoleto" (non lo stesso del valore live dalla rete), non è necessario testare due valori. Se non va bene, è necessario rendere la tua memoizzazione più sofisticata in modo da poter in qualche modo invalidare la cache quando necessario. Non è utile in memoizing se non si ha un modo per sapere quando va bene usare il valore memoized e ottenere il valore reale. – BrenBarn
Non capisco perché la soluzione proposta non funzionerebbe. Non potresti usare '_search' nei tuoi test? La mia ipotesi è che si voglia testare il comportamento di '_search' quando la chiamata di rete restituisce vari valori e non è interessata alla memoizzazione. –
Hm, avendo problemi a seguirti. I test hanno valori di ritorno diversi per simulare 'network_call()' i cui valori di ritorno possono essere diversi per gli stessi parametri (ad esempio in base al valore su un server). Dovrebbero essere indipendenti, quindi non dovrebbe esserci la necessità di invalidare la cache. –