2009-05-22 9 views
32

Próbuję napisać prosty test jednostkowy, który sprawdzi, czy pod pewnymi warunkami klasa w mojej aplikacji zarejestruje błąd za pomocą standardowego interfejsu API do rejestrowania. Nie mogę się domyślić, jaki jest najczystszy sposób na sprawdzenie tej sytuacji.Jak powinienem zweryfikować komunikat dziennika podczas testowania kodu Pythona pod nosem?

Wiem, że nos przechwytuje już wyniki logowania za pomocą wtyczki do logowania, ale wydaje się, że ma to służyć jako pomoc w raportowaniu i debugowaniu w przypadku nieudanych testów.

Dwa sposoby, aby to zrobić widzę to:

  • Mock się moduł rejestrowania, albo w sposób fragmentaryczny (mymodule.logging = mockloggingmodule) lub z odpowiednim biblioteki drwiącym.
  • Napisz lub użyj istniejącej wtyczki nosowej, aby przechwycić dane wyjściowe i zweryfikować je.

Jeśli sięgam po pierwsze podejście, chciałbym się dowiedzieć, jaki jest najczystszy sposób resetowania stanu globalnego do stanu sprzed wyłudzenia modułu logowania.

Czekamy na wskazówki na ten jeden ...

+0

Tam jest teraz wbudowany w sposób to zrobić: https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertLogs – wkschwartz

Odpowiedz

18

Kiedyś mock rejestratorów, ale w tej sytuacji uważam, najlepiej użyć koparki do rejestrowania, więc napisałem ten jeden oparciu o the document suggested by jkp:

class MockLoggingHandler(logging.Handler): 
    """Mock logging handler to check for expected logs.""" 

    def __init__(self, *args, **kwargs): 
     self.reset() 
     logging.Handler.__init__(self, *args, **kwargs) 

    def emit(self, record): 
     self.messages[record.levelname.lower()].append(record.getMessage()) 

    def reset(self): 
     self.messages = { 
      'debug': [], 
      'info': [], 
      'warning': [], 
      'error': [], 
      'critical': [], 
     } 
+0

Powyższy link nie działa, a ja zastanawiałem się, czy ktoś może opublikować post na temat korzystania z tego kodu. Kiedy próbuję dodać tę procedurę obsługi rejestrowania, ciągle otrzymuję błąd podczas próby użycia go jako 'AttributeError: klasa MockLoggingHandler nie ma atrybutu 'level''. – Randy

+0

Tutaj udzielono odpowiedzi na pytanie: http://stackoverflow.com/a/20524288/576333 – Randy

+1

Podam przykład użycia poniżej: http://stackoverflow.com/a/20553331/1286628 – wkschwartz

1

Należy użyć wyśmianie, jak kiedyś może chcesz zmienić rejestrator do, powiedzmy, jednej bazie danych. Nie będziesz szczęśliwy, jeśli spróbujesz połączyć się z bazą danych podczas testów nosa.

Szydełkowanie będzie kontynuowane, nawet jeśli standardowe wyjście zostanie wyłączone.

Użyłem stubów pyMox. Pamiętaj, aby anulować odcinki po teście.

+0

+1 Niektóre z korzyści z AOP. Zamiast owijać każdy backend w klasycznej klasie/obiekcie. –

0

Znaleziono one answer odkąd to opublikowałem. Nie jest zły.

+1

Link do pracy: http://www.domenkozar.com/2009/03/04/mocking-logging-module-for-unittests/ – iElectric

3

Jako kontynuacja odpowiedzi Reef, skorzystałem z możliwości kodowania przykładu za pomocą pymox. Wprowadzono dodatkowe funkcje pomocnicze ułatwiające funkcje pośrednie i metody.

import logging 

# Code under test: 

class Server(object): 
    def __init__(self): 
     self._payload_count = 0 
    def do_costly_work(self, payload): 
     # resource intensive logic elided... 
     pass 
    def process(self, payload): 
     self.do_costly_work(payload) 
     self._payload_count += 1 
     logging.info("processed payload: %s", payload) 
     logging.debug("payloads served: %d", self._payload_count) 

# Here are some helper functions 
# that are useful if you do a lot 
# of pymox-y work. 

import mox 
import inspect 
import contextlib 
import unittest 

def stub_all(self, *targets): 
    for target in targets: 
     if inspect.isfunction(target): 
      module = inspect.getmodule(target) 
      self.StubOutWithMock(module, target.__name__) 
     elif inspect.ismethod(target): 
      self.StubOutWithMock(target.im_self or target.im_class, target.__name__) 
     else: 
      raise NotImplementedError("I don't know how to stub %s" % repr(target)) 
# Monkey-patch Mox class with our helper 'StubAll' method. 
# Yucky pymox naming convention observed. 
setattr(mox.Mox, 'StubAll', stub_all) 

@contextlib.contextmanager 
def mocking(): 
    mocks = mox.Mox() 
    try: 
     yield mocks 
    finally: 
     mocks.UnsetStubs() # Important! 
    mocks.VerifyAll() 

# The test case example: 

class ServerTests(unittest.TestCase): 
    def test_logging(self): 
     s = Server() 
     with mocking() as m: 
      m.StubAll(s.do_costly_work, logging.info, logging.debug) 
      # expectations 
      s.do_costly_work(mox.IgnoreArg()) # don't care, we test logging here. 
      logging.info("processed payload: %s", 'hello') 
      logging.debug("payloads served: %d", 1) 
      # verified execution 
      m.ReplayAll() 
      s.process('hello') 

if __name__ == '__main__': 
    unittest.main() 
+2

Podoba mi się Twoje innowacyjne zastosowanie dekoratora kontekstowego do realizacji "szyderstwa z zakresem". Miły. – jkp

+0

PS: to wstyd, że PyMOX nie ma zgodności z pep8. – jkp

30

Na szczęście to nie jest coś, co trzeba napisz sam; Pakiet testfixtures udostępnia menedżera kontekstów przechwytującego wszystkie wyniki rejestrowania, które występują w treści instrukcji with. Można znaleźć pakiet tutaj:

http://pypi.python.org/pypi/testfixtures

A oto jego docs o jak testować rejestrowanie:

http://testfixtures.readthedocs.org/en/latest/logging.html

+2

To rozwiązanie nie tylko wyglądało na bardziej eleganckie jak w rzeczywistości pracował dla mnie, podczas gdy inni nie (mój dziennik pochodzi z wielu wątków). –

20

UPDATE: No już żadnej potrzeby poniższej odpowiedzi. Zamiast tego użyj built-in Python way! To jest przedłużenie pracy wykonanej w https://stackoverflow.com/a/1049375/1286628. Handler jest w dużej mierze taki sam (konstruktor jest bardziej idiomatyczny, używając super).Ponadto, dodam demonstrację użycia programu obsługi z biblioteką standardowej unittest.

class MockLoggingHandler(logging.Handler): 
    """Mock logging handler to check for expected logs. 

    Messages are available from an instance's ``messages`` dict, in order, indexed by 
    a lowercase log level string (e.g., 'debug', 'info', etc.). 
    """ 

    def __init__(self, *args, **kwargs): 
     self.messages = {'debug': [], 'info': [], 'warning': [], 'error': [], 
         'critical': []} 
     super(MockLoggingHandler, self).__init__(*args, **kwargs) 

    def emit(self, record): 
     "Store a message from ``record`` in the instance's ``messages`` dict." 
     try: 
      self.messages[record.levelname.lower()].append(record.getMessage()) 
     except Exception: 
      self.handleError(record) 

    def reset(self): 
     self.acquire() 
     try: 
      for message_list in self.messages.values(): 
       message_list.clear() 
     finally: 
      self.release() 

Wtedy można skorzystać z obsługi w standardowym Biblioteka unittest.TestCase tak:

import unittest 
import logging 
import foo 

class TestFoo(unittest.TestCase): 

    @classmethod 
    def setUpClass(cls): 
     super(TestFoo, cls).setUpClass() 
     # Assuming you follow Python's logging module's documentation's 
     # recommendation about naming your module's logs after the module's 
     # __name__,the following getLogger call should fetch the same logger 
     # you use in the foo module 
     foo_log = logging.getLogger(foo.__name__) 
     cls._foo_log_handler = MockLoggingHandler(level='DEBUG') 
     foo_log.addHandler(cls._foo_log_handler) 
     cls.foo_log_messages = cls._foo_log_handler.messages 

    def setUp(self): 
     super(TestFoo, self).setUp() 
     self._foo_log_handler.reset() # So each test is independent 

    def test_foo_objects_fromble_nicely(self): 
     # Do a bunch of frombling with foo objects 
     # Now check that they've logged 5 frombling messages at the INFO level 
     self.assertEqual(len(self.foo_log_messages['info']), 5) 
     for info_message in self.foo_log_messages['info']: 
      self.assertIn('fromble', info_message) 
+0

Dziękuję za wyjaśnienie, jak wykorzystać odpowiedź Gustavo i rozszerzyć ją. – Harshdeep

+1

Istnieje teraz wbudowany sposób, aby to zrobić: https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertLogs – wkschwartz

+1

W setUpClass, wywołanie foo_log.addHandler() ma brakujące podkreślenie przed zmienną 'foo_log_handler' – PaulR

6

Brandona odpowiedź:

pip install testfixtures 

urywek:

import logging 
from testfixtures import LogCapture 
logger = logging.getLogger('') 


with LogCapture() as logs: 
    # my awesome code 
    logger.error('My code logged an error') 
assert 'My code logged an error' in str(logs) 

Uwaga: Powyższe nie stoi w sprzeczności z wywołaniem nosetests i coraz wyjście logCapture wtyczki narzędzia

0

Keying off @ Odpowiedź Reef, próbowałem kodu poniżej. Działa dobrze dla mnie zarówno dla Pythona 2.7 (jeśli instalujesz mock), jak i dla Pythona 3.4.

""" 
Demo using a mock to test logging output. 
""" 

import logging 
try: 
    import unittest 
except ImportError: 
    import unittest2 as unittest 

try: 
    # Python >= 3.3 
    from unittest.mock import Mock, patch 
except ImportError: 
    from mock import Mock, patch 

logging.basicConfig() 
LOG=logging.getLogger("(logger under test)") 

class TestLoggingOutput(unittest.TestCase): 
    """ Demo using Mock to test logging INPUT. That is, it tests what 
    parameters were used to invoke the logging method, while still 
    allowing actual logger to execute normally. 

    """ 
    def test_logger_log(self): 
     """Check for Logger.log call.""" 
     original_logger = LOG 
     patched_log = patch('__main__.LOG.log', 
          side_effect=original_logger.log).start() 

     log_msg = 'My log msg.' 
     level = logging.ERROR 
     LOG.log(level, log_msg) 

     # call_args is a tuple of positional and kwargs of the last call 
     # to the mocked function. 
     # Also consider using call_args_list 
     # See: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.call_args 
     expected = (level, log_msg) 
     self.assertEqual(expected, patched_log.call_args[0]) 


if __name__ == '__main__': 
    unittest.main() 
25

z Pythona 3.4, a średnia unittest biblioteka oferuje nowy menedżer kontekstowe Test twierdzenie, assertLogs. Z docs:

with self.assertLogs('foo', level='INFO') as cm: 
    logging.getLogger('foo').info('first message') 
    logging.getLogger('foo.bar').error('second message') 
    self.assertEqual(cm.output, ['INFO:foo:first message', 
           'ERROR:foo.bar:second message'])