带有基类和子类的 Python 单元测试
Posted
技术标签:
【中文标题】带有基类和子类的 Python 单元测试【英文标题】:Python unit test with base and sub class 【发布时间】:2010-11-22 08:20:28 【问题描述】:我目前有一些单元测试共享一组通用测试。这是一个例子:
import unittest
class BaseTest(unittest.TestCase):
def testCommon(self):
print 'Calling BaseTest:testCommon'
value = 5
self.assertEquals(value, 5)
class SubTest1(BaseTest):
def testSub1(self):
print 'Calling SubTest1:testSub1'
sub = 3
self.assertEquals(sub, 3)
class SubTest2(BaseTest):
def testSub2(self):
print 'Calling SubTest2:testSub2'
sub = 4
self.assertEquals(sub, 4)
if __name__ == '__main__':
unittest.main()
上面的输出是:
Calling BaseTest:testCommon
.Calling BaseTest:testCommon
.Calling SubTest1:testSub1
.Calling BaseTest:testCommon
.Calling SubTest2:testSub2
.
----------------------------------------------------------------------
Ran 5 tests in 0.000s
OK
有没有办法重写上面的代码,使第一个testCommon
不被调用?
编辑: 而不是运行上面的 5 个测试,我希望它只运行 4 个测试,2 个来自 SubTest1,另外 2 个来自 SubTest2。似乎 Python unittest 正在自行运行原始 BaseTest,我需要一种机制来防止这种情况发生。
【问题讨论】:
我看到没有人提到它,但是您是否可以选择更改主要部分并运行包含 BaseTest 的所有子类的测试套件? 【参考方案1】:不要使用多重继承,它会咬你later。
相反,您可以将基类移动到单独的模块中或用空白类包装它:
class BaseTestCases:
class BaseTest(unittest.TestCase):
def testCommon(self):
print('Calling BaseTest:testCommon')
value = 5
self.assertEqual(value, 5)
class SubTest1(BaseTestCases.BaseTest):
def testSub1(self):
print('Calling SubTest1:testSub1')
sub = 3
self.assertEqual(sub, 3)
class SubTest2(BaseTestCases.BaseTest):
def testSub2(self):
print('Calling SubTest2:testSub2')
sub = 4
self.assertEqual(sub, 4)
if __name__ == '__main__':
unittest.main()
输出:
Calling BaseTest:testCommon
.Calling SubTest1:testSub1
.Calling BaseTest:testCommon
.Calling SubTest2:testSub2
.
----------------------------------------------------------------------
Ran 4 tests in 0.001s
OK
【讨论】:
这是我的最爱。这是最简单的方法,不会干扰覆盖方法,不会改变 MRO,并允许我在基类中定义 setUp、setUpClass 等。 我真的不明白(魔法从何而来?),但在我看来,这是最好的解决方案 :) 来自 Java,我讨厌多重继承...... @Edouardb unittest 仅运行从 TestCase 继承的模块级类。但是 BaseTest 不是模块级的。 作为一个非常相似的替代方案,您可以在一个无参数函数中定义 ABC,该函数在调用时返回 ABC【参考方案2】:使用多重继承,因此您的具有常见测试的类本身不会从 TestCase 继承。
import unittest
class CommonTests(object):
def testCommon(self):
print 'Calling BaseTest:testCommon'
value = 5
self.assertEquals(value, 5)
class SubTest1(unittest.TestCase, CommonTests):
def testSub1(self):
print 'Calling SubTest1:testSub1'
sub = 3
self.assertEquals(sub, 3)
class SubTest2(unittest.TestCase, CommonTests):
def testSub2(self):
print 'Calling SubTest2:testSub2'
sub = 4
self.assertEquals(sub, 4)
if __name__ == '__main__':
unittest.main()
【讨论】:
这是迄今为止最优雅的解决方案。 如果您颠倒基类的顺序,此方法仅适用于 setUp 和 tearDown 方法。因为方法是在unittest.TestCase中定义的,而且它们不调用super(),那么CommonTests中的setUp和tearDown方法都需要在MRO中排在第一位,否则根本不会被调用。 只是为了澄清 Ian Clelland 的言论,以便像我这样的人更清楚:如果您将setUp
和 tearDown
方法添加到 CommonTests
类,并且您希望它们被调用派生类中的每个测试,您必须颠倒基类的顺序,这样它将是:class SubTest1(CommonTests, unittest.TestCase)
。
我不太喜欢这种方法。这在代码中建立了一个契约,类必须继承自unittest.TestCase
和 CommonTests
。我认为下面的setUpClass
方法是最好的,并且不太容易出现人为错误。要么将 BaseTest 类包装在一个容器类中,这有点 hacky,但避免了测试运行打印输出中的跳过消息。
这个问题是 pylint 很合适,因为 CommonTests
正在调用该类中不存在的方法。【参考方案3】:
你可以用一个命令解决这个问题:
del(BaseTest)
所以代码应该是这样的:
import unittest
class BaseTest(unittest.TestCase):
def testCommon(self):
print 'Calling BaseTest:testCommon'
value = 5
self.assertEquals(value, 5)
class SubTest1(BaseTest):
def testSub1(self):
print 'Calling SubTest1:testSub1'
sub = 3
self.assertEquals(sub, 3)
class SubTest2(BaseTest):
def testSub2(self):
print 'Calling SubTest2:testSub2'
sub = 4
self.assertEquals(sub, 4)
del(BaseTest)
if __name__ == '__main__':
unittest.main()
【讨论】:
BaseTest 在定义时是模块的成员,因此可用作子测试的基类。就在定义完成之前,del() 将其作为成员移除,因此 unittest 框架在模块中搜索 TestCase 子类时将找不到它。 这是一个很棒的答案!我比 @MatthewMarshall 更喜欢它,因为在他的解决方案中,你会从 pylint 得到语法错误,因为标准对象中不存在self.assert*
方法。
如果 BaseTest 在基类或其子类中的任何其他位置被引用,则不起作用,例如在方法覆盖中调用 super() 时:super( BaseTest, cls ).setUpClass( )
@Hannes 至少在 python 3 中,BaseTest
可以通过super(self.__class__, self)
或仅在子类中的super()
引用,尽管apparently not if you were to inherit constructors。当基类需要引用自己时,也许还有这样一个“匿名”的替代方案(我不知道什么时候需要引用自己)。【参考方案4】:
Matthew Marshall 的回答很棒,但它要求您从每个测试用例中的两个类继承,这很容易出错。相反,我使用这个(python>=2.7):
class BaseTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
if cls is BaseTest:
raise unittest.SkipTest("Skip BaseTest tests, it's a base class")
super(BaseTest, cls).setUpClass()
【讨论】:
这很好。有没有办法避免不得不使用跳过?对我来说,skip 是不可取的,用于表示当前测试计划中的问题(无论是代码还是测试)? @ZacharyYoung 我不知道,也许其他答案会有所帮助。 @ZacharyYoung 我已尝试解决此问题,请参阅我的答案。 目前还不清楚从两个类继承什么本质上容易出错 @jwg 看到 cmets 接受的答案 :) 您需要从两个基类继承每个测试类;你需要保持它们的正确顺序;如果你想添加另一个基础测试类,你也需要继承它。 mixins 没有什么问题,但在这种情况下,它们可以用一个简单的跳过来替换。【参考方案5】:您可以在 BaseTest 类中添加 __test__ = False
,但如果添加它,请注意您必须在派生类中添加 __test__ = True
才能运行测试。
import unittest
class BaseTest(unittest.TestCase):
__test__ = False
def testCommon(self):
print 'Calling BaseTest:testCommon'
value = 5
self.assertEquals(value, 5)
class SubTest1(BaseTest):
__test__ = True
def testSub1(self):
print 'Calling SubTest1:testSub1'
sub = 3
self.assertEquals(sub, 3)
class SubTest2(BaseTest):
__test__ = True
def testSub2(self):
print 'Calling SubTest2:testSub2'
sub = 4
self.assertEquals(sub, 4)
if __name__ == '__main__':
unittest.main()
【讨论】:
此解决方案不适用于 unittest 自己的测试发现/测试运行器。 (我相信它需要使用替代的测试运行器,比如鼻子。)【参考方案6】:你想达到什么目的?如果您有通用测试代码(断言、模板测试等),请将它们放在不以test
为前缀的方法中,这样unittest
就不会加载它们。
import unittest
class CommonTests(unittest.TestCase):
def common_assertion(self, foo, bar, baz):
# whatever common code
self.assertEqual(foo(bar), baz)
class BaseTest(CommonTests):
def testCommon(self):
print 'Calling BaseTest:testCommon'
value = 5
self.assertEquals(value, 5)
class SubTest1(CommonTests):
def testSub1(self):
print 'Calling SubTest1:testSub1'
sub = 3
self.assertEquals(sub, 3)
class SubTest2(CommonTests):
def testSub2(self):
print 'Calling SubTest2:testSub2'
sub = 4
self.assertEquals(sub, 4)
if __name__ == '__main__':
unittest.main()
【讨论】:
根据您的建议,common_assertion()在测试子类时还会自动运行吗? @Stewart 不,不会。默认设置是只运行以“test”开头的方法。【参考方案7】:另一种选择是不执行
unittest.main()
你可以使用
suite = unittest.TestLoader().loadTestsFromTestCase(TestClass)
unittest.TextTestRunner(verbosity=2).run(suite)
所以你只执行TestClass
类中的测试
【讨论】:
这是最简单的解决方案。无需修改unittest.main()
收集到默认套件中的内容,而是形成显式套件并运行其测试。【参考方案8】:
马修的答案是我需要使用的答案,因为我还在 2.5 上。 但从 2.7 开始,您可以在任何要跳过的测试方法上使用 @unittest.skip() 装饰器。
http://docs.python.org/library/unittest.html#skipping-tests-and-expected-failures
您需要实现自己的跳过装饰器来检查基本类型。以前没有使用过此功能,但在我看来,您可以使用 BaseTest 作为 marker 类型来调节跳过:
def skipBaseTest(obj):
if type(obj) is BaseTest:
return unittest.skip("BaseTest tests skipped")
return lambda func: func
【讨论】:
【参考方案9】:我想到的一种解决方法是在使用基类时隐藏测试方法。这样就不会跳过测试,因此在许多测试报告工具中测试结果可以是绿色而不是黄色。
相比于 mixin 方法,ide 之类的 PyCharm 不会抱怨基类中缺少单元测试方法。
如果基类继承自此类,则需要重写 setUpClass
和 tearDownClass
方法。
class BaseTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls._test_methods = []
if cls is BaseTest:
for name in dir(cls):
if name.startswith('test') and callable(getattr(cls, name)):
cls._test_methods.append((name, getattr(cls, name)))
setattr(cls, name, lambda self: None)
@classmethod
def tearDownClass(cls):
if cls is BaseTest:
for name, method in cls._test_methods:
setattr(cls, name, method)
cls._test_methods = []
【讨论】:
【参考方案10】:我制作的内容与@Vladim P. (https://***.com/a/25695512/2451329) 大致相同,但稍作修改:
import unittest2
from some_module import func1, func2
def make_base_class(func):
class Base(unittest2.TestCase):
def test_common1(self):
print("in test_common1")
self.assertTrue(func())
def test_common2(self):
print("in test_common1")
self.assertFalse(func(42))
return Base
class A(make_base_class(func1)):
pass
class B(make_base_class(func2)):
def test_func2_with_no_arg_return_bar(self):
self.assertEqual("bar", func2())
我们去吧。
【讨论】:
【参考方案11】:从 Python 3.2 开始,您可以向模块添加 test_loader 函数,以控制测试发现机制找到哪些测试(如果有)。
例如下面将只加载原发帖者的SubTest1
和SubTest2
测试用例,忽略Base
:
def load_tests(loader, standard_tests, pattern):
suite = TestSuite()
suite.addTests([SubTest1, SubTest2])
return suite
应该可以遍历standard_tests
(一个TestSuite
,其中包含默认加载程序找到的测试)并将除Base
之外的所有内容复制到suite
,但是TestSuite.__iter__
的嵌套性质使得复杂得多。
【讨论】:
【参考方案12】:这是一个仅使用记录在案的单元测试功能的解决方案,可避免在测试结果中出现“跳过”状态:
class BaseTest(unittest.TestCase):
def __init__(self, methodName='runTest'):
if self.__class__ is BaseTest:
# don't run these tests in the abstract base implementation
methodName = 'runNoTestsInBaseClass'
super().__init__(methodName)
def runNoTestsInBaseClass(self):
pass
def testCommon(self):
# everything else as in the original question
它是如何工作的:根据unittest.TestCase
documentation,“TestCase 的每个实例都将运行一个基本方法:名为 methodName 的方法。”默认的“runTests”运行类上的所有 test* 方法——这就是 TestCase 实例正常工作的方式。但是当在抽象基类本身中运行时,您可以简单地用一个什么都不做的方法覆盖该行为。
副作用是您的测试计数将增加一:runNoTestsInBaseClass“测试”在 BaseClass 上运行时被计为成功测试。
(这也适用于 Python 2.7,如果您仍在使用它。只需将 super()
更改为 super(BaseTest, self)
。)
【讨论】:
【参考方案13】:只需将 testCommon 方法重命名为其他名称即可。单元测试(通常)会跳过任何没有“测试”的内容。
快速简单
import unittest
class BaseTest(unittest.TestCase):
def methodCommon(self):
print 'Calling BaseTest:testCommon'
value = 5
self.assertEquals(value, 5)
class SubTest1(BaseTest):
def testSub1(self):
print 'Calling SubTest1:testSub1'
sub = 3
self.assertEquals(sub, 3)
class SubTest2(BaseTest):
def testSub2(self):
print 'Calling SubTest2:testSub2'
sub = 4
self.assertEquals(sub, 4)
if __name__ == '__main__':
unittest.main()`
【讨论】:
这将导致在任何一个子测试中都没有运行 methodCommon 测试。【参考方案14】:所以这是一个旧线程,但我今天遇到了这个问题并想到了我自己的破解方法。它使用一个装饰器,当通过基类访问时,该装饰器使函数的值变为无。无需担心 setup 和 setupclass,因为如果基类没有测试,它们将无法运行。
import types
import unittest
class FunctionValueOverride(object):
def __init__(self, cls, default, override=None):
self.cls = cls
self.default = default
self.override = override
def __get__(self, obj, klass):
if klass == self.cls:
return self.override
else:
if obj:
return types.MethodType(self.default, obj)
else:
return self.default
def fixture(cls):
for t in vars(cls):
if not callable(getattr(cls, t)) or t[:4] != "test":
continue
setattr(cls, t, FunctionValueOverride(cls, getattr(cls, t)))
return cls
@fixture
class BaseTest(unittest.TestCase):
def testCommon(self):
print('Calling BaseTest:testCommon')
value = 5
self.assertEqual(value, 5)
class SubTest1(BaseTest):
def testSub1(self):
print('Calling SubTest1:testSub1')
sub = 3
self.assertEqual(sub, 3)
class SubTest2(BaseTest):
def testSub2(self):
print('Calling SubTest2:testSub2')
sub = 4
self.assertEqual(sub, 4)
if __name__ == '__main__':
unittest.main()
【讨论】:
【参考方案15】:将 BaseTest 方法名称更改为 setUp:
class BaseTest(unittest.TestCase):
def setUp(self):
print 'Calling BaseTest:testCommon'
value = 5
self.assertEquals(value, 5)
class SubTest1(BaseTest):
def testSub1(self):
print 'Calling SubTest1:testSub1'
sub = 3
self.assertEquals(sub, 3)
class SubTest2(BaseTest):
def testSub2(self):
print 'Calling SubTest2:testSub2'
sub = 4
self.assertEquals(sub, 4)
输出:
在 0.000 秒内运行 2 次测试
调用 BaseTest:testCommon 调用 SubTest1:testSub1 调用 BaseTest:test 常用调用 SubTest2:testSub2
来自documentation:
TestCase.setUp() 调用的方法 准备测试夹具。这是 在调用之前立即调用 测试方法;引发的任何异常 这种方法将被视为 错误而不是测试失败。这 默认实现什么都不做。
【讨论】:
那行得通,如果我有 n testCommon,我应该把它们都放在setUp
下吗?
是的,您应该将所有不是实际测试用例的代码放在 setUp 下。
但是如果一个子类有多个test...
方法,setUp
会一遍又一遍地执行,每个这样的方法一次;所以把测试放在那里不是一个好主意!
不太确定 OP 在更复杂的场景中执行时想要什么。以上是关于带有基类和子类的 Python 单元测试的主要内容,如果未能解决你的问题,请参考以下文章