用 Python 从单元测试中输出数据

如果我用 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,以查看为什么这个特定的数据导致失败。通过输出,我的意思是在测试运行之后,可以像访问其他变量一样访问这些变量。

160739 次浏览

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

如果您使用 译自: 美国《科学》杂志网站(https://web.archive. org/web/20081120065052/http://www.some thingabout orange.com/mrl/Projects/nose)来运行您的测试(我建议这样做) ,它将收集每个测试的标准输出,并且只在测试失败时显示给您,这样当测试通过时,您就不必忍受混乱的输出。

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

我觉得这不是你想要的。没有办法显示不会失败的变量值,但是这可以帮助您更接近于按照您希望的方式输出结果。

您可以使用 Run ()返回的 < a href = “ http://docs.python.org/library/unittest.html # id3”rel = “ nofollow noReferrer”> TestResult 对象 进行结果分析和处理,特别是 TestResult.error 和 TestResult.fault

关于 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')]
>>>

捕获由断言失败生成的异常。在 catch 块中,您可以随意输出数据。然后,当您完成后,您可以重新抛出异常。测试人员可能不知道其中的区别。

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

我们使用日志模块进行此操作。

例如:

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 )
self.assertEqual( 3.14, pi )


if __name__ == "__main__":
logging.basicConfig( stream=sys.stderr )
logging.getLogger( "SomeTest.testSomething" ).setLevel( logging.DEBUG )
unittest.main()

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

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

我可能想太多了。我想到的一个解决方法,就是使用一个全局变量来积累诊断数据。

就像这样:

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

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

尝试使用 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)

在 Python 2.7中,您可以使用一个额外的参数 msg来向错误消息添加信息,如下所示:

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

官方文件是 给你

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

法昆多 · 卡斯科的回答的基础上扩展,这对我来说非常有效:

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

我使用的方法非常简单,我只是把它作为一个警告来记录,这样它就会真正显示出来。

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)

使用日志记录:

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

您可以使用 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)

在这些情况下,我在应用程序中使用 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