单元测试方法上的装饰器工厂

Posted

技术标签:

【中文标题】单元测试方法上的装饰器工厂【英文标题】:Decorator factory on a unittest method 【发布时间】:2017-04-13 11:48:44 【问题描述】:
def register_processor2(processor_name='SomeProcessor'):
    def decorator(func):
        class SomeProcessor(GenericPaymentProcessor, TriggeredProcessorMixin):
            name = processor_name
            transaction_class = Transaction

            @staticmethod
            def setup(data=None):
                pass

        @wraps(func)
        def func_wrapper(*args, **kwargs):
            PaymentProcessorManager.register(SomeProcessor)
            result = func(*args, **kwargs)
            PaymentProcessorManager.unregister(SomeProcessor)
            return result

        return func_wrapper
    return decorator


def register_processor(func):
    class SomeProcessor(GenericPaymentProcessor, TriggeredProcessorMixin):
         name = 'SomeProcessor'
         transaction_class = Transaction

         @staticmethod
         def setup(data=None):
             pass

    @wraps(func)
    def func_wrapper(*args, **kwargs):
        PaymentProcessorManager.register(SomeProcessor)
        result = func(*args, **kwargs)
        PaymentProcessorManager.unregister(SomeProcessor)
        return result

    return func_wrapper


class TestPaymentMethodEndpoints(APITestCase):
    @register_processor
    def test_put_detail_cannot_change_processor(self):
        self.assertEqual(True, False)

好的,所以装饰器register_processor 按预期工作。并且测试失败了,但是我想让内部类的名称可以自定义,所以我选择了一个装饰器工厂实现。

问题是在运行带有register_processor2 装饰的测试时,我得到以下信息:

AttributeError: 'TestPaymentMethodEndpoints' object has no attribute '__name__'

这是来自@wraps(func),我的问题是为什么func 这里是TestPaymentMethodEndpoints 的实例,而不是绑定方法?

此外,如果我删除 @wraps 装饰器,那么 测试会运行并通过。 我希望不会发现测试,因为 func_wrapper 不是以 test_* 开头的,即使发现它也应该失败。

对正在发生的事情以及我将如何做这件事有任何见解吗?

编辑

所以我想通了,即使装饰器工厂的参数具有默认值,您仍然需要在调用它时放置 ()

但仍然希望听到有关测试通过/被发现时发生的情况的解释。

class TestPaymentMethodEndpoints(APITestCase):
    @register_processor()
    def test_put_detail_cannot_change_processor(self):
        self.assertEqual(True, False)

现在想想就明白了:D,天哪,你每天都能学到新东西!

【问题讨论】:

【参考方案1】:

我想你现在在问“unittest 模块为什么可以找到已包装在名称不以 test 开头的函数中的测试用例?”

答案是因为unittest 不使用函数的名称 来查找要运行的方法,它使用测试的属性名称案例类来找到它们。

所以尝试运行以下代码:

from unittest import TestCase

def apply_fixture(func):

    def wrap_with_fixture(self):
        print('setting up fixture...')
        try:
            func(self)
        finally:
            print('tearing down fixture')

    return wrap_with_fixture


class MyTestCase(TestCase):

    @apply_fixture
    def test_something(self):
        print('run test')


print('Attributes of MyTestCase: %s' % dir(MyTestCase))
print('test_something method: %s' % MyTestCase.test_something)

mtc = MyTestCase()
mtc.test_something()

您将看到dir 的输出包含名称test_something

Attributes of MyTestCase: ['__call__', ...lots of things..., 'test_something']

但是那个属性的值是包装函数wrap_with_fixture:

test_something method: <function apply_fixture.<locals>.wrap_with_fixture at 0x10d90aea0>

当您考虑到当您创建一个函数时,您会创建一个具有提供的名称的函数和一个具有相同名称的局部变量,并且装饰器@ 语法只是语法糖,所以这是有道理的,所以以下将是一种同样有效的创建测试用例类的方法,尽管方法比较冗长:

class MyTestCase(TestCase):

    def test_something(self):
        print('run test')
    # Overwrite existing 'local' (or 'class' variable in this context) 
    # with a new value. We haven't deleted the test_something function
    # which still exists but now is owned by the function we've created.
    test_something = apply_fixture(test_something)

【讨论】:

好吧,这是有道理的,但为什么测试首先通过了:D。我的意思是不应该。 您是否可能对装饰工厂后何时以及何时不放置括号感到困惑?您可能已经创建了一个实际上只是一个装饰器函数的测试用例,例如def decorator_factory(*args, **kwargs): ... return decorator,然后是@decorator_factory def test_method(self): self.assertEqual(True, False),所以这里使用test_method 调用装饰器工厂并返回一个用作测试用例的装饰器,但这只是创建了一个包装测试的新函数,实际上并没有运行测试,所以似乎通过了。 无论是这样还是在运行测试时,它都会捕获断言错误并将失败的测试存储在基于测试函数 name 的某种字典中,该函数不会在字典中所以它忽略它。无论如何,我基本上掌握了它的要点,谢谢。

以上是关于单元测试方法上的装饰器工厂的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 @Transaction() 装饰器为方法编写单元测试?

无效!有装饰器时不运行单元测试

Python单元测试--使用装饰器实现测试跳过和预期故障

Python:如何在单元(鼻子)测试期间忽略装饰器?

在单元测试中模拟认证装饰器

如何用 Jest 单元测试覆盖 TypeORM @Column 装饰器?