【问题标题】:abstract test case using python unittest使用 python unittest 抽象测试用例
【发布时间】:2011-06-01 19:14:09
【问题描述】:

是否可以创建一个抽象的TestCase,它会有一些test_* 方法,但是这个TestCase 不会被调用并且这些方法只会在子类中使用?我想我将在我的测试套件中有一个抽象的TestCase,它将被子类化为单个接口的几个不同的实现。这就是为什么所有的测试方法都是一些,只有一种,内部方法变化。我怎样才能优雅地做到这一点?

【问题讨论】:

  • 如果您使用鼻子运行测试,这可能会更容易一些。见Finding and running tests。例如,您可以使用鼻子将__test__=False 放入您的基类中。
  • 在我的抽象测试用例上使用skip怎么样?
  • @bstpierre 很好的建议。这对我来说非常有效

标签: python unit-testing testcase


【解决方案1】:

我不太明白你打算做什么—— 经验法则是“不要聪明地测试” - 把它们放在那里,简单明了。

但是为了实现你想要的,如果你从 unittest.TestCase 继承,每当你调用 unittest.main() 你的“抽象”类都会被执行——我认为这是你想要避免的情况。

只需这样做: 创建继承自“对象”而不是 TestCase 的“抽象”类。 对于实际的“具体”实现,只需使用多重继承: 继承自 unittest.TestCase 和抽象类。

import unittest

class Abstract(object):
    def test_a(self):
        print "Running for class", self.__class__

class Test(Abstract, unittest.TestCase):
    pass

unittest.main()

更新:颠倒了继承顺序 - 首先是 Abstract,这样它的定义就不会被 TestCase 默认值覆盖,下面的 cmets 也指出了这一点。

【讨论】:

  • 这就是我最初想要做的。我想变得聪明,因为我正在针对不同的数据库运行我的测试。访问数据库的接口总是相同的,所以我只需要实例化我与数据库的连接,并且总是运行相同的测试。我不知道,这对于测试来说是否太聪明了,但我只是不喜欢输入很多 ;-)
  • 哇,多年来 C++ 中多重继承的疯狂让我害怕 MI(为我辩护,我继承了代码库——邪恶早于我的参与)。现在我看到了一个实际有效的用途。
  • 注意一个常见的陷阱:如果您在 Abstract 中覆盖 TestCase 方法(覆盖“setUp”是常见的情况),您希望反转基类的顺序 - class Test(Abstract, unittest.TestCase)。否则 unittest.TestCase 中的空 setUp 方法优先于 Abstract 中实现的方法。
  • @Yonatan 提出了一个很好的观点,颠倒继承顺序似乎是一个更明智的默认设置。为什么你会希望你的自定义基类被TestCase 覆盖?
  • 这通常被称为SomethingMixin。
【解决方案2】:

到目前为止,每个人都错过了一个非常简单的方法。与其他几个答案不同的是,它适用于所有测试驱动程序,而不是在您切换它们的那一刻失败。

照常使用继承,然后添加:

del AbstractTestCase

在模块的末尾。

【讨论】:

  • 有什么缺点吗?
  • @MaxMalysh 您不能对超类使用旧式调用。但是super 仍然有效,每个人都应该真的使用它。
  • 在我发现的所有解决方案中,我最喜欢这个。 Mixin 要么缺乏对断言的支持,要么如果您使用它们,它们可能在运行时可用,但它们确实会在 IDE 中引发警告。模块末尾的 del 语句可能很容易被忽略,其意图并不那么明显,但我仍然认为这是所有选项中最干净的,因为实际上只有几个变量会发生变化并且所有测试都保持不变。或者您在此期间提出了更好的解决方案?
  • 如果您担心忽略del,只需在类定义旁边添加注释即可。
  • 请扩展这个答案——我差点错过了。解释这是如何解决的:TypeError: Can't instantiate abstract class AbstractTestCase with abstract methods my_help_method,... 解释将 del 放在哪里(在继承基础的模块中)以及为包含基础的模块命名以使其被忽略。
【解决方案3】:

在这里,多重继承并不是一个好的选择,主要有以下两个原因:

  1. TestCase 中的所有方法都没有使用 super(),因此您必须首先列出您的类,setUp() 和 tearDown() 等方法才能工作。
  2. pylint 会警告基类使用 self.assertEquals() 等,而这些在 self 上尚未定义。

这是我想出的问题:将 run() 变成仅用于基类的无操作。

class TestBase( unittest.TestCase ):

  def __init__( self, *args, **kwargs ):
    super( TestBase, self ).__init__( *args, **kwargs )
    self.helper = None
    # Kludge alert: We want this class to carry test cases without being run
    # by the unit test framework, so the `run' method is overridden to do
    # nothing.  But in order for sub-classes to be able to do something when
    # run is invoked, the constructor will rebind `run' from TestCase.
    if self.__class__ != TestBase:
      # Rebind `run' from the parent class.
      self.run = unittest.TestCase.run.__get__( self, self.__class__ )                          
    else:
      self.run = lambda self, *args, **kwargs: None

  def newHelper( self ):
    raise NotImplementedError()

  def setUp( self ):
    print "shared for all subclasses"
    self.helper = self.newHelper()

  def testFoo( self ):
    print "shared for all subclasses"
    # test something with self.helper

class Test1( TestBase ):
  def newHelper( self ):
    return HelperObject1()

class Test2( TestBase ):
  def newHelper( self ):
    return HelperObject2()

【讨论】:

  • (1) 并不是一个真正的问题 - 显而易见的事情是让你的班级排在第一位并使用 super (我的答案是倒置的 - 我只是更正了它)。 (2) 你使用的词就是这么说的——你必须使用“kludge”而不是使用 Python 的 OOP 中内置的干净机制,因为第 3 方 linting 工具无法正确内省。这是您对工具的选择。
【解决方案4】:

如果你真的想使用继承而不是 mixins,一个简单的解决方案是将抽象测试嵌套在另一个类中。

它避免了测试运行器发现问题,您仍然可以从另一个模块导入抽象测试。

import unittest

class AbstractTests(object):
    class AbstractTest(unittest.TestCase)
        def test_a(self):
            print "Running for class", self.__class__

class Test(AbstractTests.AbstractTest):
    pass

【讨论】:

  • 你在读我的心!我刚刚在另一个线程上找到了 Vadim 的解决方案,并完全同意这更优雅,因为它不需要对 TestLoader 进行任何修改。我已经撤消了我的 TestLoader 更改并测试了嵌套类技巧。谢谢! stackoverflow.com/a/25695512/532621
  • 完成。还编辑了我关于嵌套类更好的答案。谢谢。
【解决方案5】:

只是我的两分钱,虽然它可能违反某些约定,但您可以将您的抽象测试用例定义为受保护的成员以防止其执行。我在 Django 中实现了以下功能并按要求工作。请参阅下面的示例。

from django.test import TestCase


class _AbstractTestCase(TestCase):

    """
    Abstract test case - should not be instantiated by the test runner.
    """

    def test_1(self):
        raise NotImplementedError()

    def test_2(self):
        raise NotImplementedError()


class TestCase1(_AbstractTestCase):

    """
    This test case will pass and fail.
    """

    def test_1(self):
        self.assertEqual(1 + 1, 2)


class TestCase2(_AbstractTestCase):

    """
    This test case will pass successfully.
    """

    def test_1(self):
        self.assertEqual(2 + 2, 4)

    def test_2(self):
        self.assertEqual(12 * 12, 144)

【讨论】:

  • 这似乎效果很好,而且比上面的猴子补丁方法简单得多。
  • 它可能适用于 django(在我的情况下,适用于nose),但 unittest 发现机制似乎仍然可以找到这些抽象测试用例...
【解决方案6】:

在setUpClass() 中提高unittest.SkipTest

另一种方法是在基类的setUpClass() 中引发unittest.SkipTest 并在子类中覆盖setUpClass():

class BaseTestCase(TestCase):
    @classmethod
    def setUpClass(cls):
        "Child classes must override this method and define cls.x and cls.y"
        raise unittest.SkipTest

    def test_x(self):
        self.assertEqual(self.x * 3, self.x)

    def test_y(self):
        self.assertEqual(self.y * 3, self.y + self.y + self.y)

    def test_z(self):
        self.assertEqual(self.x + self.y, self.y)


class IntegerTestCase(BaseTestCase):
    @classmethod
    def setUpClass(cls):
        cls.x = 0
        cls.y = 2


class StringTestCase(BaseTestCase):
    @classmethod
    def setUpClass(cls):
        cls.x = ''
        cls.y = 'zuzuka'

如果您需要使用定义了自己的setUpClass() 的自定义TestCase 并且需要调用super().setUpClass(),您可以定义自己的方法来“设置数据”并仅在该方法内引发SkipTest:

class BaseTestCase(ThidPartyTestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()  # if ThirdPartyTestCase has own setUpClass()
        cls.setUpTestCaseData()

    @classmethod
    def setUpTestCaseData(cls):
        "Override and set up cls.x and cls.y here"
        raise unittest.SkipTest

    ...  # tests


class IntegerTestCase(BaseTestCase):
    @classmethod
    def setUpTestCaseData(cls):
        cls.x = 0
        cls.y = 2

【讨论】:

  • 我觉得这里多余的setUpTestCaseData方法有点多余。请参阅my answer 以获得更清洁的解决方案。
【解决方案7】:

如果您遵循在 run_unittest 中明确列出所有测试类的约定(参见例如 Python 测试套件了解该约定的许多用途),那么将直接不列出特定的类。

如果你想继续使用 unittest.main,并且你可以允许使用 unittest2(例如从 Python 2.7 开始),你可以使用它的load_tests 协议来指定哪些类包含测试用例)。在早期版本中,您必须继承 TestLoader,并覆盖 loadTestsFromModule。

【讨论】:

  • 我不会失去从命令行调用这些测试的能力吗?我想默认运行所有测试(抽象测试用例中的测试除外),但在某些情况下只运行其中的一部分。
【解决方案8】:

Python unittest 库有load_tests protocol,可以用来实现你想要的:

# Add this function to module with AbstractTestCase class
def load_tests(loader, tests, _):
    result = []
    for test_case in tests:
        if type(test_case._tests[0]) is AbstractTestCase:
            continue
        result.append(test_case)
    return loader.suiteClass(result)

【讨论】:

    【解决方案9】:

    unittest 模块为skipping tests 提供了几个选项。

    我的首选解决方案是重写“抽象”基类中的setUpClass 方法,以便在需要时引发unittest.SkipTest 异常:

    class BaseTestCase(unittest.TestCase):
      @classmethod
      def setUpClass(cls):
        if cls is BaseTestCase:
          raise unittest.SkipTest("%s is an abstract base class" % cls.__name__)
        else:
          super(BaseTestCase, cls).setUpClass()
    

    【讨论】:

      【解决方案10】:

      想要做 OP 正在做的事情的另一个原因是创建一个高度参数化的基类,它实现了一组需要在多个环境/场景中重现的核心测试。我所描述的本质上是使用 unittest 创建一个参数化的fixture,一个 la pytest。

      假设您(像我一样)决定尽可能快地逃离任何基于多重继承的解决方案,使用 load_tests() 从加载的套件中过滤掉您的基类时可能会遇到以下问题:

      在标准的TestLoader 中,load_tests 被调用在自动加载类完成之后。因为: * 此自动加载类将尝试使用标准签名 init(self, name) 从您的基类构造实例,并且 * 你可能希望这个基类有一个非常不同的 ctor 签名,或者 * 您可能出于其他原因希望跳过构建然后删除基类实例

      .. 您可能希望完全阻止从基类自动加载测试实例。

      编辑:Vadim's solution in this other thread 是一种更优雅、简洁和独立的方式来执行此操作。我已经实现了“嵌套类技巧”,并确认它可以很好地防止 TestLoader “找到”您的 TestCase 基础。

      我最初是通过修改 TestLoader.loadTestsFromModule 来简单地跳过作为模块中任何其他 TestCase 类的基类的任何 TestCase 类来做到这一点的:

      for name in dir(module):
          obj = getattr(module, name)
          # skip TestCase classes:
          # 1. without any test methods defined
          # 2. that are base classes
          #    (we don't allow instantiating TestCase base classes, which allows test designers
          #     to implement actual test methods in highly-parametrized base classes.)
          if isinstance(obj, type) and issubclass(obj, unittest.TestCase) and \
                  self.getTestCaseNames(obj) and not isbase(obj, module):
              loaded_suite = self.loadTestsFromTestCase(obj)
              # ignore empty suites
              if loaded_suite.countTestCases():
                  tests.append(loaded_suite)
      

      地点:

      def isbase(cls, module):
          '''Returns True if cls is base class to any classes in module, else False.'''
          for name in dir(module):
              obj = getattr(module, name)
              if obj is not cls and isinstance(obj, type) and issubclass(obj, cls):
                  return True
          return False
      

      我上面提到的参数化是通过让每个子类定义它的夹具细节(参数)并将它们传递给基类 TestCase ctor 来实现的,这样它的所有公共 impl 方法(“fixturey”那些 setUp*/ tearDown*/cleanup* 和测试方法本身)具有定义该子 TestCase 类要操作的现在非常具体的夹具的所有信息。

      对我来说,这是在 unittest 中快速实现一些参数化固定装置的临时解决方案,因为我计划尽快将团队的测试转移到 pytest。

      【讨论】:

        【解决方案11】:

        这是一种相对简单的方法,它允许您的常见测试从 TestCase 继承(因此类型检查和 IDE 工具保持良好状态),仅使用文档化的单元测试功能,并避免“跳过”测试状态:

        import unittest
        
        class CommonTestCases(unittest.TestCase):
            def __init__(self, methodName='runTest'):
                if self.__class__ is CommonTestCases:
                    # don't run these tests on the abstract base implementation
                    methodName = 'runNoTestsInBaseClass'
                super().__init__(methodName)
        
            def runNoTestsInBaseClass(self):
                print('not running tests in abstract base class')
                pass
        
            def test_common(self):
                # This will run *only* in subclasses. Presumably, this would 
                # be a test you need to repeat in several different contexts.
                self.assertEqual(2 + 2, 4)
        
        
        class SomeTests(CommonTestCases):
            # inherited test_common *will* be run here
        
            def test_something(self):
                self.assertTrue(True)
        
        
        # Also plays nicely with MRO, if needed:
        class SomeOtherTests(CommonTestCases, django.test.SimpleTestCase):
            # inherited test_common *will* be run here
        
            def test_something_else(self):
                self.client.get('/')  # ...
        

        它是如何工作的:根据unittest.TestCase documentation,“TestCase 的每个实例都将运行一个基本方法:名为 methodName 的方法。”默认的“runTests”运行类上的所有 test* 方法——这就是 TestCase 实例正常工作的方式。但是,当在抽象基类本身中运行时,您可以简单地使用不执行任何操作的方法覆盖该行为。

        副作用是您的测试计数将增加一:runNoTestsInBaseClass“测试”在 CommonTestCases 上运行时被计为成功测试。

        【讨论】:

          【解决方案12】:

          我已经按照以下方式完成了,也许它可以启发你:

          class AbstractTest(TestCase):
              def setUp(self):
                  pass
          
              def tearDown(self):
                  pass
          
              def _test_1(self):
                  # your test case here
          
          class ConcreteTest(AbstractTest)
          
              def test_1(self):
                  self._test_1()
          

          虽然它不是最方便的解决方案,但它可以让您摆脱多重继承。此外,Dan Ward 建议的解决方案不适用于 PyCharm 中的 Django 测试。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2018-03-24
            • 2014-01-04
            • 2014-02-27
            • 2021-09-20
            • 1970-01-01
            相关资源
            最近更新 更多