是否可以创建一个TestCase
具有一些 test_* 方法的 abstract ,但这TestCase
不会被调用,并且这些方法只会在子类中使用?我想我将TestCase
在我的测试套件中拥有一个抽象,它将为单个接口的几个不同实现进行子类化。这就是为什么所有的测试方法都是一些,只有一种,内部方法变化。我怎样才能以优雅的方式做到这一点?
12 回答
我不太明白你打算做什么——经验法则是“不要对测试很聪明”——只要把它们放在那里,写得很清楚。
但是为了实现你想要的,如果你从 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
默认覆盖,正如下面的评论中所指出的那样。
到目前为止,每个人都错过了一种非常简单的方法。与几个答案不同的是,它适用于所有测试驱动程序,而不是在您在它们之间切换的那一刻失败。
像往常一样简单地使用继承,然后添加:
del AbstractTestCase
在模块的末尾。
多重继承在这里不是一个很好的选择,主要有以下两个原因:
- 没有
TestCase
使用任何方法,super()
因此您必须首先列出您的类以获取类似setUp()
和tearDown()
工作的方法。 - pylint 将警告基类使用了当时未定义的
self.assertEquals()
etc。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()
如果您真的想使用继承而不是 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
只需投入我的两分钱,尽管它可能违反某些约定,但您可以将抽象测试用例定义为受保护的成员以防止其执行。我在 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)
加注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'
如果您需要使用定义自己的自定义 TestCasesetUpClass()
并且需要调用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
如果您遵循在 run_unittest 中明确列出所有测试类的约定(参见例如 Python 测试套件以了解该约定的许多用途),那么不列出特定类将是直截了当的。
如果您想继续使用 unittest.main,并且您可以允许使用 unittest2(例如从 Python 2.7 开始),您可以使用它的load_tests协议来指定哪些类包含测试用例)。在早期版本中,您必须继承 TestLoader 并覆盖loadTestsFromModule。
Python unittest 库有load_tests 协议,可以用来实现你想要的:
# 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)
该unittest
模块提供了几个跳过测试的选项。
我首选的解决方案是重写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()
想要做 OP 正在做的事情的另一个原因是创建一个高度参数化的基类,它实现了一组需要在多个环境/场景中重现的核心测试。我所描述的本质上是使用 unittest 创建一个参数化的fixture,一个 la pytest。
假设您(像我一样)决定尽可能快地逃离任何基于多重继承的解决方案,使用 load_tests() 从加载的套件中过滤掉您的基类可能会遇到以下问题:
在标准的 TestLoader 中,load_tests在自动加载类完成后被调用。因为: * 这个自动加载类将尝试使用标准签名init (self, name) 从你的基类构造实例,并且 * 你可能希望这个基类有一个非常不同的 ctor 签名,或者 * 你由于某些其他原因,可能想要跳过基类实例的构造然后删除
..您可能希望完全阻止从基类自动加载测试实例。
编辑:Vadim 在这个其他线程中的解决方案是一种更优雅、简洁和独立的方式来做到这一点。我已经实现了“嵌套类技巧”并确认它可以很好地工作,以防止 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。
这是一种相对简单的方法,它允许您的常见测试从 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
文档,“TestCase 的每个实例都将运行一个基本方法:名为 methodName 的方法。” 默认的“runTests”运行类上的所有 test* 方法——这就是 TestCase 实例正常工作的方式。但是当在抽象基类本身中运行时,您可以简单地用一个什么都不做的方法覆盖该行为。
副作用是您的测试计数将增加一:runNoTestsInBaseClass“测试”在 CommonTestCases 上运行时被计为成功测试。
我已经按照以下方式完成了,也许它可以启发你:
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 测试。