TestCase - Exception not raised in with statement when mocking __enter__ - python

I'm trying to test a Python method containing a with statement. The code inside the with statement can raise an RuntimeError. The test I'm talking about tests if the RuntimeError is raised.
The __enter__ and __exit__ methods are heavy (typically open and close SSH connections), and I mock them when testing.
Here is a simplified definition of the method I want to test:
# client.py
class Client():
def method_to_test():
with self:
raise RuntimeError()
For clarification purpose, I omitted the definition of __enter__ and __exit__, and removed all the code in method_to_test which was not involved in the current issue.
To test this method, I mock __enter__ and __exit__, and check if RuntimeError is raised:
# tests.py
from django.test import TestCase
import mock
from .client import Client
class ClientTestCase(TestCase):
#mock.patch('mymodule.client.Client.__enter__')
#mock.patch('mymodule.client.Client.__exit__')
def test_method_raises_Runtime(self, mock_exit, mock_enter):
mock_enter.return_value = None
client = Client()
with self.assertRaises(RuntimeError):
client.method_to_test()
This test fails with: AssertionError: RuntimeError not raised
If I do not mock __enter__, the RuntimeError is raised. Why does mocking __enter__ makes this test fail?

Seen in PEP343 https://www.python.org/dev/peps/pep-0343/
The exception is swallowed if exit() returns true
As self.__exit__ is a MagicMock in Client.method_to_test, self.__exit__ returns a MagicMock, evaluated as True. The RuntimeError is swallowed.
The fix is easy. self.__exit__() as to return None instead of a MagicMock:
# tests.py
from django.test import TestCase
import mock
from .client import Client
class ClientTestCase(TestCase):
#mock.patch('mymodule.client.Client.__enter__')
#mock.patch('mymodule.client.Client.__exit__')
def test_method_raises_Runtime(self, mock_exit, mock_enter):
# __exit__ returns None, evaluated as False
mock_exit.return_value = None
mock_enter.return_value = None
client = Client()
with self.assertRaises(RuntimeError):
client.method_to_test()

Related

Python Mock: raise 3rd party exception for unit testing

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

pytest : How to write pytest code to detect a "func" called without actually executing the "func"

pytest : How to write pytest code to detect a "func" called without actually executing the "func" ,
target.py:
import requests
import threading
CFG=None
targets={}
def add_target(func):
targets[func.__name__] = func
return func
def parent_func(url):
for funct in set(targets):
threading.Thread(target=targets[funct], args=(CFG[url],)).start()
#add_target
def child_func(url):
try:
response = requests.request("POST", url,
headers={"Content-Type": "application/json"},
data="{\"text\": \"target py test\"}")
response.raise_for_status()
except Exception as err:
raise RuntimeError(err)
test_target.py:
During testing, I want child_func() to get called but the child_func() body should not get executed. Rather, after running tests, child_func() gets executed & test run results in "AssertionError: False is not true "
from unittest.mock import Mock, patch
import target
# Third-party imports...
from nose.tools import assert_true, assert_is_not_none
target.CFG={'url':"some valid url"}
#patch('target.child_func')
def test_child_func(mock_child_func):
parent_func("url")
assert_true(mock_child_func.called)
First (for the sake of my own sanity) let's whittle this down to an actual MRE -- we don't actually need threading or requests or any of the implementation details of child_func to demonstrate the problem here.
target.py:
targets={}
def add_target(func):
targets[func.__name__] = func
return func
def parent_func(url):
for func in targets.values():
func(url)
#add_target
def child_func(url):
raise NotImplementedError("The test shouldn't actually call this function!")
test_target.py:
from unittest.mock import Mock, patch
from target import parent_func
#patch('target.child_func')
def test_parent_func(mock_child_func):
parent_func("url")
assert mock_child_func.call_count == 1
Within our test, we want parent_func to call mock_child_func, not the real child_func. But when we run our test, we can quickly see that our patch didn't work:
============================================== short test summary info ===============================================
FAILED test_target.py::test_parent_func - NotImplementedError: The test shouldn't actually call this function!
The reason for this is that parent_func doesn't call child_func, it calls targets['child_func']. Patching 'target.child_func' doesn't modify the actual function, it modifies what the name child_func points to in the target module -- and that name isn't in use in the code under test!
The easiest fix here is to patch target.targets:
from unittest.mock import Mock, patch
from target import parent_func
#patch('target.targets', new_callable=dict)
def test_parent_func(mock_targets):
mock_targets['child_func'] = Mock()
parent_func("url")
assert mock_targets['child_func'].call_count == 1
Now our test passes because when parent_func iterates through its targets, it gets our mock_targets dict, which we can populate with whatever functions/mocks we want.

Unit test object construction that raises ValueError

I have a class which raises ValueError for a non-existent file path constructor parameter and I'd like to test it.
import os
import os.path
class MetricsManager:
def __init__(self, reference_file_path, translations_file_path):
if not os.path.isfile(reference_file_path):
raise ValueError(f'{reference_file_path} does not exist.')
if not os.path.isfile(translations_file_path):
raise ValueError(f'{translations_file_path} does not exist')
self._references_file_path = reference_file_path
self.translations_file_path = translations_file_path
But my test below fails.
import unittest
from src.metrics_manager import MetricsManager
def create_instance():
x = MetricsManager('foo.txt', 'bar.txt')
class MyTestCase(unittest.TestCase):
def test_something(self):
self.assertRaises(ValueError, create_instance())
if __name__ == '__main__':
unittest.main()
I did not have the assertRaises() call coded properly:
self.assertRaises(ValueError, MetricsManager, 'foo.txt', 'bar.txt')
assertRaises takes the function to be called as an argument, not the return value from actually calling it.
class MyTestCase(unittest.TestCase):
def test_something(self):
self.assertRaises(ValueError, create_instance)
If you call it first, the exception occurs before assertRaises gets called. You have to let assertRaises make the actual call.
You can also use assertRaises in a with statement:
class MyTestCase(unittest.TestCase):
def test_something(self):
with self.assertRaises(ValueError):
create_instance()
Here, you do call create_instance yourself, because any exception raised inside the with statement is implicitly caught and passed to the context manager's __exit__ method.

Python mock requests.post to throw exception

Using Python 3.5, requests==2.18.4, Flask==0.12.2, urllib3==1.22
I have a method some_method in my main server.py file, that is supposed to make a POST to some url with some data:
def some_method(url, data):
...
error = None
try:
response = requests.post(url, json=data)
except requests.exceptions.ConnectionError as e:
...
app.logger.error(...)
response = None
error = str(e)
return error, response
The server file defines: app = Flask(__name__), and some_method is called from #app.route(... methods=['PATCH']).
If this method throws an error, the route will eventually return a 500.
Tests are run from a test file importing the app with import server and app = server.app, using unittest, and importing mock.patch.
I am able to test the overall app behavior, with a test that shows that the app route behave as expected when the method returns an error and seeing that the route terminates at the right spot:
class ServerTestCase(unittest.TestCase):
...
#patch('server.some_method')
def test_route_response_status_500_when_throws(self, mock_response):
mock_response.return_value = 'some_error_string', None
response = self.app.patch(some_url, some_data, content_type='application/json')
self.assertEqual(response.status_code, 500)
However, I would really like to have another test to test some_method in isolation:
Mock requests.post to throw requests.exceptions.ConnectionError
Show that the method logs an error (I know I can mock my app.logger and assert that it logged during the execution)
Mock the requests.post function, and on the mock set the side_effect attribute to the desired exception:
#patch('requests.post')
def test_request_post_exception(self, post_mock):
post_mock.side_effect = requests.exceptions.ConnectionError()
# run your test, code calling `requests.post()` will trigger the exception.
From the linked documentation:
This can either be a function to be called when the mock is called, an iterable or an exception (class or instance) to be raised.
[...]
An example of a mock that raises an exception (to test exception handling of an API):
>>> mock = Mock()
>>> mock.side_effect = Exception('Boom!')
>>> mock()
Traceback (most recent call last):
...
Exception: Boom!
(Bold emphasis mine).
This is also covered in the Quick Guide section:
side_effect allows you to perform side effects, including raising an exception when a mock is called:
>>> mock = Mock(side_effect=KeyError('foo'))
>>> mock()
Traceback (most recent call last):
...
KeyError: 'foo'

Mocking fails in Django unittest of utils.py

I am trying to write a unittest for a function named search_ldap(), which searches an LDAP server given a particular username. Here is the function definition in utils.py (note: I'm using Python 3):
from ldap3 import Server, Connection
def search_ldap(username):
result = ()
baseDN = "o=Universiteit van Tilburg,c=NL"
searchFilter = '(uid={})'.format(username)
attributes = ['givenName', 'cn', 'employeeNumber', 'mail']
try:
server = Server('ldap.example.com', use_ssl=True)
conn = Connection(server, auto_bind=True)
conn.search(baseDN, searchFilter, attributes=attributes)
for a in attributes:
result += (conn.response[0]['attributes'][a][0], )
except Exception:
raise LDAPError('Error in LDAP query')
return result
Of course I don't want to actually connect to ldap.example.com during testing, so I've decided to use Python's mock object library to mock the Server() and Connection() classes in my unittests. Here is the test code:
from unittest import mock
from django.test import TestCase
class LdapTest(TestCase):
#mock.patch('ldap3.Server')
#mock.patch('ldap3.Connection')
def test_search_ldap(self, mockConnection, mockServer):
from .utils import search_ldap
search_ldap('username')
self.assertTrue(mockServer.called)
self.assertTrue(mockConnection.called)
This test simply asserts that the mocked Server and Connection objects are instantiated. However, thay don't, because when I run the tests with ./manage.py test I receive the following error:
Creating test database for alias 'default'...
F.
======================================================================
FAIL: test_search_ldap (uvt_user.tests.LdapTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/lib/python3.4/unittest/mock.py", line 1142, in patched
return func(*args, **keywargs)
File "/home/jj/projects/autodidact/uvt_user/tests.py", line 28, in test_search_ldap
self.assertTrue(mockServer.called)
AssertionError: False is not true
----------------------------------------------------------------------
Ran 2 tests in 0.030s
FAILED (failures=1)
Destroying test database for alias 'default'...
Why are my tests failing? How can I successfully mock ldap3's Server and Connection classes?
To mock class you should provide it's fake implementation with required methods. For example:
class FakeServer:
def call():
pass
class LdapTest(TestCase):
#mock.patch('ldap3.Server', FakeServer)
def test_search_ldap(self):
<do you checks here>
With patch() it is important that you patch objects in the namespace where they are looked up. This is explained in the Where to patch section of the documentation. There is a fundamental difference between doing
from ldap3 import Server
server = Server()
and
import ldap3
server = ldap3.Server()
In the first case (also the case in the original question), the name "Server" belongs to the current module. In the second case, the name "Server" belongs to the ldap3 module where it is defined. The following Django unittest patches the correct "Server" and "Connection" names and should work as intended:
from unittest import mock
from django.test import TestCase
class LdapTest(TestCase):
#mock.patch('appname.utils.Server')
#mock.patch('appname.utils.Connection')
def test_search_ldap(self, mockConnection, mockServer):
from .utils import search_ldap
search_ldap('username')
self.assertTrue(mockServer.called)
self.assertTrue(mockConnection.called)

Categories