Вывод данных из unit test в 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, чтобы узнать, почему именно эти данные привели к сбою. По результату я имею в виду, что после запуска теста переменные могут быть доступны, как и любые другие переменные.

Ответ 1

Мы используем для этого модуль протоколирования.

Например:

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

Это позволяет нам включать отладку для определенных тестов, которые, как мы знаем, не работают, и для которых мы хотим получить дополнительную информацию об отладке.

Мой предпочтительный метод, однако, не тратит много времени на отладку, но тратит его на создание более тонких тестов, чтобы выявить проблему.

Ответ 2

Очень поздний ответ для кого-то, который, как и я, приходит сюда в поисках простого и быстрого ответа.

В Python 2.7 вы можете использовать дополнительный параметр msg для добавления информации в сообщение об ошибке следующим образом:

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

Официальные документы здесь

Ответ 3

Вы можете использовать простые инструкции печати или любой другой способ записи в стандартный вывод. Вы также можете вызывать отладчик Python в любом месте тестов.

Если вы используете nose для запуска своих тестов (что я рекомендую), он будет собирать stdout для каждого теста и показывать только это вам, если тест не удался, поэтому вам не нужно жить с загроможденным выходом, когда проходят тесты.

нос также имеет переключатели для автоматического отображения переменных, упомянутых в утверждениях, или для вызова отладчика при неудачных тестах. Например -s (--nocapture) предотвращает захват stdout.

Ответ 4

Я не думаю, что это совсем то, что вы ищете, нет способа отобразить значения переменных, которые не терпят неудачу, но это может помочь вам приблизиться к выводу результатов так, как вы хотите.

Вы можете использовать объект TestResult, возвращенный TestRunner.run() для анализа и обработки результатов. В частности, 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

Думаю, я мог бы переусердствовать. Один из способов, с помощью которого я выполнял эту работу, - это просто иметь глобальную переменную, которая накапливает диагностические данные.

Что-то вроде этого:

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 (он будет запускать ваш набор unittest без изменений), и вы можете использовать ключ командной строки '--debug', чтобы открыть отладчик, когда тест не удался.

Здесь сеанс терминала в окнах:

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)

Ответ 7

Метод, который я использую, очень прост. Я просто зарегистрирую его как предупреждение, чтобы оно действительно появилось.

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)

Ответ 8

Использовать протоколирование:

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.

Ответ 9

Для этого вы можете использовать модуль logging.

Итак, в коде unit test используйте:

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)

Ответ 10

В этих случаях я должен иметь log.debug() с некоторыми сообщениями в своем приложении. Поскольку уровень ведения журнала по умолчанию WARNING, такие сообщения не отображаются при нормальном выполнении.

Затем в unittest я меняю уровень ведения журнала на DEBUG, чтобы такие сообщения показывались во время их запуска.

import logging

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

В unittests:

# 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

Затем я создаю 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

Ответ 11

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 det gøy: -)

Ответ 12

Как насчет того, чтобы поймать исключение, которое генерируется при ошибке утверждения? В вашем блоке catch вы можете выводить данные, но хотите, где бы вы ни находились. Затем, когда вы закончите, вы можете повторно выбросить исключение. Вероятно, испытуемый не знал разницы.

Отказ от ответственности: я не пробовал это с python unit test framework, но имел другие рамки unit test.

Ответ 14

Расширение @F.C. ответ, это работает для меня хорошо:

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