在python中输出unit testing的数据

如果我正在用python编写unit testing(使用unittest模块),是否有可能从失败的testing中输出数据,所以我可以检查它以帮助推断导致错误的原因? 我意识到能够创build一个自定义的消息,它可以携带一些信息,但有时你可能会处理更复杂的数据,不能很容易地表示为一个string。

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

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

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

我们使用这个日志logging模块。

例如:

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

这使我们可以打开特定testing的debugging,我们知道这些testing正在失败,并且我们需要额外的debugging信息。

然而,我的首选方法是不花费大量的时间进行debugging,而是花费更多精细的testing来揭示问题。

对于像我这样的人来说,要find一个简单而快速的答案来得很迟。

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

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

官方文档在这里

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

如果你用鼻子来运行你的testing(我build议),它会为每个testing收集stdout,只有在testing失败的时候才显示给你,所以当testing通过时你不必忍受混乱的输出。

鼻子也有开关来自动显示断言中提到的variables,或在失败的testing中调用debugging器。 例如-s (– --nocapture )防止捕获标准输出。

我不认为这是你所期望的,没有办法显示不失败的variables值,但这可能会帮助你更接近于按照你想要的方式输出结果。

您可以使用TestRunner.run()返回的TestResult对象进行结果分析和处理。 特别是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')] >>> 

我想我可能已经过时了。 我提出的一个方法就是完成这个工作,只不过是有一个全局variables,用来累积诊断数据。

像这样的东西:

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

感谢您的回应。 他们给了我一些关于如何从unit testing中logging信息的另一种想法。

另一种select – 在testing失败的情况下启动一个debugging器。

试着用Testoob运行你的testing(它会运行你的unittest套件,不用改变),当testing失败时,你可以使用'–debug'命令行开关打开一个debugging器。

这是Windows上的terminal会话:

 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) 

我使用的方法非常简单。 我只是把它作为一个警告,所以它会实际显示。

 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) 

使用logging:

 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 ,日志logging将会变为stderr

你可以使用logging模块。

所以在unit testing代码中,使用:

 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) 

我在这些情况下做的是在我的应用程序中有一个log.debug()和一些消息。 由于缺省日志logging级别是WARNING ,因此这些消息在正常执行中不显示。

然后,在unittest中,我将日志级别更改为DEBUG ,以便在运行时显示这些消息。

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

在单位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 

然后,我创build一个unittest 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 

inspect.trace会让你在引发exception之后得到局部variables。 然后你可以用下面的一个装饰器来包装unit testing,以便在验尸期间保存这些局部variables以供检查。

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

最后一行将打印testing成功的返回值和本地variables,在这种情况下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}} 

Har detgøy:-)

如何捕捉断言失败产生的exception? 在你的catch块你可以输出数据,但是你想要的地方。 那么当你完成后,你可以重新抛出exception。 testing运动员可能不知道其中的差别。

免责声明:我还没有尝试过与Python的unit testing框架,但与其他unit testing框架。

承认我还没有尝试过, testfixtures的日志function看起来相当有用…

扩大@FC的答案,这对我来说很好:

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