使用 Python 的单元测试子测试时避免重复测试?

Avoid repetition in tests when using Python's unittest subTest?

我无法为这种情况想出一个优雅的解决方案。

假设我在 Python 中有一个单元测试,它将测试可迭代对象中的几个元素。由于在内存中构建此可迭代对象的成本很高,因此我只想通过 setUpClass method. Then, in each test, I want to sequentially pass each element in the iterable to the test, which I can to using the context manager and the subTest method 构建它一次。这一切都很好。

以下代码是一个模拟测试用例的示例,它完全符合我所描述的内容:

import unittest

class NumberTest(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        cls.numbers = []
        for x in range(1000):
            cls.numbers.append(x)

    @classmethod
    def tearDownClass(cls):
        del cls.numbers

    def test_number_is_even(self):
        for n in self.numbers:
            with self.subTest(current_number=n):
                self.assertEqual(n % 2, 0)

    def test_number_is_odd(self):
        for n in self.numbers:
            with self.subTest(current_number=n):
                self.assertEqual(n % 2, 1)

    def test_number_is_positive(self):
        for n in self.numbers:
            with self.subTest(current_number=n):
                self.assertTrue(n > 0)

    def test_number_is_negative(self):
        for n in self.numbers:
            with self.subTest(current_number=n):
                self.assertTrue(n < 0)


if __name__ == '__main__':
    unittest.main()

让我感到困扰的是,for n in self.numbers: with self.subTest(current_number=n): . . . 行对每种测试方法都重复了。有什么办法可以避免这种情况吗?我知道我可以简单地将所有 self.assert 语句集中在一个方法中,但这首先违背了对不同方面进行不同测试的目的。

在我看来,"ideal" 解决方案是以某种方式 yield 来自 iterable 的值(也许通过 setUp 方法?没有线索)并将这些值传递给在产生下一个之前的每个测试方法。但我不知道如何实际执行此操作,特别是因为它涉及上下文管理器......有没有人有任何其他解决方案,或者根本没有解决方法?

一个简单的解决方案是将 forwith 语句外包给装饰器。但是每个测试用例仍然需要一行:

from functools import wraps
from unittest import TestCase


def for_each_number():
    def decorator(test_func):
        @wraps(test_func)
        def wrapper(self: TestCase):
            for n in self.numbers:
                with self.subTest(current_number=n):
                    test_func(self, n)

        return wrapper
    return decorator


class NumberTest(TestCase):
    # ...

    @for_each_number()
    def test_number_is_even(self, n):
        self.assertEqual(n % 2, 0)

    @for_each_number()
    def test_number_is_odd(self, n):
        self.assertEqual(n % 2, 1)
    # ...

当然,这 而不是 运行 只是在您的可迭代对象上执行一次,就像您的理想解决方案所做的那样。为此,您需要装饰整个 class。这似乎可以通过 parameterized 库中的 @parameterized_class 装饰器实现。我自己从来没有使用过这个装饰器。