简体   繁体   English

如何对装饰功能进行单元测试?

[英]How to unit-test decorated functions?

I tried lately to train myself a lot in unit-testing best practices. 我最近尝试过对单元测试最佳实践进行大量训练。 Most of it makes perfect sense, but there is something that is often overlooked and/or badly explained: how should one unit-test decorated functions ? 其中大部分内容都非常有意义,但有一些内容经常被忽视和/或解释得很糟糕:一个单元测试如何装饰功能?

Let's assume I have this code: 我们假设我有这个代码:

def stringify(func):
    @wraps(func)
    def wrapper(*args):
        return str(func(*args))

    return wrapper


class A(object):
    @stringify
    def add_numbers(self, a, b):
        """
        Returns the sum of `a` and `b` as a string.
        """
        return a + b

I can obviously write the following tests: 我显然可以写下面的测试:

def test_stringify():
    @stringify
    def func(x):
        return x

    assert func(42) == "42"

def test_A_add_numbers():
    instance = MagicMock(spec=A)
    result = A.add_numbers.__wrapped__(instance, 3, 7)
    assert result == 10

This gives me 100% coverage: I know that any function that gets decorated with stringify() gets his result as a string, and I know that the undecorated A.add_numbers() function returns the sum of its arguments. 这给了我100%的覆盖率:我知道用stringify()修饰的任何函数都将其结果作为字符串获取,并且我知道未修饰的A.add_numbers()函数返回其参数的总和。 So by transitivity, the decorated version of A.add_numbers() must return the sum of its argument, as a string. 因此,通过传递性, A.add_numbers()的修饰版本必须以字符串形式返回其参数的总和。 All seems good ! 一切都好看!

However I'm not entirely satisfied with this: my tests, as I wrote them could still pass if I were to use another decorator (that does something else, say multiply the result by 2 instead of casting to a str ). 但是我对此并不完全满意:我的测试,正如我写的那样,如果我要使用另一个装饰器(那就做其他事情,比如将结果乘以2而不是转换为str ),我的测试仍然可以通过。 My function A.add_numbers would not be correct anymore yet the tests would still pass. 我的函数A.add_numbers不再正确,但测试仍然会通过。 Not awesome. 不太棒。

I could test the decorated version of A.add_numbers() but then I would overtest things since my decorator is already unit-tested. 我可以测试A.add_numbers()的装饰版本但是我会因为我的装饰器已经过单元测试而A.add_numbers()

It feels like I'm missing something here. 感觉我在这里遗漏了一些东西。 What is a good strategy to unit-test decorated functions ? 单元测试装饰功能的好策略是什么?

I ended up splitting my decorators in two. 我最后将装饰师分成两部分。 So instead of having: 所以不要:

def stringify(func):
    @wraps(func)
    def wrapper(*args):
        return str(func(*args))

    return wrapper

I have: 我有:

def to_string(value):
    return str(value)

def stringify(func):
    @wraps(func)
    def wrapper(*args):
        return to_string(func(*args))

    return wrapper

Which allows me later to simply mock-out to_string when testing the decorated function. 这允许我稍后在测试装饰函数时简单地模拟出to_string

Obviously in this simple example case it might seem overkill, but when used over a decorator that actually does something complex or expensive (like opening a connection to a DB, or whatever), being able to mock it out is a very nice thing. 显然在这个简单的例子中它可能看起来有点过分,但是当在装饰器上使用时实际上做了复杂或昂贵的事情(比如打开与DB的连接,或者其他),能够模拟出来是一件非常好的事情。

Test the public interface of your code. 测试代码的公共接口 If you only expect people to call the decorated functions, then that's what you should test. 如果你只希望人们调用装饰函数,那么你应该测试它。 If the decorator is also public, then test that too (like you did with test_stringify() ). 如果装饰器也是公共的,那么也要测试它(就像你使用test_stringify() )。 Don't test the wrapped versions unless people are directly calling them. 除非人们直接调用它们,否则不要测试包装版本。

One of the major benefits of unit testing is to allow refactoring with some degree of confidence that the refactored code continues to work the same as it did previously. 单元测试的主要好处之一是允许重构具有一定程度的信心,即重构代码继续像以前一样工作。 Suppose you had started with 假设你已经开始了

def add_numbers(a, b):
    return str(a + b)

def mult_numbers(a, b):
    return str(a * b)

You would have some tests like 你会有一些测试

def test_add_numbers():
    assert add_numbers(3, 5) == "8"

def test_mult_numbers():
    assert mult_numbers(3, 5) == "15"

Now, you decide to refactor the common parts of each function (wrapping the output in a string), using your stringify decorator. 现在,您决定使用stringify装饰器重构每个函数的公共部分(将输出包装在一个字符串中)。

def stringify(func):
    @wraps(func)
    def wrapper(*args):
        return str(func(*args))

    return wrapper

@stringify
def add_numbers(a, b):
    return a + b

@stringify
def mult_numbers(a, b):
    return a * b

You'll notice that your original tests continue to work after this refactoring. 您会注意到在重构之后您的原始测试仍然有效。 It doesn't matter how you implemented add_numbers and mult_numbers ; 如何实现add_numbersmult_numbers并不重要; what matters is they continue to work as defined: returing a stringified result of the desired operation. 重要的是它们继续按照定义工作:重新获得所需操作的字符串化结果。

The only remaining test you need to write is one to verify that stringify does what it is intended to do: return the result of the decorated function as a string, which your test_stringify does. 唯一剩下的测试,你需要写一个验证stringify完成打算做:返回装饰功能作为一个字符串,你的结果test_stringify一样。


Your issue seems to be that you want to treat the unwrapped function, the decorator, and the wrapped function as units. 您的问题似乎是您希望将展开的函数,装饰器包装函数视为单位。 But if that's the case, then you are missing one unit test: the one that actually runs add_wrapper and tests its output, rather than just add_wrapper.__wrapped__ . 但如果是这种情况,那么你就缺少一个单元测试:一个实际运行add_wrapper并测试其输出的单元测试,而不仅仅是add_wrapper.__wrapped__ It doesn't really matter if you consider testing the wrapped function as a unit test or an integration test, but whatever you call it, you need to write it, because as you pointed out, it's not sufficient to test just the unwrapped function and the decorator separately. 如果你考虑将包装函数作为单元测试或集成测试进行测试并不重要,但无论你怎么称呼它,你都需要编写它,因为正如你所指出的,仅测试未包装的函数和装饰师分开。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM