在python中从单元测试输出数据


115

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

Answers:


73

对于像我这样来这里寻求简单快速答案的人,答案很晚。

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

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

官方文档在这里


1
同样适用于Python 3。
MrDBA 2015年

18
文档暗示了这一点,但值得一提:默认情况下,如果msg使用,它将替换正常的错误消息。要msg附加到正常错误消息中,还需要将TestCase.longMessage设置为True
Catalin Iacob 2015年

1
很高兴知道我们可以传递自定义错误消息,但是我很想打印一些错误消息。
哈里·莫雷诺

5
@CatalinIacob的注释适用于Python2.x。在Python 3.x中,TestCase.longMessage默认为True
ndmeiri '18

70

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

例如:

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软件包的工作原理吗?多记录器是常态。
S.Lott 2010年

8
我以您指定的确切方式设置日志记录。我认为它正在运行,但是在哪里可以看到输出?它不会输出到控制台。我尝试通过登录到文件来配置它,但是也不会产生任何输出。
MikeyE

“不过,我的首选方法不是花很多时间进行调试,而是花更多的时间编写更细粒度的测试来揭示问题。” - 说得好!
赛斯

34

您可以使用简单的打印语句,也可以使用其他任何方式写入stdout。您还可以在测试中的任何地方调用Python调试器。

如果你用鼻子来运行测试(我建议这样做),它将为每个测试收集标准输出,并且仅在测试失败时才向您显示该标准输出,因此在测试通过时,您不必忍受混乱的输出。

鼻子还具有自动显示断言中提到的变量或在失败的测试中调用调试器的开关。例如-s--nocapture)阻止捕获stdout。


不幸的是,鼻子似乎没有使用日志记录框架收集写入到stdout / err的日志。我有printlog.debug()彼此相邻,并明确把DEBUG在从根记录setUp()方法,但只有print输出显示出来。
haridsv

7
nosetests -s显示stdout的内容是否存在错误-我发现这很有用。
hargriffle,2014年

我找不到在鼻子文档中自动显示变量的开关。你能指出我一些描述他们的东西吗?
ABM

我不知道一种自动显示鼻子或单元测试变量的方法。我打印我想在测试中看到的东西。
Ned Batchelder

16

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

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

5

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

尝试使用Testoob运行测试(它将运行您的unittest套件而不进行任何更改),并且当测试失败时,您可以使用'--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)

2
鼻子(nas.readthedocs.org/en/latest/index.html)是另一个提供“启动调试器会话”选项的框架。我用'-sx --pdb --pdb-failures'运行它,它不占用输出,在第一次失败后停止,并在异常和测试失败时进入pdb。除非我懒惰并在循环中进行测试,否则这消除了我对大量错误消息的需求。
jwhitlock

5

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

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)

如果测试成功,这项工作会成功吗?就我而言,警告仅在测试失败时才会显示
Shreya Maria

@ShreyaMaria是的
Orane

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

感谢您的答复。他们给了我一些关于如何在python中记录单元测试信息的替代想法。


2

使用日志记录:

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


2

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

2

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

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

import logging

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

在单元测试中:

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



查看完整示例:

这是daikiri.py一个使用其名称和价格实现Daikiri的基本类。有一种方法make_discount()可在应用给定折扣后返回该特定daikiri的价格:

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

1

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

Har detgøy:-)


0

如何捕获由断言失败生成的异常?在catch块中,您可以将数据输出到任何地方。然后,当您完成操作后,可以重新引发异常。测试跑步者可能不会知道区别。

免责声明:我尚未在python的单元测试框架中尝试过此方法,但在其他单元测试框架中尝试过此方法。



-1

扩展@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),
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.