【问题标题】:Outputting data from unit test in Python在 Python 中从单元测试中输出数据
【发布时间】:2010-09-21 23:50:27
【问题描述】:

如果我在 Python 中编写单元测试(使用 unittest 模块),是否可以从失败的测试中输出数据,以便我可以检查它以帮助推断导致错误的原因?

我知道创建自定义消息的能力,它可以携带一些信息,但有时您可能会处理更复杂的数据,这些数据不容易表示为字符串。

例如,假设您有一个 Foo 类,并且正在使用名为 testdata 的列表中的数据测试方法栏:

class TestBar(unittest.TestCase):
    def runTest(self):
        for t1, t2 in testdata:
            f = Foo(t1)
            self.assertEqual(f.bar(t2), 2)

如果测试失败,我可能想输出 t1、t2 和/或 f,看看为什么这个特定的数据会导致失败。通过输出,我的意思是在测试运行之后,变量可以像任何其他变量一样被访问。

【问题讨论】:

    标签: python unit-testing


    【解决方案1】:

    您可以使用简单的打印语句,或任何其他写入标准输出的方式。您还可以在测试中的任何位置调用 Python 调试器。

    如果您使用 nose 来运行您的测试(我推荐),它会收集每个测试的标准输出,并且仅在测试失败时显示给您,所以您不要当测试通过时,不必忍受杂乱的输出。

    nose 还有一些开关可以自动显示断言中提到的变量,或者在测试失败时调用调试器。例如,-s (--nocapture) 会阻止捕获标准输出。

    【讨论】:

    • 不幸的是,nose 似乎没有使用日志框架收集写入 stdout/err 的日志。我将printlog.debug() 并排放置,并通过setUp() 方法在根目录显式打开DEBUG 日志记录,但只显示print 输出。
    • nosetests -s 显示 stdout 的内容是否有错误 - 我觉得很有用。
    • 我在鼻子文档中找不到自动显示变量的开关。你能指出一些描述它们的东西吗?
    • 我不知道如何从鼻子或单元测试中自动显示变量。我打印我想在测试中看到的东西。
    【解决方案2】:

    我不认为这正是您想要的。没有办法显示不会失败的变量值,但这可能会帮助您更接近以您想要的方式输出结果。

    您可以使用 TestRunner.run() 返回的 TestResult object 进行结果分析和处理。特别是 TestResult.errors 和 TestResult.failures

    关于 TestResults 对象:

    http://docs.python.org/library/unittest.html#id3

    还有一些代码可以为您指明正确的方向:

    >>> import random
    >>> import unittest
    >>>
    >>> class TestSequenceFunctions(unittest.TestCase):
    ...     def setUp(self):
    ...         self.seq = range(5)
    ...     def testshuffle(self):
    ...         # make sure the shuffled sequence does not lose any elements
    ...         random.shuffle(self.seq)
    ...         self.seq.sort()
    ...         self.assertEqual(self.seq, range(10))
    ...     def testchoice(self):
    ...         element = random.choice(self.seq)
    ...         error_test = 1/0
    ...         self.assert_(element in self.seq)
    ...     def testsample(self):
    ...         self.assertRaises(ValueError, random.sample, self.seq, 20)
    ...         for element in random.sample(self.seq, 5):
    ...             self.assert_(element in self.seq)
    ...
    >>> suite = unittest.TestLoader().loadTestsFromTestCase(TestSequenceFunctions)
    >>> testResult = unittest.TextTestRunner(verbosity=2).run(suite)
    testchoice (__main__.TestSequenceFunctions) ... ERROR
    testsample (__main__.TestSequenceFunctions) ... ok
    testshuffle (__main__.TestSequenceFunctions) ... FAIL
    
    ======================================================================
    ERROR: testchoice (__main__.TestSequenceFunctions)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<stdin>", line 11, in testchoice
    ZeroDivisionError: integer division or modulo by zero
    
    ======================================================================
    FAIL: testshuffle (__main__.TestSequenceFunctions)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<stdin>", line 8, in testshuffle
    AssertionError: [0, 1, 2, 3, 4] != [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.031s
    
    FAILED (failures=1, errors=1)
    >>>
    >>> testResult.errors
    [(<__main__.TestSequenceFunctions testMethod=testchoice>, 'Traceback (most recent call last):\n  File "<stdin>"
    , line 11, in testchoice\nZeroDivisionError: integer division or modulo by zero\n')]
    >>>
    >>> testResult.failures
    [(<__main__.TestSequenceFunctions testMethod=testshuffle>, 'Traceback (most recent call last):\n  File "<stdin>
    ", line 8, in testshuffle\nAssertionError: [0, 1, 2, 3, 4] != [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n')]
    >>>
    

    【讨论】:

      【解决方案3】:

      捕获断言失败产生的异常。在您的 catch 块中,您可以将数据输出到任何地方。然后,当你完成后,你可以重新抛出异常。测试运行者可能不知道其中的区别。

      免责声明:我没有在 Python 的单元测试框架中尝试过这个,但我在其他单元测试框架中尝试过。

      【讨论】:

        【解决方案4】:

        我们为此使用日志记录模块。

        例如:

        import logging
        class SomeTest( unittest.TestCase ):
            def testSomething( self ):
                log= logging.getLogger( "SomeTest.testSomething" )
                log.debug( "this= %r", self.this )
                log.debug( "that= %r", self.that )
                # etc.
                self.assertEquals( 3.14, pi )
        
        if __name__ == "__main__":
            logging.basicConfig( stream=sys.stderr )
            logging.getLogger( "SomeTest.testSomething" ).setLevel( logging.DEBUG )
            unittest.main()
        

        这允许我们为我们知道失败并且我们需要额外调试信息的特定测试打开调试。

        然而,我更喜欢的方法不是花很多时间在调试上,而是花时间编写更细粒度的测试来暴露问题。

        【讨论】:

        • 如果我在 testSomething 中调用 foo 方法并记录一些内容怎么办。在不将记录器传递给 foo 的情况下如何查看输出?
        • @simao:foo是什么?单独的功能? SomeTest的方法函数?在第一种情况下,一个函数可以有它自己的记录器。在第二种情况下,其他方法函数可以拥有自己的记录器。你知道logging 包是如何工作的吗?多个记录器是常态。
        • 我按照您指定的确切方式设置日志记录。我认为它正在工作,但我在哪里看到输出?它没有输出到控制台。我尝试通过记录到文件来配置它,但这也不会产生任何输出。
        • “然而,我更喜欢的方法不是花很多时间在调试上,而是花时间编写更细粒度的测试来暴露问题。” ——说得好!
        【解决方案5】:

        我想我可能想多了。我想出的一种方法是简单地使用一个全局变量来累积诊断数据。

        类似这样的:

        log1 = dict()
        class TestBar(unittest.TestCase):
            def runTest(self):
                for t1, t2 in testdata:
                    f = Foo(t1)
                    if f.bar(t2) != 2:
                        log1("TestBar.runTest") = (f, t1, t2)
                        self.fail("f.bar(t2) != 2")
        

        【讨论】:

          【解决方案6】:

          另一个选项 - 在测试失败的地方启动一个调试器。

          尝试使用 Testoob 运行您的测试(它将运行您的单元测试套件而无需更改),并且您可以使用“--debug”命令行开关在测试失败时打开调试器。

          这是 Windows 上的终端会话:

          C:\work> testoob tests.py --debug
          F
          Debugging for failure in test: test_foo (tests.MyTests.test_foo)
          > c:\python25\lib\unittest.py(334)failUnlessEqual()
          -> (msg or '%r != %r' % (first, second))
          (Pdb) up
          > c:\work\tests.py(6)test_foo()
          -> self.assertEqual(x, y)
          (Pdb) l
            1     from unittest import TestCase
            2     class MyTests(TestCase):
            3       def test_foo(self):
            4         x = 1
            5         y = 2
            6  ->     self.assertEqual(x, y)
          [EOF]
          (Pdb)
          

          【讨论】:

          • Nose (nose.readthedocs.org/en/latest/index.html) 是另一个提供“启动调试器会话”选项的框架。我用'-sx --pdb --pdb-failures'运行它,它不吃输出,在第一次失败后停止,并在异常和测试失败时进入pdb。这消除了我对丰富错误消息的需求,除非我很懒惰并在循环中进行测试。
          • 什么是Testoob?它仅适用于Windows吗?默认包含?您能否为其添加参考(没有“编辑:”、“更新:”或类似的 - 答案应该看起来好像是今天写的)?跨度>
          【解决方案7】:

          在 Python 2.7 中,您可以使用附加参数 msg 向错误消息中添加信息,如下所示:

          self.assertEqual(f.bar(t2), 2, msg='{0}, {1}'.format(t1, t2))
          

          官方文档是here

          【讨论】:

          • 也适用于 Python 3。
          • 文档提示了这一点,但值得明确提及:默认情况下,如果使用msg,它将替换正常的错误消息。要将msg附加到正常的错误消息中,您还需要将TestCase.longMessage设置为True
          • 很高兴知道我们可以传递自定义错误消息,但我有兴趣打印一些消息而不考虑错误。
          • @CatalinIacob 的评论适用于 Python 2.x。在 Python 3.x 中,TestCase.longMessage 默认为 True
          • 谢谢!这既简单又有用。
          【解决方案8】:

          inspect.trace 将让您在抛出异常后获取局部变量。然后,您可以使用如下装饰器包装单元测试,以保存这些局部变量以供事后检查。

          import random
          import unittest
          import inspect
          
          
          def store_result(f):
              """
              Store the results of a test
              On success, store the return value.
              On failure, store the local variables where the exception was thrown.
              """
              def wrapped(self):
                  if 'results' not in self.__dict__:
                      self.results = {}
                  # If a test throws an exception, store local variables in results:
                  try:
                      result = f(self)
                  except Exception as e:
                      self.results[f.__name__] = {'success':False, 'locals':inspect.trace()[-1][0].f_locals}
                      raise e
                  self.results[f.__name__] = {'success':True, 'result':result}
                  return result
              return wrapped
          
          def suite_results(suite):
              """
              Get all the results from a test suite
              """
              ans = {}
              for test in suite:
                  if 'results' in test.__dict__:
                      ans.update(test.results)
              return ans
          
          # Example:
          class TestSequenceFunctions(unittest.TestCase):
          
              def setUp(self):
                  self.seq = range(10)
          
              @store_result
              def test_shuffle(self):
                  # make sure the shuffled sequence does not lose any elements
                  random.shuffle(self.seq)
                  self.seq.sort()
                  self.assertEqual(self.seq, range(10))
                  # should raise an exception for an immutable sequence
                  self.assertRaises(TypeError, random.shuffle, (1,2,3))
                  return {1:2}
          
              @store_result
              def test_choice(self):
                  element = random.choice(self.seq)
                  self.assertTrue(element in self.seq)
                  return {7:2}
          
              @store_result
              def test_sample(self):
                  x = 799
                  with self.assertRaises(ValueError):
                      random.sample(self.seq, 20)
                  for element in random.sample(self.seq, 5):
                      self.assertTrue(element in self.seq)
                  return {1:99999}
          
          
          suite = unittest.TestLoader().loadTestsFromTestCase(TestSequenceFunctions)
          unittest.TextTestRunner(verbosity=2).run(suite)
          
          from pprint import pprint
          pprint(suite_results(suite))
          

          最后一行将打印测试成功的返回值和局部变量,在本例中为 x,失败时:

          {'test_choice': {'result': {7: 2}, 'success': True},
           'test_sample': {'locals': {'self': <__main__.TestSequenceFunctions testMethod=test_sample>,
                                      'x': 799},
                           'success': False},
           'test_shuffle': {'result': {1: 2}, 'success': True}}
          

          【讨论】:

            【解决方案9】:

            扩展 Facundo Casco's answer,这对我来说效果很好:

            class MyTest(unittest.TestCase):
                def messenger(self, message):
                    try:
                        self.assertEqual(1, 2, msg=message)
                    except AssertionError as e:      
                        print "\nMESSENGER OUTPUT: %s" % str(e),
            

            【讨论】:

              【解决方案10】:

              我使用的方法非常简单。我只是将其记录为警告,因此它实际上会显示出来。

              import logging
              
              class TestBar(unittest.TestCase):
                  def runTest(self):
              
                     #this line is important
                     logging.basicConfig()
                     log = logging.getLogger("LOG")
              
                     for t1, t2 in testdata:
                       f = Foo(t1)
                       self.assertEqual(f.bar(t2), 2)
                       log.warning(t1)
              

              【讨论】:

              • 如果测试成功,这会起作用吗?在我的情况下,只有在测试失败时才会显示警告
              • @ShreyaMaria 是的,它会的
              【解决方案11】:

              使用日志记录:

              import unittest
              import logging
              import inspect
              import os
              
              logging_level = logging.INFO
              
              try:
                  log_file = os.environ["LOG_FILE"]
              except KeyError:
                  log_file = None
              
              def logger(stack=None):
                  if not hasattr(logger, "initialized"):
                      logging.basicConfig(filename=log_file, level=logging_level)
                      logger.initialized = True
                  if not stack:
                      stack = inspect.stack()
                  name = stack[1][3]
                  try:
                      name = stack[1][0].f_locals["self"].__class__.__name__ + "." + name
                  except KeyError:
                      pass
                  return logging.getLogger(name)
              
              def todo(msg):
                  logger(inspect.stack()).warning("TODO: {}".format(msg))
              
              def get_pi():
                  logger().info("sorry, I know only three digits")
                  return 3.14
              
              class Test(unittest.TestCase):
              
                  def testName(self):
                      todo("use a better get_pi")
                      pi = get_pi()
                      logger().info("pi = {}".format(pi))
                      todo("check more digits in pi")
                      self.assertAlmostEqual(pi, 3.14)
                      logger().debug("end of this test")
                      pass
              

              用法:

              # LOG_FILE=/tmp/log python3 -m unittest LoggerDemo
              .
              ----------------------------------------------------------------------
              Ran 1 test in 0.047s
              
              OK
              # cat /tmp/log
              WARNING:Test.testName:TODO: use a better get_pi
              INFO:get_pi:sorry, I know only three digits
              INFO:Test.testName:pi = 3.14
              WARNING:Test.testName:TODO: check more digits in pi
              

              如果你没有设置LOG_FILE,那么日志记录会到stderr

              【讨论】:

                【解决方案12】:

                您可以为此使用logging 模块。

                所以在单元测试代码中,使用:

                import logging as log
                
                def test_foo(self):
                    log.debug("Some debug message.")
                    log.info("Some info message.")
                    log.warning("Some warning message.")
                    log.error("Some error message.")
                

                默认情况下,警告和错误会输出到/dev/stderr,因此它们应该在控制台上可见。

                要自定义日志(例如格式化),请尝试以下示例:

                # Set-up logger
                if args.verbose or args.debug:
                    logging.basicConfig( stream=sys.stdout )
                    root = logging.getLogger()
                    root.setLevel(logging.INFO if args.verbose else logging.DEBUG)
                    ch = logging.StreamHandler(sys.stdout)
                    ch.setLevel(logging.INFO if args.verbose else logging.DEBUG)
                    ch.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(name)s: %(message)s'))
                    root.addHandler(ch)
                else:
                    logging.basicConfig(stream=sys.stderr)
                

                【讨论】:

                  【解决方案13】:

                  在这些情况下,我在我的应用程序中使用带有一些消息的log.debug()。由于默认日志记录级别为WARNING,因此此类消息不会在正常执行中显示。

                  然后,在单元测试中,我将日志记录级别更改为DEBUG,以便在运行时显示此类消息。

                  import logging
                  
                  log.debug("Some messages to be shown just when debugging or unit testing")
                  

                  在单元测试中:

                  # Set log level
                  loglevel = logging.DEBUG
                  logging.basicConfig(level=loglevel)
                  



                  查看完整示例:

                  这是daikiri.py,一个基本类,它实现了一个带有名称和价格的daikiri。有一个方法 make_discount() 在应用给定折扣后返回特定大切的价格:

                  import logging
                  
                  log = logging.getLogger(__name__)
                  
                  class Daikiri(object):
                      def __init__(self, name, price):
                          self.name = name
                          self.price = price
                  
                      def make_discount(self, percentage):
                          log.debug("Deducting discount...")  # I want to see this message
                          return self.price * percentage
                  

                  然后,我创建了一个单元测试test_daikiri.py,用于检查其使用情况:

                  import unittest
                  import logging
                  from .daikiri import Daikiri
                  
                  
                  class TestDaikiri(unittest.TestCase):
                      def setUp(self):
                          # Changing log level to DEBUG
                          loglevel = logging.DEBUG
                          logging.basicConfig(level=loglevel)
                  
                          self.mydaikiri = Daikiri("cuban", 25)
                  
                      def test_drop_price(self):
                          new_price = self.mydaikiri.make_discount(0)
                          self.assertEqual(new_price, 0)
                  
                  if __name__ == "__main__":
                      unittest.main()
                  

                  所以当我执行它时,我会收到 log.debug 消息:

                  $ python -m test_daikiri
                  DEBUG:daikiri:Deducting discount...
                  .
                  ----------------------------------------------------------------------
                  Ran 1 test in 0.000s
                  
                  OK
                  

                  【讨论】:

                    【解决方案14】:

                    您也可以使用--locals 选项:python3 -m unittest --locals

                    来自python3 -m unittest -h--locals Show local variables in tracebacks

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2014-01-27
                      • 1970-01-01
                      相关资源
                      最近更新 更多