使用python unittest的抽象测试用例

时间:2010-12-30 22:54:30

标签: python unit-testing testcase

是否可以创建一个抽象的TestCase,它将有一些test_ *方法,但是这个TestCase将不会被调用,那些方法只会在子类中使用?我想我将在我的测试套件中有一个抽象TestCase,它将被子类化为单个界面的一些不同实现。这就是为什么所有测试方法都是一些,只有一个内部方法发生变化的原因。我怎么能以优雅的方式做到这一点?

12 个答案:

答案 0 :(得分:60)

我不太明白你打算做什么 - 经验法则是“不要用测试来聪明” - 只是把它们放在那里,写得很清楚。

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

这样做: 创建继承自“object”的“抽象”类,而不是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默认值覆盖,并在下面的评论中指出。

答案 1 :(得分:11)

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

  1. TestCase中的所有方法均未使用super(),因此您必须首先列出您的课程,以便setUp()tearDown()等方法发挥作用。
  2. pylint将警告基类使用self.assertEquals()等,此时self未定义。
  3. 以下是我提出的问题:将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()
    

答案 2 :(得分:6)

只是为了加入我的2美分,虽然它可能违反某些约定,但您可以将您的抽象测试用例定义为受保护的成员以防止其执行。我在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)

答案 3 :(得分:5)

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

像往常一样使用继承,然后添加:

del AbstractTestCase

在模块的最后。

答案 4 :(得分:4)

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

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

答案 5 :(得分:4)

提高unittest.SkipTest中的setUpClass()

另一种方法是在基础类的unittest.SkipTest中引发一个setUpClass()并在子类中覆盖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

答案 6 :(得分:2)

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)

答案 7 :(得分:1)

如果你真的想使用继承而不是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

答案 8 :(得分:1)

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()

答案 9 :(得分:0)

想要执行OP正在执行的操作的另一个原因是创建一个高度参数化的基类,该基类实现了许多核心测试的大部分,这些核心测试需要在几种环境/场景中进行重现。我要描述的实际上是使用unittest创建一个参数化的夹具,一个pytest。

假设您(像我一样)决定从任何基于多重继承的解决方案中尽快逃脱,那么使用load_tests()从加载的套件中过滤基类可能会有以下问题:< / p>

在标准TestLoader中,在完成“自动加载类”之后,将load_tests称为 。因为: *此自动加载自类将尝试使用标准签名 init ((自身,名称))从您的基类构造实例,并且 *您可能希望此基类具有完全不同的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方法(“固定装置”的setUp * / tearDown * / cleanup * 测试方法本身)具有所有信息,这些信息定义了子TestCase类要在其上运行的非常特定的固定装置。

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

答案 10 :(得分:0)

我已经按照以下方式进行了操作,也许可以激发您的灵感:

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测试。

答案 11 :(得分:0)

这是一种相对简单的方法,它允许您的通用测试从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“ test”在CommonTestCases上运行时被视为成功测试。