Виведення даних з одиничного тесту в 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працює пакет? Кілька лісорубів - норма.
С.Лотт

8
Я налаштовую журнал саме таким чином, який ви вказали. Я припускаю, що він працює, але де я бачу вихід? Це не виведення на консоль. Я спробував налаштувати його із входом у файл, але це не дає жодного результату.
MikeyE

"Однак, мій кращий метод - не витрачати багато часу на налагодження, а витратити його на написання більш тонких тестів, щоб викрити проблему." -- добре сказано!
Сет

34

Ви можете використовувати прості заяви для друку або будь-який інший спосіб написання для stdout. Ви також можете викликати налагоджувач Python в будь-якому місці ваших тестів.

Якщо ви використовуєте ніс для запуску своїх тестів (що я рекомендую), він збиратиме stdout для кожного тесту і показуватиме вам лише тест, якщо тест не вдався, тому вам не доведеться жити зі скрученим результатом, коли тести проходять.

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


На жаль, ніс, схоже, не збирає журнал, написаний на stdout / err, використовуючи рамку реєстрації. У мене є printі log.debug()поруч один з одним, і явно включаю DEBUGреєстрацію в корені з setUp()методу, але відображається лише printрезультат.
haridsv

7
nosetests -sпоказує вміст stdout, є помилка чи ні - те, що я вважаю корисним.
поспіх

Я не можу знайти перемикачі, щоб автоматично відображати змінні в носових документах. Ви можете вказати мені на щось, що описує їх?
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')]
>>>

5

Інший варіант - запустити налагоджувач там, де тест не вдається.

Спробуйте запустити свої тести з 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)

2
Ніс ( nos.readthedocs.org/en/latest/index.html ) - це ще одна рамка, яка забезпечує параметри "запустити сеанс налагодження". Я запускаю його з '-sx --pdb --pdb-збої', який не їсть вихід, зупиняється після першого відмови і потрапляє в 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)

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()який повертає ціну конкретного дайкірі після застосування вказаної знижки:

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


0

Як щодо того, щоб знайти виняток, що виникає внаслідок відмови у твердженні? У вашому блоці вилову ви можете виводити дані, однак хочете куди завгодно. Тоді, коли ви закінчили, ви могли повторно кинути виняток. Тест-бігун, ймовірно, не знав би різниці.

Відмова: Я не пробував цього з тестовою рамкою 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),
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.