使用基类和子类进行Python单元测试

Thi*_*Lam 137 python testing unit-testing

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

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()
Run Code Online (Sandbox Code Playgroud)

以上的输出是:

Calling BaseTest:testCommon
.Calling BaseTest:testCommon
.Calling SubTest1:testSub1
.Calling BaseTest:testCommon
.Calling SubTest2:testSub2
.
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK
Run Code Online (Sandbox Code Playgroud)

有没有办法重写上面的内容,以便第一个testCommon不被调用?

编辑: 我没有运行上面的5个测试,而是希望它只运行4个测试,2个来自SubTest1,另外2个来自SubTest2.似乎Python unittest自己运行原始的BaseTest,我需要一种机制来防止这种情况发生.

Mat*_*all 147

使用多重继承,因此具有常见测试的类本身不会继承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()
Run Code Online (Sandbox Code Playgroud)

  • 只是为了澄清Ian Clelland的评论,以便像我这样的人更清楚:如果你将`setUp`和`tearDown`方法添加到`CommonTests`类,并且你希望在派生类中为每个测试调用它们,你有要颠倒基类的顺序,所以它将是:`class SubTest1(CommonTests,unittest.TestCase)`. (28认同)
  • 如果颠倒基类的顺序,此方法仅适用于setUp和tearDown方法.由于该方法在定义unittest.TestCase生成,它们不叫超(),然后在CommonTests任何的安装和拆卸方法,需要先在MRO,否则将不能在所有被调用. (24认同)
  • 这个的问题是pylint是合适的,因为`CommonTests`正在调用该类中不存在的方法. (8认同)
  • 我不是这种方法的粉丝.这在代码中建立了一个合同,类必须从`unittest.TestCase`*和*`CommonTests`继承.我认为下面的`setUpClass`方法是最好的,不太容易出现人为错误.要么是将BaseTest类包装在容器类中,要么更容易使用,而是避免测试运行打印输出中的跳过消息. (6认同)

Vad*_* P. 125

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

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

import unittest

class BaseTestCases:

    class BaseTest(unittest.TestCase):

        def testCommon(self):
            print 'Calling BaseTest:testCommon'
            value = 5
            self.assertEquals(value, 5)


class SubTest1(BaseTestCases.BaseTest):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTestCases.BaseTest):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

if __name__ == '__main__':
    unittest.main()
Run Code Online (Sandbox Code Playgroud)

输出:

Calling BaseTest:testCommon
.Calling SubTest1:testSub1
.Calling BaseTest:testCommon
.Calling SubTest2:testSub2
.
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK
Run Code Online (Sandbox Code Playgroud)

  • 这是我最喜欢的.它是最少hacky手段,不会干扰重写方法,不会改变MRO并允许我在基类中定义setUp,setUpClass等. (6认同)
  • 我真的没有得到它(魔法来自哪里?),但它是我最好的解决方案:)来自Java,我讨厌多重继承...... (6认同)
  • @Edouardb unittest仅运行从TestCase继承的模块级类.但BaseTest不是模块级的. (4认同)

小智 29

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

del(BaseTest)
Run Code Online (Sandbox Code Playgroud)

所以代码看起来像这样:

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()
Run Code Online (Sandbox Code Playgroud)

  • 有趣.你能解释一下为什么会这样吗? (6认同)
  • BaseTest在定义时是模块的成员,因此可用作SubTests的基类.在定义完成之前,del()将其作为成员删除,因此unittest框架在模块中搜索TestCase子类时将找不到它. (3认同)
  • 这是一个了不起的答案!我比@MatthewMarshall更喜欢它,因为在他的解决方案中,您会从pylint中得到语法错误,因为标准对象中不存在self.assert *方法。 (2认同)
  • 如果 BaseTest 在基类或其子类中的任何其他地方被引用,则不起作用,例如在方法覆盖中调用 super() 时:`super( BaseTest, cls ).setUpClass( )` (2认同)
  • @Hannes至少在python 3中,可以通过子类中的`super(self.__class__, self)`或仅通过`super()`引用`BaseTest`,尽管[如果你要继承构造函数,则显然不是](http: //stackoverflow.com/questions/576169/understanding-python-super-with-init-methods)。当基类需要引用自身时,也许还有这样一种“匿名”替代方案(我不知道类何时需要引用自身)。 (2认同)

Den*_*zov 24

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()
Run Code Online (Sandbox Code Playgroud)

  • 那很整齐。有没有一种方法可以避免必须使用跳过?对我来说,跳过是不希望有的,并用于指示当前测试计划中的问题(是代码还是测试)? (2认同)

pej*_*eja 9

您可以添加__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()
Run Code Online (Sandbox Code Playgroud)

  • 该解决方案不适用于unittest自己的测试发现/测试运行程序。(我相信它需要使用替代测试运行器,例如鼻子。) (2认同)

Joh*_*kin 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()
Run Code Online (Sandbox Code Playgroud)


Jas*_*n A 6

马修的答案是我需要使用的答案,因为我还在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
Run Code Online (Sandbox Code Playgroud)


小智 6

另一种选择是不执行

unittest.main()
Run Code Online (Sandbox Code Playgroud)

而不是你可以使用

suite = unittest.TestLoader().loadTestsFromTestCase(TestClass)
unittest.TextTestRunner(verbosity=2).run(suite)
Run Code Online (Sandbox Code Playgroud)

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


sim*_*ack 5

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

与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 = []
Run Code Online (Sandbox Code Playgroud)