Python 如何测试记忆功能?

Python 如何测试记忆功能?,python,unit-testing,python-decorators,Python,Unit Testing,Python Decorators,我有一个简单的回忆录,我用它来节省昂贵的网络通话时间。大致上,我的代码如下所示: # 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) >>>

我有一个简单的回忆录,我用它来节省昂贵的网络通话时间。大致上,我的代码如下所示:

# 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
def _search(user_id):
    return network_call(user_id)
search = memoize(_search)
我对这段代码进行了测试,我模拟了
network\u 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
,我想您已经测试过了

您似乎有
网络调用
,因此您应该单独测试,而不是记忆


现在您希望将两者结合起来,但这可能是为了其他代码的好处,以避免长时间的网络延迟。但是,如果您想测试另一个代码,那么它甚至不应该进行一次网络调用,因此您可能必须提供一个函数名作为参数。

是否确实需要在函数级别定义您的备忘录

这有效地使记忆数据成为一个全局变量(就像函数一样,它共享其作用域)

顺便说一句,这就是为什么您在测试它时遇到困难

那么,把它包装成一个对象怎么样

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

我不太明白。为什么有两个测试测试不同的返回值?如果记忆函数可以返回一个“stale”值(与网络中的live值不同),那么不应该测试两个值。如果不行,那么您需要使您的备忘录更复杂,以便在必要时以某种方式使缓存无效。如果你没有办法知道什么时候可以使用记忆值,什么时候可以获得真实值,那么记忆就没有用了。好吧,想象一下下面的真实场景。您使用输入调用memoized
search
函数,它返回1(因为这是服务器当前的行为方式)。假设将来服务器状态发生变化,因此当您传入
1
时,它会返回其他内容。记忆化的
search
函数的预期行为是否总是返回
1
?@ceaarbautista:如果代码的实际用户在
网络调用返回2,然后调用
mem.search(1)的情况下调用
mem.search(1)
网络调用
返回3的情况下,结果应该是什么?如果希望调用独立,则需要使缓存无效。如果从不使缓存无效,则
网络调用
行为的更改将永远不会影响以后对
mem.search
(使用相同参数)的调用。@ceaarbautista这仅适用于不影响环境状态的单元测试。单元测试并非如此(包装器函数的缓存发生了变化)。如果希望单元测试被隔离,则需要在它们之间运行拆卸和设置。在这种情况下,这将涉及从搜索函数的定义和装饰符中重新创建搜索函数,或以其他方式清除备忘录缓存。@CesarBautista:它们无法隔离全局状态,而全局状态实际上就是你的备忘录。如果一个测试设置了一个全局变量
blah=1
,其他测试也会看到。同样,已记忆的值也全局存储(在已记忆函数的闭包中)。如果希望在某个非全局级别上进行记忆,则需要将
search
设置为非全局函数(例如,某个类的方法)。或者,您也可以提供某种“手动覆盖”,使缓存无效。+1是一个很好的答案,但如果您使用OP的原始代码,理解这一点可能会快一点。