I have a code in python as below:
import logging
def method():
value = get_value()
if value is not None:
do_something()
else:
logging.critical("value cannot be None")
I wanted to write a unit-test to cover both of the above scenarios. The first case for success was easy. But for the failure scenario, what is the correct way to catch that logging.critical was called?
I tried to put the method inside try-except block and it works.
... Mock required function and specify return values ...
try:
method()
except Exception as ex:
import traceback
traceback.format_exc()
Though the above method works, I don't think I correctly captured that logging.critical was called rather than an exception being raised. What is the correct way to go about it?
You could mock the logger. But, you are currently using the default logger, which you'd rather not mock. You could change your code to use a module specific logger:
import logging
logger = logging.getLogger(__name__)
def method():
value = get_value()
if value is not None:
return True
else:
logger.critical("value cannot be None")
Instead of method accessing that global logger object, you could pass a Logger object as an argument to method, or have a class around method and make a Logger object a class member, etc.
However, staying with the example as it is, you can then mock this logger object and assert that it is called as expected:
import unittest
import unittest.mock
class TestStringMethods(unittest.TestCase):
def test_method_withNone_shallLogCritical(self):
# setup
global logger
tmpLogger = logger
logger = unittest.mock.Mock()
# exercise
method()
# verify
logger.critical.assert_called() # python >= 3.6
# teardown
logger = tmpLogger
Related
I wrote a Thing class that does logging using loguru. At the bottom of the class file I add a handler to the logger. This is what thing.py looks like.
from loguru import logger
class Thing:
def __init__(self):
logger.info("Thing created")
def __enter__(self):
logger.info("Thing entered")
return self
def __exit__(self, exc_type, exc_value, traceback):
logger.info("Thing exited")
logger.add("thing.log")
if __name__ == "__main__":
with Thing() as thing:
logger.info("In with block")
This works fine and it logs to thing.log as expected. What I would like to achieve is that it does not add the handler to thing.log when running tests.
This is my test file:
import pytest
from loguru import logger
from thing import Thing
#pytest.fixture
def thing(mocker):
mocker.patch("thing.logger", logger)
with Thing() as thing:
yield thing
def test_thing(thing, mocker):
mocker.patch("thing.logger", logger)
logger.info("In test")
assert isinstance(thing, Thing)
Now this test passes, but the logs are still written to thing.log (instead to only stdout, which is the default in for a loguru.logger).
How do I make sure that it only logs to the basic loguru.logger when running pytest?
What I tried:
Using monkeypatch instead of using mocker: monkeypatch.setattr("thing.logger", logger)
Patching in only one place (either in the fixture or in the test function)
Patching without replacement: mocker.patch("thing.logger") (so without a replacement logger)
Remove logger.add("thing.log") from thing.py!
You can either specify (as said in the docs) that you want to log to stdout: logger.add(sys.stdout) or just leave it out because the default for loguru.logger is in fact stdout!
The example provided in their docs:
logger.add(sys.stdout, format="{time} - {level} - {message}", filter="sub.module")
EDIT:
if __name__ == "__main__":
logger.add("thing.log")
if __name__ == "__main__":
with Thing() as thing:
#...
Now the logger will log to thing.log when the module is executed directly, but it will NOT add the file handler when the module is imported by another module (e.g. a test file).
Or you can use logger.remove(0) to stop logging when calling thing(mocker)!
Let's say i have a method is_validate, which internally calls validate method from library gateway.service
import gateway.service
from gateway.service.exception import ValidatorException
def is_validate():
try:
gateway.service.validate() # which throws ValidatorException
return True
except ValidatorException ex:
return False
How to unit test is_validate method, mocking gateway.service.validate to throw ValidatorException ?
You can do this with a combination of:
mocking a function (creating a fake version of the function dictating what it returns);
monkeypatching the actual function with your mock version;
and using pytest to actually run the test.
I've written a description of how to do this (pulled from my own work) here, in case an example I know works is useful.
But this is what I think you'll need to do in your code:
Define a pytest fixture to mock the scenario you want to test, using monkeypatch to fake the results you want from the parts of the is_validate().
And a test to check that a ValidatorException is raised; the code that raises the error in the test is in the pytest fixture. The entire pytest fixture defined there is passed as a parameter to the test.
import pytest
from unittest import TestCase
import gateway.service
from gateway.service.exception import ValidatorException
# Create object for accessing unittest assertions
assertions = TestCase("__init__")
#pytest.fixture
def validation_fails(monkeypatch):
"""
Mocks a call to gateway.service.validate().
"""
def mock_validate(*args, **kwargs):
"""Mock an absolute file path."""
raise ValidatorException
# Replace calls to existing methods with the mocked versions
monkeypatch.setattr(gateway.service, "validate", mock_validate)
def test_validation_fails(validation_fails):
"""Test validation."""
# check that the correct exception is raised
with assertions.assertRaises(ValidatorException):
is_validate()
Note: This does not include whatever setup is required to get pytest working for your project.
-------------------------------------
mymodule.py
-------------------------------------
import os
def remove(file_path):
if os.path.exists(file_path):
os.remove(file_path)
else:
print('File does not exist')
-------------------------------------
from mymodule import remove
import mock
import unittest
class RemoveTestCase(unittest.TestCase):
#mock.patch('mymodule.os.path')
#mock.patch('mymodule.os.remove')
def test_remove(self, mock_os_remove, mock_os_path):
mock_os_path.exists.return_value = True
#side_effect
remove("any path")
mock_os_remove.assert_called_with("any path")
I was able to mock gateway.service.validate by referencing it with module name where is_validate method is present.
ex: #mock.patch('mymodule.gateway.service.validate')
Reference this doc for more info
While subclassing logging.Handler, I can make a custom handler by doing something like:
import requests
import logging
class RequestsHandler(logging.Handler):
def emit(self, record):
res = requests.get('http://google.com')
print (res, record)
handler = RequestsHandler()
logger = logging.getLogger(__name__)
logger.addHandler(handler)
logger.warning('ok!')
# <Response [200]> <LogRecord: __main__, 30, <stdin>, 1, "ok!">
What would be the simplest RequestHandler (i.e., what methods would it need?) if it was just a base class without subclassing logging.Handler ?
In general, you can find out which attributes of a class is getting accessed externally by overriding the __getattribue__ method with a wrapper function
that adds the name of the attribute being accessed to a set if the caller's class is not the same as the current class:
import logging
import sys
class MyHandler(logging.Handler):
def emit(self, record):
pass
def show_attribute(self, name):
caller_locals = sys._getframe(1).f_locals
if ('self' not in caller_locals or
object.__getattribute__(caller_locals['self'], '__class__') is not
object.__getattribute__(self, '__class__')):
attributes.add(name)
return original_getattribute(self, name)
attributes = set()
original_getattribute = MyHandler.__getattribute__
MyHandler.__getattribute__ = show_attribute
so that:
handler = MyHandler()
logger = logging.getLogger(__name__)
logger.addHandler(handler)
logger.warning('ok!')
print(attributes)
outputs:
{'handle', 'level'}
Demo: https://repl.it/#blhsing/UtterSoupyCollaborativesoftware
As you see from the result above, handle and level are the only attributes needed for a basic logging handler. In other words, #jirassimok is correct in that handle is the only method of the Handler class that is called externally, but one also needs to implement the level attribute as well since it is also directly accessed in the Logger.callHandlers method:
if record.levelno >= hdlr.level:
where the level attribute has to be an integer, and should be 0 if records of all logging levels are to be handled.
A minimal implementation of a Handler class should therefore be something like:
class MyHandler:
def __init__(self):
self.level = 0
def handle(self, record):
print(record.msg)
so that:
handler = MyHandler()
logger = logging.getLogger(__name__)
logger.addHandler(handler)
logger.warning('ok!')
outputs:
ok!
Looking at the source for Logger.log leads me to Logger.callHandlers, which calls only handle on the handlers. So that might be the minimum you need if you're injecting the fake handler directly into a logger instance.
If you want to really guarantee compatibility with the rest of the logging module, the only thing you can do is go through the module's source to figure out how it works. The documentation is a good starting place, but that doesn't get into the internals much at all.
If you're just trying to write a dummy handler for a small use case, you could probably get away with skipping a lot of steps; try something, see where it fails, and build on that.
Otherwise, you won't have much choice but to dive into the source code (though trying things and seeing what breaks can also be a good way to find places to start reading).
A quick glance at the class' source tells me that the only gotchas in the class are related to the module's internal management of its objects; Handler.__init__ puts the handler into a global handler list, which the module could use in any number of places. But beyond that, the class is quite straightforward; it shouldn't be too hard to read.
I have a python method like
import external_object
from external_lib1 import ExternalClass1
from external_lib2 import Hook
class MyClass(self):
def my_method(self):
ExternalClass.get('arg1') #should be mocked and return a specific value with this arg1
ExternalClass.get('arg2') #should be mocked and return a specific value with this arg2
def get_hook(self):
return Hook() # return a mock object with mocked method on it
def my_method(self):
object_1 = external_object.instance_type_1('args') # those are two different object instanciate from the same lib.
object_2 = external_object.instance_type_2('args')
object_1.method_1('arg') # should return what I want when object_1 mocked
object_2.method_2 ('arg') # should return what I want when object_2 mocked
In my test I would like to realise what I put in comments.
I could manage to do it, but every time it gets really messy.
I use to call flexmock for some stuff (by example ExternalClass.get('arg1') would be mock with a flexmock(ExternalClass).should_return('arg').with_args('arg') # etc...) but I'm tired of using different test libs to mock.
I would like to use only the mock library but I struggle to find a consistent way of doing it.
I like to use python's unittest lib. Concretely the unittest.mock which is a great lib to customize side effects and return value in unit tested functions.
They can be used as follows:
class Some(object):
"""
You want to test this class
external_lib is an external component we cannot test
"""
def __init__(self, external_lib):
self.lib = external_lib
def create_index(self, unique_index):
"""
Create an index.
"""
try:
self.lib.create(index=unique_index) # mock this
return True
except MyException as e:
self.logger.error(e.__dict__, color="red")
return False
class MockLib():
pass
class TestSome(unittest.TestCase):
def setUp(self):
self.lib = MockLib()
self.some = Some(self.lib)
def test_create_index(self):
# This will test the method returns True if everything went fine
self.some.create_index = MagicMock(return_value={})
self.assertTrue(self.some.create_index("test-index"))
def test_create_index_fail(self):
# This will test the exception is handled and return False
self.some.create_index = MagicMock(side_effect=MyException("error create"))
self.assertFalse(self.some.create_index("test-index"))
Put the TestSome() class file somewhere like your-codebase-path/tests and run:
python -m unittest -v
I hope it's useful.
I have this code that I want to test:
log = logging.getLogger(__name__)
class A(object):
def __init__(self):
log.debug('Init')
but I cannot figure out how to assert that log.debug was called with 'Init'
I tried patching logger but inspecting it I only found a getLogger mock.
I'm sure its simple, but I just cant figure it!
Thanks in advance for any and all help!
You can use patch.object() on the actual logging object. that lets you verify that you're using the correct logger too:
logger = logging.getLogger('path.to.module.under.test')
with mock.patch.object(logger, 'debug') as mock_debug:
run_code_under_test()
mock_debug.assert_called_once_with('Init')
Alternatively, if you're using Pytest, then it already has a fixture that captures logs for you:
def test_bar(caplog):
with caplog.at_level(logging.DEBUG):
run_code_under_test()
assert "Init" in caplog.text
# or, if you really need to check the log-level
assert caplog.records[-1].message == "Init"
assert caplog.records[-1].levelname == "DEBUG"
More info in the pytest docs on logging
Assuming log is a global variable in a module mymod, you want to mock the actual instance that getLogger returned, which is what invokes debug. Then, you can check if log.debug was called with the correct argument.
with mock.patch('mymod.log') as log_mock:
# test code
log_mock.debug.assert_called_with('Init')
I am late for this question but another of way to achieve it is:
#patch('package_name.module_name.log')
def test_log_in_A(self, mocked_log):
a = A()
mocked_log.debug.assert_called_once_with('Init')
This also works:
from unittest.mock import patch
class Test(TestCase):
def test_logger(self):
with patch('logging.Logger.warning') as mocked_logger:
call_func()
mocked_logger.assert_called_once_with('log')
Here is a complete example
"""
Source to test
"""
import logging
logger = logging.getLogger("abc")
def my_fonction():
logger.warning("Oops")
"""
Testing part
"""
import unittest
from unittest.mock import patch, MagicMock
abc_logger = logging.getLogger("abc")
class TestApp(unittest.TestCase):
#patch.object(abc_logger, "warning", MagicMock())
def test_my_fonction(self):
# When
my_fonction()
# Then
abc_logger.warning.assert_called_once()