具有基类和子类的Python单元测试


148

我目前有一些单元测试,它们共享一组通用的测试。这是一个例子:

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的所有子类的测试套件?
kon psych

Answers:


154

使用多重继承,因此具有通用测试的类本身不会继承自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()

1
到目前为止,这是最优雅的解决方案。
Thierry Lam,

27
如果您反转基类的顺序,则此方法仅适用于setUp和tearDown方法。因为方法是在unittest.TestCase中定义的,并且它们不调用super(),所以CommonTests中的任何setUp和tearDown方法都必须在MRO中首先出现,否则它们将根本不会被调用。
伊恩·克莱兰

32
只是为了澄清Ian Clelland的话,以便对像我这样的人更加清楚:如果在类中添加setUptearDown方法CommonTests,并且希望派生类中的每个测试都调用它们,则必须颠倒基类的顺序,这样它将是:class SubTest1(CommonTests, unittest.TestCase)
丹尼斯·哥洛马佐夫

6
我并不真的喜欢这种方法。这在代码中建立了一个契约,类必须同时从unittest.TestCase 继承CommonTests。我认为以下setUpClass方法是最好的,并且不易出现人为错误。要么将BaseTest类包装在一个容器类中,否则它会更hacky,但可以避免在测试运行打印输出中出现跳过消息。
大卫·桑德斯

10
这个问题是pylint很合适,因为CommonTests正在调用该类中不存在的方法。
MadScientist

145

不要使用多重继承,它会咬你以后

相反,您可以将您的基类移至单独的模块中,或将其与空白类包装在一起:

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

6
这是我最喜欢的。这是最简单的方法,不会干扰替代方法,不会更改MRO,并且允许我在基类中定义setUp,setUpClass等。
汉尼斯,2015年

6
我真的不明白(魔术从哪里来?),但是对我来说,这是最好的解决方案:)来自Java,我讨厌多重继承……
Edouard Berthe

4
@Edouardb单元测试仅运行从TestCase继承的模块级类。但是BaseTest不是模块级别的。
JoshB

作为非常相似的替代方法,您可以在无参数函数中定义ABC,该函数在调用时返回ABC
Anakhand

34

您可以使用单个命令解决此问题:

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

3
BaseTest在定义时是模块的成员,因此可以用作SubTests的基类。在定义完成之前,del()将其删除为成员,因此unittest框架在模块中搜索TestCase子类时将找不到它。
mhsmith 2015年

3
这是一个了不起的答案!我比@MatthewMarshall更喜欢它,因为在他的解决方案中,您会从pylint中得到语法错误,因为这些self.assert*方法在标准对象中不存在。
SimplyKnownAsG 2015年

1
如果在基类或其子类的其他任何地方引用了BaseTest,则不起作用,例如在方法覆盖中调用super()时: super( BaseTest, cls ).setUpClass( )
Hannes

1
@Hannes至少可以在python 3中BaseTest通过@Hannes 进行引用,也可以super(self.__class__, self)super()在其子类中进行引用,尽管如果要继承构造函数显然不是这样。当基类需要引用自身时,也许还有这样的“匿名”替代方法(不是我不知道何时类需要引用自身)。
斯坦因

28

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

3
那很整齐。有没有一种方法可以绕过不得不使用跳过?对我来说,跳过是不希望有的,并用于指示当前测试计划中的问题(是代码还是测试)?
2014年

@ZacharyYoung我不知道,也许其他答案可能会有所帮助。
丹尼斯·哥洛马佐夫

@ZacharyYoung我已尝试解决此问题,请参阅我的答案。
simonzack 2014年

目前尚不清楚从两个类继承的固有错误倾向
jwg 2015年

@jwg参见对已接受答案的注释:)您需要从两个基类继承每个测试类。您需要保留它们的正确顺序;如果您想添加另一个基础测试类,则也需要继承它。mixin没什么错,但是在这种情况下,可以用简单的跳过替换它们。
丹尼斯·哥洛马佐夫

7

您想达到什么目的?如果您有通用的测试代码(断言,模板测试等),则将它们放在没有前缀的方法中,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不,不会。默认设置是仅运行以“ test”开头的方法。
CS

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

5

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

与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 = []

5

您可以添加__test_ = FalseBaseTest类,但是如果添加它,请注意必须添加__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()

4

另一种选择是不执行

unittest.main()

除此之外,您可以使用

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

所以你只执行类中的测试 TestClass


这是最小的解决方案。无需将unittest.main()收集的内容修改为默认套件,而是形成显式套件并运行其测试。
zgoda

1

我所做的与@Vladim P.(https://stackoverflow.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())

然后我们去。


1

从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包含默认加载器发现测试)并复制,但Basesuite代替,但嵌套性质TestSuite.__iter__品牌是一个复杂得多。


0

只需将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()`

2
这将导致在两个SubTest中的任何一个中都不运行methodCommon测试。
Pepper Lebeck-Jobe

0

因此,这是一个旧线程,但是我今天遇到了这个问题,并为此想到了自己的技巧。它使用一个装饰器,当通过基类访问时,该装饰器使函数的值变为None。无需担心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()

-2

将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:testCommon调用
SubTest2:testSub2

文档中

TestCase.setUp()
用于准备测试治具的方法。在调用测试方法之前立即调用该方法。此方法引发的任何异常都将被视为错误而不是测试失败。默认实现不执行任何操作。


那行得通,如果我没有testCommon,应该将它们全部放在下面setUp吗?
Thierry Lam 2009年

1
是的,您应该将所有不是实际测试用例的代码置于setUp下。
布赖恩·邦迪

但是,如果一个子类具有多个test...方法,则setUp每个这样的方法一次又一次地执行该方法。因此在此处进行测试不是一个好主意!
亚历克斯·马丁里

不确定在更复杂的情况下执行时OP想要什么。
Brian R. Bondy
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.