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

тарпон
источник

Ответы:

73

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

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

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

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

Факундо Каско
источник
1
Также работает в Python 3.
MrDBA
18
Документы намекают на это, но об этом стоит упомянуть явно: по умолчанию, если msgон используется, он заменит обычное сообщение об ошибке. Чтобы msgдобавить к обычному сообщению об ошибке, вам также необходимо установить для TestCase.longMessage значение True
Catalin Iacob
1
хорошо знать, что мы можем передать собственное сообщение об ошибке, но мне было интересно напечатать какое-нибудь сообщение независимо от ошибки.
Гарри Морено
5
Комментарий @CatalinIacob относится к Python 2.x. В Python 3.x TestCase.longMessage по умолчанию True.
ndmeiri
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()

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

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

С. Лотт
источник
Что делать, если я вызываю метод foo внутри testSomething, и он что-то регистрирует. Как я могу увидеть результат, не передавая логгер в foo?
simao
@simao: Что это такое foo? Отдельная функция? Функция метода SomeTest? В первом случае функция может иметь собственный логгер. Во втором случае у другой функции метода может быть собственный логгер. Вы в курсе, как loggingработает пакет? Использование нескольких регистраторов - это норма.
S.Lott
8
Я настроил ведение журнала именно так, как вы указали. Я предполагаю, что это работает, но где я могу увидеть результат? Он не выводится на консоль. Я попытался настроить его с записью в файл, но это тоже не дает никаких результатов.
MikeyE
«Однако я предпочитаю не тратить много времени на отладку, а тратить его на написание более детальных тестов, чтобы выявить проблему». -- хорошо сказано!
Сет,
34

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

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

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

Нед Батчелдер
источник
К сожалению, Нос, похоже, не собирает журнал, записанный в stdout / err с использованием структуры ведения журнала. У меня есть printи log.debug()рядом друг с другом, и я явно включаю DEBUGведение журнала в корне из setUp()метода, но printотображается только вывод.
haridsv
7
nosetests -sпоказывает содержимое stdout независимо от того, есть ли ошибка - что-то, что я считаю полезным.
Hargriffle
Я не могу найти переключатели для автоматического отображения переменных в носовых документах. Вы можете указать мне что-нибудь, описывающее их?
ABM
Я не знаю, как автоматически показывать переменные из носа или unittest. Я печатаю то, что хочу увидеть в своих тестах.
Нед Батчелдер
16

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

Вы можете использовать объект 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')]
>>>
monkut
источник
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)
orip
источник
2
Нос ( нос.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)
Оранэ
источник
Будет ли это работать, если тест пройдет успешно? В моем случае предупреждение отображается только в том случае, если тест не проходит
Шрейя Мария
@ShreyaMaria да это будет
Оранэ
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)
kenorb
источник
2

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

Затем в модуле unittest я меняю уровень ведения журнала на 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()который возвращает цену конкретного дайкири после применения данной скидки:

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
fedorqui 'ТАК, хватит вредить'
источник
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}}

Хар дет гёй :-)

Макс Мерфи
источник
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),
georgepsarakis
источник