带有基类和子类的 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 的言论,以便像我这样的人更清楚:如果您将 setUptearDown 方法添加到 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 作为 ma​​rker 类型来调节跳过:

def skipBaseTest(obj):
    if type(obj) is BaseTest:
        return unittest.skip("BaseTest tests skipped")
    return lambda func: func

【讨论】:

【参考方案9】:

我想到的一种解决方法是在使用基类时隐藏测试方法。这样就不会跳过测试,因此在许多测试报告工具中测试结果可以是绿色而不是黄色。

相比于 mixin 方法,ide 之类的 PyCharm 不会抱怨基类中缺少单元测试方法。

如果基类继承自此类,则需要重写 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 = []

【讨论】:

【参考方案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 函数,以控制测试发现机制找到哪些测试(如果有)。

例如下面将只加载原发帖者的SubTest1SubTest2测试用例,忽略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 单元测试的主要内容,如果未能解决你的问题,请参考以下文章

iOS 开发-单元测试

15. Unittest单元测试框架的介绍与使用

第三次作业

java中基类和子类的类型是一样的吗?

单元/仪器测试 Android Gradle。仪器测试不会运行

Java Junit 基础笔记