Python基础和子类的单元测试

191

我目前有几个单元测试共享一组公共测试。以下是一个示例:

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 个测试,其中两个来自 SubTest1,另外两个来自 SubTest2。似乎 Python 的 unittest 自己在运行原始的 BaseTest,我需要一种机制来防止这种情况发生。

我看没有人提到,但你有更改主要部分并运行包含所有BaseTest子类的测试套件的选项吗? - kon psych
2022年了,这个问题还没有很好的解决方案吗?多重继承很麻烦,会导致代码检查问题。使用setUpClass并引发SkipTest是相当不错的解决方法,但测试运行器会显示跳过的测试。其他框架通过添加__abstract__ = True来解决这些问题。难道还没有更好的解决方法吗? - Matthew Moisen
15个回答

199

不要使用多重继承,它会在以后捉弄你。

相反,您可以将基类移到单独的模块中或用空白类进行包装:

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

9
这是我的最爱。它是最少hacky的方法,不会干扰覆盖方法,也不会改变MRO,同时允许我在基类中定义setUp、setUpClass等。 - Hannes
6
我真的不理解这里的魔法从哪里来,但在我看来,这是最好的解决方案 :) 作为来自Java的人,我讨厌多继承... - Edouard Berthe
4
@Edouardb 的意思是,unittest 只能运行从 TestCase 继承的模块级别类。但是 BaseTest 不是模块级别的。我的翻译是:unittest 只能运行继承自 TestCase 的模块级别类,而 BaseTest 不属于模块级别。 - JoshB
作为一种非常相似的选择,您可以在无参数函数中定义 ABC,在调用时返回 ABC。 - Anakhand
1
这是一个不错的解决方案,只要unittest的行为像这样(仅运行模块级别的类)。更具未来性的解决方案是在BaseTest中覆盖run(self, result=None)方法: if self.__class__ is BaseTest: return result; else: return super().run(result) - interDist

169

使用多重继承,这样您的具有公共测试的类就不会从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()

30
只有当你颠倒基类的顺序时,这种方法才适用于setUp和tearDown方法。由于这些方法是在unittest.TestCase中定义的,并且它们不调用super(),因此CommonTests中的任何setUp和tearDown方法都需要在MRO中排在第一位,否则它们将根本不会被调用。 - Ian Clelland
36
为了让像我这样的人更明白Ian Clelland的评论,请澄清一下:如果您向CommonTests类添加setUp和tearDown方法,并且希望它们对派生类中的每个测试进行调用,则必须反转基类的顺序,以便为“ class SubTest1(CommonTests,unittest.TestCase)” 。 - Dennis Golomazov
10
我不太喜欢这种方法。这在代码中建立了一个合同,即类必须同时继承unittest.TestCaseCommonTests。我认为下面的setUpClass方法是最好的选择,而且更少出现人为错误。或者将BaseTest类包装在容器类中,虽然有点不太正规,但可以避免测试运行输出中的跳过消息。 - David Sanders
14
问题在于Pylint不通过,因为CommonTests类调用了该类中不存在的方法。 - MadScientist
1
我最喜欢这个。我认为CommonTests不应该继承自TestCase,因为它实际上并不是一个测试用例(即您不希望它与其他测试一起运行)。如果您真的不喜欢需要从两者中继承的“合同”,请考虑使用MixIn:class CommonMixIn(CommonTests, unittest.TestCase): - CodeJockey
显示剩余3条评论

39

你可以通过一条命令解决这个问题:

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

4
BaseTest是模块的成员,同时在定义模块时就已存在,因此可用作SubTests的基类。在定义完成之前,del()将其移除为成员,这样unittest框架在搜索模块中的TestCase子类时就无法找到它了。 - mhsmith
3
这个答案太棒了!我比起 @MatthewMarshall 的更喜欢它,因为在他的解决方案中,您将会从 pylint 得到语法错误,因为 self.assert* 方法在标准对象中不存在。 - SimplyKnownAsG
2
如果BaseTest在基类或其子类中的任何其他地方被引用,例如在方法重写中调用super(),则此代码将无法正常工作:super(BaseTest, cls).setUpClass() - Hannes
2
@Hannes 在Python 3中,BaseTest可以通过子类中的super(self.__class__, self)或者只是super()来引用,尽管显然如果你要继承构造函数就不行。也许当基类需要引用自身时,还有这样的“匿名”替代方案(虽然我不知道什么情况下一个类需要引用自身)。 - Stein

38

马修·马歇尔的回答很好,但它要求在每个测试用例中继承两个类,这样容易出错。相反,我使用以下方法(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()

3
很好。有没有办法避免使用跳过?对我来说,跳过是不可取的,并且用于指示当前测试计划中的问题(无论是代码还是测试)。 - Zach Young
@ZacharyYoung 我不知道,也许其他答案可以帮助你。 - Dennis Golomazov
@ZacharyYoung 我已经尝试修复这个问题了,请看我的回答。 - simonzack
继承两个类存在哪些固有的错误风险并不是很明显。 - jwg
@jwg 请查看已接受答案的评论 :) 您需要从这两个基类中继承您的每个测试类; 您需要保留它们的正确顺序; 如果要添加另一个基本测试类,则还需要从其继承。混入没有任何问题,但在这种情况下,它们可以用简单的跳过替换。 - Dennis Golomazov
1
我喜欢检查是否在BaseTest类中运行的方法。您可以使用一些其他记录的TestCase功能来避免跳过结果。 - medmunds

12

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

2
这个解决方案不能与unittest自己的测试发现/测试运行器一起使用。(我认为它需要使用替代的测试运行器,比如nose。) - medmunds

8

你想要达到什么目的?如果你有通用的测试代码(断言、模板测试等),那么请把它们放到没有前缀为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()

1
在您的建议下,当测试子类时,common_assertion() 仍然会自动运行吗? - Stewart
@Stewart 不会的。默认设置是仅运行以“test”开头的方法。 - C S

6

Matthew的回答是我需要使用的,因为我仍然在2.5上。 但是从2.7开始,您可以在任何要跳过的测试方法上使用@unittest.skip()装饰器。

http://docs.python.org/library/unittest.html#skipping-tests-and-expected-failures

你需要实现自己的跳过装饰器来检查基本类型。我以前没用过这个功能,但是我能想到的是,你可以使用BaseTest作为标记类型来条件跳过它。
def skipBaseTest(obj):
    if type(obj) is BaseTest:
        return unittest.skip("BaseTest tests skipped")
    return lambda func: func

6
另一种选择是不执行。
unittest.main()

你可以使用以下方法代替

suite = unittest.TestLoader().loadTestsFromTestCase(TestClass)
unittest.TextTestRunner(verbosity=2).run(suite)

所以你只需在TestClass类中执行测试


这是最不破坏性的解决方案。不要修改 unittest.main() 收集到默认测试套件中的内容,而是形成显式测试套件并运行其测试。 - zgoda

6
我想到的解决方法是,如果使用基类,则隐藏测试方法。这样,测试不会被跳过,因此在许多测试报告工具中,测试结果可以变为绿色,而不是黄色。
与 mixin 方法相比,像 PyCharm 这样的 IDE 不会抱怨基类缺少单元测试方法。
如果一个基类继承了该类,它将需要重写 setUpClasstearDownClass 方法。
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 = []

2
这里有一个解决方案,仅使用文档化的unittest功能,并避免在测试结果中出现“跳过”状态:
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 实例通常的工作方式。但是当在抽象基类本身中运行时,您可以简单地通过一个什么都不做的方法覆盖该行为。
一个副作用是您的测试计数会增加一个:当在 BaseClass 上运行 runNoTestsInBaseClass“test”时,它会被计算为成功的测试。
(如果您仍在使用 Python 2.7,则此方法也适用。只需将 super() 更改为 super(BaseTest, self) 即可。)

1
更好的方法是重写run(result=None)方法,并在BaseTest的情况下直接返回result。这样还可以正确报告测试的数量。 - interDist

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接