Assertion Error when logging.exception(error) - python

I have this function in a script called mymodule.py
import logging
def foo():
try:
raise ConnectionError('My Connection Error')
except ConnectionError as ce:
logging.exception(ce)
And I have the test for it called test_mymodule.py:
import unittest
import unittest.mock as um
import mymodule
class TestLoggingException(unittest.TestCase):
#um.patch('mymodule.logging')
def test_connection_error_correctly_logged_without_raising(self, mock_logging):
mymodule.foo()
mock_logging.assert_has_calls(
[um.call(ConnectionError('My Connection Error'))]
)
However, when running test_mymodule.py, the below assertion error is raised.
AssertionError: Calls not found.
Expected: [call(ConnectionError('My Connection Error'))]
Actual: [call(ConnectionError('My Connection Error'))]
Why is it thinking they are different and how could I work around this?

The problem is that two instances of ConnectionError, even if create with the same arguments, are not equal.
You create two instances in your code : in foo and in the um.call().
However, those two instance are not the same, and are therefore not equal. You can illustrate that simply:
>>> ConnectionError("test") == ConnectionError("test")
False
One solution is to check which calls were made to the mockup. The calls are exposed through a variable called mockup_calls.
Something like this
class TestLoggingException(unittest.TestCase):
#um.patch('mymodule.logging')
def test_connection_error_correctly_logged_without_raising(self, mock_logging):
mymodule.foo()
print("Calls are: ", mock_logging.mock_calls)
# Check that logging was called with logging.exception(ConnectionError("My Connection Error"))
calls = mock_logging.mock_calls
assert(len(calls) == 1)
# Unpack call
function_called, args, kwargs = calls[0]
assert(function_called == "exception")
connection_error = args[0]
assert(isinstance(connection_error, ConnectionError))
assert(connection_error.args[0] == "My Connection Error") # This will depend on how ConnectionError is defined
What is tricky about this example is that it would work with types that evaluate equal even if they are not the same, like str("hi" == "hi" will yield True), but not most classes.
Does it help ?

Related

Python Mock: Raising Error for function of Mocked Class

I'm trying to test some code that create or changes directories and files based on some inputs. My issue is raising exceptions when a method of a mocked object is called. For example, say I have some code:
def create_if_not_exists(dest):
dir = os.path.dirname(dest)
if not dir:
# create if it doesn't exist
try:
os.makedirs(os.path.dirname(dest))
except OSError:
pass # Nevermind what goes here, it's unimportant to the question
and a unit test:
#patch('my.package.os')
def test_create_dir_if_not_exists(self, mock_os):
mock_os.path.dirname.return_value = None
mock_os.makedirs.raiseError.side_effect = OSError()
with self.assertRaises(OSError)
create_if_not_exists('test')
This setup returns AssertionError: OSError not raised but my understanding is it should raise the error when the makedirs call is made in the actual (non-test) method. Is my understanding incorrect?
Try with the following patch in the test file:
#patch('my.package.os.path.dirname')
#patch('my.package.os.makedirs')
def test_create_dir_if_not_exists(self, mock_os_makedirs, mock_os_path_dirname):
mock_os_path_dirname.return_value = None
mock_os_makedirs.raiseError.side_effect = OSError()
with self.assertRaises(OSError)
create_if_not_exists('test')

How to unittest two function return values in a function using python unitttest

I have a function in a python program which does a function call twice:
def add_user(uname,dserver,pwd,dinstance,proc1, query1):
db_conn = db_connect(uname,dserver,pwd,dinstance)
if db_conn is not_conn:
print("Failed to connect")
sys.exit(-1)
db_conn.run_proc(proc1)
if db_conn.error_msg:
print("Failed procedure")
sys.exit(-1)
db_conn.run_query(query1)
if db_conn.err_msg:
print("Failed query")
sys.exit(-1)
Now the unit test is as follows:
#patch('mydir.proj_dir.db_execu.db_connect')
def test_add_user(self, mock_conn):
mock_conn.return_value.not_conn.return_value = True
mock_conn.return_value.run_proc.return_value = True
mock_conn.return_value.run_query.return_value = True
mock_conn.return_value.err_msg.side_effect= [True, False]
with self.assertRaises(SystemExit):
add_user(name,dserver,pwd,dinstance,proc1, query1)
print("failed query")
My objective is to test the second error condition. But after adding side_effect it only going to first condition which is displaying as "Failed procedure". I want to test "Failed Query" condition. First two error conditions I have tested but the third one is failing and calling the second condition always. Please advise.
It's not super clear what you're asking exactly. You're calling add_user only once so you're only testing a single "path", you need to call it multiple times to test multiple "paths" through it.
That aside, there's a huge issue with your understanding of mock return_value and side_effect configure mocks as callables, when accessing attributes but not calling them, you're just getting the next mock in the chain, you're not using any of your configuration. mock also supports configuring some of the "magic methods" of the data model, but it does not support configuring __getattr__ (the "simple" attribute access) as that's used internally by mock.
As a result, you should only use mock to patch / replace db_connect to return a pseudo-connection, but rather than a mock that pseudo-connection should be a fake: an object you build yourself which looks like a connection but behaves however you want it e.g.
class PseudoConnection:
def __init__(self, fail_proc=False, fail_query=False):
self._fail_proc = fail_proc
self._fail_query = fail_query
self.err_msg = None # or error_msg, or both
def run_proc(self, proc):
self.err_msg = self._fail_proc
def run_query(self, query):
self.err_msg = self._fail_query
Then you just configure mock_conn.return_value = PseudoConnection(True, False) to test the first case and mock_conn.return_value = PseudoConnection(False, True) for the second.
Also as hinted by the comment you're accessing both error_msg and err_msg, and only attempting to configure err_msg. I don't know which is right, but I doubt both are.

How to mock PythonOperator's python_callable and on_failure_callback?

What and where do I mock, with an Airflow PythonOperator, such that:
the python_callback raises a exception, triggering the call of the on_failure_callback, and
I can test whether that callback is called, and with what arguments?
I have tried mocking both the {python_callable} and PythonOperator.execute, in a number of places, without success.
The code files look something like this:
dags/my_code.py
class CustomException(Exception): pass
def a_callable():
if OurSqlAlchemyTable.count() == 0:
raise CustomException("{} is empty".format(OurSqlAlchemyTable.name))
return True
def a_failure_callable(context):
SlackWebhookHook(
http_conn_id=slack_conn_id,
message= context['exception'].msg,
channel='#alert-channel'
).execute()
dags/a_dag.py
from my_code import a_callable, a_failure_callable
new_task = PythonOperator(
task_id='new-task', dag=dag-named-sue, conn_id='a_conn_id', timeout=30,
python_callable=a_callable,
on_failure_callback=a_failure_callable)
dags/test_a_dag.py
class TestCallback(unittest.TestCase):
def test_on_failure_callback(self):
tested_task = DagBag().get_dag('dag-named-sue').get_task('new-task')
with patch('airflow.operators.python_operator.PythonOperator.execute') as mock_execute:
with patch('dags.a_dag.a_failure_callable') as mock_callback:
mock_execute.side_effect = CustomException
tested_task.execute(context={})
# does failure of the python_callable trigger the failure callback?
mock_callback.assert_called()
# did the exception message make it to the failure callback?
failure_context = mock_callback.call_args[0]
self.assertEqual(failure_context['exception'].msg,
'OurSqlAlchemyTable is empty')O
The test does raise CustomException at the line self.task.execute(context={}) -- but, in the test code itself. What I want is for that error to be
raised in the Airflow code such that the PythonOperator fails and calls on_failure_callback.
I have tried any number of permutations, all either raising in test without
triggering, calling the python_callable, or not finding an object to patch:
patch('dags.a_dag.a_callable') as mock_callable
'a_dag.a_callable'
'dags.my_code.a_callable'
'my_code.a_callable'
'airflow.models.Task.execute'
(Python3, pytest, and mock.)
What am I missing / doing wrong?
(Better still, I would like to verify the arguments passed to SlackWebhookHook. Something like:
with patch('???.SlackWebhookHook.execute') as mock_webhook:
... as above ...
kw_dict = mock_webhook.call_args[-1]
assert kw_dict['http_conn_id'] == slack_conn_id
assert kw_dict['message'] == 'OurSqlAlchemyTable is empty'
assert kw_dict['channel'] == '#alert-channel'
(But I am first focussing on testing the failure callback.)
Thank you in advance.

Python mock failing call assertion

I have been reading into python mocking but can't get my head around why the following code is failing.
I have two classes, a Potato and a PotatoBag like the following. Figure is stored in food.py and Report is stored in bag.py.
class Potato:
def create_potato(self):
pass
def output_potato(self):
pass
class PotatoBag:
def __init__(self, potatoes):
self.potatoes = potatoes
def output_to_file(self):
for fig in self.potatoes:
fig.create_potato()
fig.output_potato()
Currently I am trying to unit test the output method so that Report correctly calls create_figure and output_figure from Figure using a mock. This is my test code:
from unittest.mock import MagicMock, patch
from bag import PotatoBag
from food import Potato
import pytest
#pytest.fixture(scope='module')
def potatoes():
x = Potato()
y = Potato()
return [x, y]
#patch('food.Potato')
def test_output_to_file(mock_potato, potatoes):
test_potato_bag = PotatoBag(potatoes)
test_potato_bag.output_to_file()
mock_potato.return_value.create_potato.assert_called()
mock_potato.return_value.output_potato.assert_called()
Immediately pytest yields an AssertionError stating that create_figure was never called.
_mock_self = <MagicMock name='Potato().create_potato' id='140480853451272'>
def assert_called(_mock_self):
"""assert that the mock was called at least once
"""
self = _mock_self
if self.call_count == 0:
msg = ("Expected '%s' to have been called." %
self._mock_name or 'mock')
> raise AssertionError(msg)
E AssertionError: Expected 'create_potato' to have been called.
/home/anaconda3/lib/python3.7/unittest/mock.py:792: AssertionError
What is wrong with my code?
You are passing the Report a list of Figures from your fixture instead of a mock.
Changing your test to...
#patch('figure.Figure')
def test_output_to_file(mock_figure, figures):
test_report = Report([mock_figure])
test_report.output_to_file()
mock_figure.create_figure.assert_called_once()
mock_figure.output_figure.assert_called_once()
This resolves testing that output_to_file correctly is calling the functions on Figure without actually worrying about setting up a figure and dealing with any side effects or additional complexities that may come with calling those functions. The worries of that can be saved for the unit tests for Figure ;)

PyTest-Mock not working due to AttributeError

I am trying to use PyTest_Mock in order to do some testing in my Python project. I created a very simple test to try it out, but I am getting an AttributeError and I don't know why.
model.py
def square(x):
return x * x
if __name__ == '__main__':
res = square(5)
print("result: {}".format(res))
test_model.py
import pytest
from pytest_mock import mocker
import model
def test_model():
mocker.patch(square(5))
assert model.square(5) == 25
After running python -m pytest I get a failure and the following error:
def test_model():
> mocker.patch(square(5))
E AttributeError: 'function' object has no attribute 'patch'
test_model.py:7: AttributeError
You don't need to import mocker, it's available as fixture, so you just pass it as a parameter in the test function:
def test_model(mocker):
mocker.patch(...)
square(5) evaluates to 25, so mocker.patch(square(5)) will effectively try to patch a number 25. Instead, pass the function name as parameter: either
mocker.patch('model.square')
or
mocker.patch.object(model, 'square')
Once patched, square(5) will not return 25 anymore since the original function is replaced with a mock object that can return anything and will return a new mock object by default. assert model.square(5) == 25 will thus fail. Usually, you patch stuff either to avoid complex test setup or simulate behaviour of components that is desired in test scenario (for example, a website being unavailable). In your example, you don't need mocking at all.
Complete working example:
import model
def test_model(mocker):
mocker.patch.object(model, 'square', return_value='foo')
assert model.square(5) == 'foo'

Categories

Resources