【问题标题】:Python unit test with base and sub class带有基类和子类的 Python 单元测试
【发布时间】: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 的所有子类的测试套件?

标签: python unit-testing testing


【解决方案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://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())
      

      我们去吧。

      【讨论】:

        【解决方案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 在更复杂的场景中执行时想要什么。
              猜你喜欢
              • 2011-10-21
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2020-07-22
              • 1970-01-01
              • 2020-06-30
              相关资源
              最近更新 更多