Wyprowadzanie danych z testu jednostkowego w Pythonie

115

Jeśli piszę testy jednostkowe w Pythonie (używając modułu unittest), czy jest możliwe wyprowadzenie danych z nieudanego testu, więc mogę go zbadać, aby pomóc wydedukować, co spowodowało błąd? Zdaję sobie sprawę z możliwości stworzenia niestandardowej wiadomości, która może zawierać pewne informacje, ale czasami możesz mieć do czynienia z bardziej złożonymi danymi, których nie można łatwo przedstawić jako ciąg.

Na przykład załóżmy, że masz klasę Foo i testujesz pasek metod, używając danych z listy o nazwie testdata:

class TestBar(unittest.TestCase):
    def runTest(self):
        for t1, t2 in testdata:
            f = Foo(t1)
            self.assertEqual(f.bar(t2), 2)

Jeśli test się nie powiedzie, mogę chcieć wyprowadzić t1, t2 i / lub f, aby zobaczyć, dlaczego te konkretne dane spowodowały awarię. Przez dane wyjściowe rozumiem, że dostęp do zmiennych można uzyskać tak samo, jak do innych zmiennych, po przeprowadzeniu testu.

Rybik cukrowy
źródło

Odpowiedzi:

73

Bardzo późna odpowiedź dla kogoś, kto tak jak ja przychodzi tutaj, szukając prostej i szybkiej odpowiedzi.

W Pythonie 2.7 możesz użyć dodatkowego parametru, msgaby dodać informacje do komunikatu o błędzie w następujący sposób:

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

Oficjalne dokumenty tutaj

Facundo Casco
źródło
1
Działa również w Pythonie 3.
MrDBA
18
Dokumentacja wskazuje na to, ale warto o tym wyraźnie wspomnieć: domyślnie, jeśli msgjest używany, zastąpi normalny komunikat o błędzie. Aby msgdołączyć do normalnego komunikatu o błędzie, musisz również ustawić TestCase.longMessage na True
Catalin Iacob
1
dobrze wiedzieć, że możemy przekazać niestandardowy komunikat o błędzie, ale byłem zainteresowany wydrukowaniem komunikatu niezależnie od błędu.
Harry Moreno
5
Komentarz @CatalinIacob dotyczy Pythona 2.x. W Pythonie 3.x TestCase.longMessage domyślnie True.
ndmeiri
70

Używamy do tego modułu logowania.

Na przykład:

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

To pozwala nam włączyć debugowanie dla określonych testów, o których wiemy, że zawodzą i dla których chcemy uzyskać dodatkowe informacje o debugowaniu.

Jednak moja preferowana metoda nie polega na spędzaniu dużo czasu na debugowaniu, ale na pisaniu bardziej szczegółowych testów, aby ujawnić problem.

S.Lott
źródło
Co jeśli wywołam metodę foo wewnątrz testSomething i coś zarejestruje. Jak mogę zobaczyć wynik dla tego bez przekazywania loggera do foo?
simao
@simao: Co to jest foo? Osobna funkcja? Funkcja metody SomeTest? W pierwszym przypadku funkcja może mieć swój własny rejestrator. W drugim przypadku druga funkcja metody może mieć swój własny rejestrator. Czy wiesz, jak loggingdziała pakiet? Wiele rejestratorów to norma.
S.Lott
8
Skonfigurowałem rejestrowanie dokładnie tak, jak podałeś. Zakładam, że to działa, ale gdzie widzę wynik? Nie wyświetla się na konsoli. Próbowałem skonfigurować go z logowaniem do pliku, ale to również nie daje żadnych wyników.
MikeyE
„Jednak moja preferowana metoda nie polega na spędzaniu dużo czasu na debugowaniu, ale na pisaniu bardziej szczegółowych testów, aby ujawnić problem”. -- dobrze powiedziane!
Seth
34

Możesz użyć prostych instrukcji print lub dowolnego innego sposobu zapisu na standardowe wyjście. Możesz również wywołać debuger Pythona w dowolnym miejscu testów.

Jeśli używasz nosa do uruchamiania testów (co polecam), zbierze on standardowe wyjście dla każdego testu i pokaże go tylko wtedy, gdy test się nie powiedzie, więc nie musisz żyć z zaśmieconym wyjściem, gdy testy zakończą się pomyślnie.

nos posiada również przełączniki do automatycznego wyświetlania zmiennych wymienionych w potwierdzeniach lub do wywoływania debugera w przypadku niepowodzenia testów. Na przykład -s( --nocapture) zapobiega przechwytywaniu stdout.

Ned Batchelder
źródło
Niestety nos nie wydaje się zbierać logów zapisywanych na stdout / err przy użyciu frameworka logowania. Mam printi log.debug()obok siebie i jawnie włączam DEBUGrejestrowanie w katalogu głównym z setUp()metody, ale printpojawia się tylko wyjście.
haridsv
7
nosetests -spokazuje zawartość stdout, czy jest błąd, czy nie - coś, co uważam za przydatne.
hargriffle
Nie mogę znaleźć przełączników do automatycznego wyświetlania zmiennych w dokumentach nosowych. Czy możesz wskazać mi coś, co je opisuje?
ABM
Nie znam sposobu, aby automatycznie wyświetlać zmienne z nosa lub nieokreślonego. Drukuję rzeczy, które chcę zobaczyć w moich testach.
Ned Batchelder
16

Wydaje mi się, że nie jest to dokładnie to, czego szukasz, nie ma sposobu, aby wyświetlić wartości zmiennych, które nie zawodzą, ale może to pomóc ci zbliżyć się do uzyskania wyników w pożądany sposób.

Do analizy i przetwarzania wyników można użyć obiektu TestResult zwróconego przez TestRunner.run () . W szczególności TestResult.errors i TestResult.failures

Informacje o obiekcie TestResults:

http://docs.python.org/library/unittest.html#id3

Oraz kod wskazujący właściwy kierunek:

>>> 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
źródło
5

Inna opcja - uruchom debugger, w którym test się nie powiedzie.

Spróbuj uruchomić testy za pomocą Testoob (uruchomi on Twój pakiet unittest bez zmian) i możesz użyć przełącznika wiersza poleceń „--debug”, aby otworzyć debugger, gdy test się nie powiedzie.

Oto sesja terminala w systemie 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
źródło
2
Nose ( nose.readthedocs.org/en/latest/index.html ) to kolejny framework, który zapewnia opcje „rozpocznij sesję debuggera”. Uruchamiam go z '-sx --pdb --pdb-failures', który nie zjada wyjścia, zatrzymuje się po pierwszej awarii i przechodzi do pdb w przypadku wyjątków i niepowodzeń testów. Dzięki temu nie potrzebuję bogatych komunikatów o błędach, chyba że jestem leniwy i testuję w pętli.
jwhitlock,
5

Metoda, której używam, jest naprawdę prosta. Po prostu rejestruję to jako ostrzeżenie, aby faktycznie się pojawiło.

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)
Orane
źródło
Czy to zadziała, jeśli test się powiedzie? W moim przypadku ostrzeżenie jest wyświetlane tylko wtedy, gdy test się nie powiedzie
Shreya Maria
@ShreyaMaria yes it will
Orane
5

Myślę, że mogłem się nad tym zastanawiać. Jednym ze sposobów, które wymyśliłem, jest po prostu posiadanie zmiennej globalnej, która gromadzi dane diagnostyczne.

Coś takiego:

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

Dzięki za odpowiedzi. Dali mi kilka alternatywnych pomysłów na rejestrowanie informacji z testów jednostkowych w Pythonie.

Rybik cukrowy
źródło
2

Użyj logowania:

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

Stosowanie:

# 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

Jeśli nie ustawisz LOG_FILE, logowanie będzie musiało stderr.

nie jest użytkownikiem
źródło
2

Możesz do tego użyć loggingmodułu.

Więc w kodzie testu jednostkowego użyj:

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

Domyślnie ostrzeżenia i błędy są wyprowadzane /dev/stderr, więc powinny być widoczne na konsoli.

Aby dostosować dzienniki (na przykład formatowanie), wypróbuj następujący przykład:

# 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
źródło
2

W takich przypadkach mam log.debug()w swojej aplikacji wiadomość. Ponieważ domyślny poziom rejestrowania to WARNING, takie komunikaty nie są wyświetlane podczas normalnego wykonywania.

Następnie najczęściej zmieniam poziom logowania na DEBUG, aby takie komunikaty były wyświetlane podczas ich uruchamiania.

import logging

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

W publikacjach:

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



Zobacz pełny przykład:

To jest daikiri.pypodstawowa klasa, która implementuje Daikiri z jego nazwą i ceną. Istnieje metoda, make_discount()która zwraca cenę tego konkretnego daikiri po zastosowaniu danego rabatu:

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

Następnie tworzę unittest, test_daikiri.pyktóry sprawdza jego użycie:

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

Więc kiedy go wykonuję, otrzymuję log.debugkomunikaty:

$ python -m test_daikiri
DEBUG:daikiri:Deducting discount...
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
fedorqui 'SO przestań szkodzić'
źródło
1

inspect.trace pozwoli ci uzyskać zmienne lokalne po wyrzuceniu wyjątku. Następnie można owinąć testy jednostkowe dekoratorem, takim jak poniższy, aby zapisać te zmienne lokalne do zbadania podczas sekcji zwłok.

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

W ostatnim wierszu zostaną wydrukowane zwrócone wartości, w przypadku których test się powiódł, oraz zmienne lokalne, w tym przypadku x, gdy się nie powiedzie:

{'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 :-)

Max Murphy
źródło
0

A co z przechwytywaniem wyjątku, który jest generowany w wyniku błędu potwierdzenia? W swoim bloku catch możesz wyprowadzać dane w dowolny sposób, gdziekolwiek chcesz. Następnie, gdy skończysz, możesz ponownie zgłosić wyjątek. Biegacz testowy prawdopodobnie nie wiedziałby różnicy.

Zastrzeżenie: nie próbowałem tego z frameworkiem testów jednostkowych Pythona, ale miałem z innymi strukturami testów jednostkowych.

Sam Corder
źródło
-1

Rozszerzając odpowiedź @FC, działa to całkiem dobrze dla mnie:

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
źródło