I have a function that creates a temporary directory, switches to that temporary directory, performs some work, and then switches back to the original directory. I am trying to write a unit test that tests this. I don't have a problem verifying that the current directory was changed to the temp dir and changed back again, but I'm having a problem verifying that the important stuff took place in between those calls.
My original idea was to abstract the function into three sub functions so that I could test the call order. I can replace each of the three sub functions with mocks to verify that they are called -- however, I am still presented with the issue of verifying the order. On a mock I can use assert_has_calls, but upon what object do I call that function?
Here is the class I'm trying to test:
import shutil
import os
import subprocess
import tempfile
import pkg_resources
class Converter:
def __init__(self, encoded_file_path):
self.encoded_file_path = encoded_file_path
self.unencoded_file_path = None
self.original_path = None
self.temp_dir = None
def change_to_temp_dir(self):
self.original_path = os.getcwd()
self.temp_dir = tempfile.mkdtemp()
os.chdir(self.temp_dir)
def change_to_original_dir(self):
os.chdir(self.original_path)
shutil.rmtree(self.temp_dir)
def do_stuff(self):
pass
def run(self):
self.change_to_temp_dir()
self.do_stuff()
self.change_to_original_dir()
This is as far as I got writing the test case:
def test_converter(self, pkg_resources, tempfile, subprocess, os, shutil):
encoded_file_path = Mock()
converter = Converter(encoded_file_path)
converter.change_to_temp_dir = Mock()
converter.do_stuff= Mock()
converter.change_to_original_dir = Mock()
assert converter.encoded_file_path == encoded_file_path
assert converter.unencoded_file_path is None
converter.run()
Now that I have each function mocked, I can verify THAT they were called, but not in what ORDER. How do I go about doing this?
One workaround would to be to create a separate mock object, attach methods to it and use assert_has_calls() to check the call order:
converter = Converter(encoded_file_path)
converter.change_to_temp_dir = Mock()
converter.do_stuff = Mock()
converter.change_to_original_dir = Mock()
m = Mock()
m.configure_mock(first=converter.change_to_temp_dir,
second=converter.do_stuff,
third=converter.change_to_original_dir)
converter.run()
m.assert_has_calls([call.first(), call.second(), call.third()])
Related
How am I supposed to write this test? I've tried the various options listed but each returns the same failed test:
import zipfile
from mock import Mock, patch
def unzip_file(fp):
with zipfile.ZipFile(fp, 'r') as z:
z.extractall('dir')
#patch('zipfile.ZipFile')
def test_unzip_file(m_zipfile):
# I have tried the following...
# m_zipfile.__enter__.extractall = Mock()
# m_zipfile.extractall = Mock()
# m_zipfile.return_value.__enter__.return_value = Mock()
m_zipfile.return_value.__enter__.return_value.extractall = Mock()
unzip_file('test')
m_zipfile.assert_called_with('test', 'r') # this test passes
m_zipfile.extractall.assert_called_with('dir') # this test fails
I tried to use this answer as a guide but I'm still lost as to how to properly do this. The actual function in our code is more complex with additional parameters but I am trying to start at the base first.
The failure...
E AssertionError: expected call not found.
E Expected: extractall('dir')
E Actual: not called.
You're pretty close, you just need a reference to the mock within the context manager to assert against
#patch('zipfile.ZipFile')
def test_unzip_file(m_zipfile):
m_extractall = Mock()
m_zipfile.return_value.__enter__.return_value.extractall = m_extractall
unzip_file('test')
m_zipfile.assert_called_with('test', 'r') # this test passes
m_extractall.assert_called_with('dir') # and this one does, too
I need to know if Path.exists() was called. I want to do that in a unittest but allways got the answer that it was not called.
I assume something is wrong with the import path. I know from the docs and some blog-posts that this can be tricky.
This is the "productive" code:
import pathlib
class Foo():
def __init__(self, file_path: pathlib.Path):
if not file_path.exists():
print('Not good.')
else:
print('Fine.')
And this is the unittest for it
import unittest
import unittest.mock
import pathlib
import mycode
class MyTest(unittest.TestCase):
#unittest.mock.patch('pathlib.Path')
def test_mycode(self, mock_path):
mypath = pathlib.Path('bar')
foo = mycode.Foo(mypath)
mock_path.exists.assert_called_once()
But the error is still
AssertionError: Expected 'exists' to have been called once. Called 0
times.
You can create mock for pathlib.Path using create_autospec helper function. And pass this mock object to the constructor of the Foo class.
Functions or methods being mocked will have their arguments checked to ensure that they are called with the correct signature.
E.g. (Python 3.9.6)
foo.py:
import pathlib
class Foo():
def __init__(self, file_path: pathlib.Path):
if not file_path.exists():
print('Not good.')
else:
print('Fine.')
foo_test.py:
import unittest
import pathlib
from unittest.mock import create_autospec
from foo import Foo
class MyTest(unittest.TestCase):
def test_Foo_file_path_exists(self):
mock_path = create_autospec(pathlib.Path)
mock_path.exists.return_value = True
Foo(mock_path)
mock_path.exists.assert_called_once()
def test_Foo_file_path_not_exists(self):
mock_path = create_autospec(pathlib.Path)
mock_path.exists.return_value = False
Foo(mock_path)
mock_path.exists.assert_called_once()
if __name__ == '__main__':
unittest.main()
Test result:
Fine.
.Not good.
.
----------------------------------------------------------------------
Ran 2 tests in 0.184s
OK
Name Stmts Miss Cover Missing
----------------------------------------------------------------------
src/stackoverflow/71945781/foo.py 6 0 100%
src/stackoverflow/71945781/foo_test.py 17 0 100%
----------------------------------------------------------------------
TOTAL 23 0 100%
The answer of #slideshowp2 is fine and working. And it pointed me to the right solution better fitting to my own MWE.
class MyTest(unittest.TestCase):
#unittest.mock.patch('pathlib.Path')
def test_mycode(self, mocked_path):
foo = mycode.Foo(mocked_path)
mocked_path.exists.assert_called_once()
The problem with my code was that I instantiated a real object of pathlib.Path. But the key of the solution is to use the mock object mocked_path because this is a surrogate for a real Path object.
In my questions intial code the Foo.__init__() never used the mocked object but the real pathlib.Path.
I have a fixture mocking an external library like so, using pytest-mock, which is a wrapper around unittest.mock.
# client.py
import Test as TestLibrary
class LibraryName():
def get_client():
return TestLibrary.Library()
# library_service.py
def using_library():
'''
Edited note: Library().attribute behind the scenes is set to
self.attribute = Attribute()
so this may be affecting the mocking
'''
client = LibraryName.get_client()
return client.attribute.method()
# conftest.py
#pytest.fixture
def library_client_mock(mocker):
import Test as TestLibrary
return mocker.patch.object(TestLibrary, 'Library')
# test_library_service.py
def test_library_method(library_client_mock):
result = using_library()
I can mock a return value like so:
def test_library_method(library_client_mock):
library_client_mock.return_value.attribute.return_value.method.return_value = "test"
result = using_library()
assert result == "test"
but I can't mock throwing an Exception with side_effect
def test_library_method(library_client_mock):
library_client_mock.return_value.attribute.return_value.method.side_effect = TypeError # doesn't work
library_client_mock.return_value.attribute.return_value.method.side_effect = TypeError() # doesn't work
attrs = { 'attribute.method.side_effect': TypeError }
library_client_mock.configure_mock(**attrs) # doesn't work
with pytest.raises(TypeError):
using_library() # fails assertion
what I missing here?
These are the errors in your code:
Change:
library_client_mock.return_value.attribute.return_value.method.return_value = "test"
To:
library_client_mock.return_value.attribute.method.return_value = "test"
Change:
library_client_mock.return_value.attribute.return_value.method.side_effect = TypeError
To:
library_client_mock.return_value.attribute.method.side_effect = TypeError
Explanation
The .return_value must only be used for callable objects e.g. a function as documented:
return_value
Set this to configure the value returned by calling the mock:
>>> mock = Mock()
>>> mock.return_value = 'fish'
>>> mock()
'fish'
Thus, you can use .return_value only for the following:
TestLibrary.Library()
TestLibrary.Library().attribute.method()
But not for:
TestLibrary.Library().attribute
Because .attribute is not a callable e.g. TestLibrary.Library().attribute().
Warning
The way you are patching Library is via its source location at Test.Library (or aliased as TestLibrary.Library). specifically via:
import Test as TestLibrary
return mocker.patch.object(TestLibrary, 'Library')
It works currently because the way you import and use it is via the root path.
# client.py
import Test as TestLibrary
...
return TestLibrary.Library()
...
But if we change the way we imported that library and imported a local version to client.py:
# client.py
from Test import Library # Instead of <import Test as TestLibrary>
...
return Library() # Instead of <TestLibrary.Library()>
...
It will now fail. Ideally, you should patch the specific name that is used by the system under test, which here is client.Library.
import client
return mocker.patch.object(client, 'Library')
Unless you are sure that all files that will use the library will import only the root and not a local version.
#Niel Godfrey Ponciano set me on the right path with this syntax for the side_effect
library_client_mock.return_value.attribute.method.side_effect = TypeError
but it wasn't enough.
In
# conftest.py
#pytest.fixture
def library_client_mock(mocker):
import Test as TestLibrary
return mocker.patch.object(TestLibrary, 'Library')
I had to add an extra mock:
# conftest.py
#pytest.fixture
def library_client_mock(mocker):
import Test as TestLibrary
mock_library_client = mocker.patch.object(TestLibrary, 'Library')
# option 1
mock_attribute = Mock()
# option 2, path to Library.attribute = Attribute()
mock_attribute = mocker.patch.object(TestLibrary.services, 'Attribute', autospec=True)
mock_library_client.attach_mock(mock_attribute, "attribute")
return mock_library_client
and then both of the following statements worked as expected. Although I am not sure why return_value works out of the box without an attached mock, but side_effect does not.
# return_value set correctly
# NOTE return_value needed after each
library_client_mock.return_value.attribute.return_value.method.return_value = "test"
# side_effect set correctly
# NOTE return_value not needed after "attribute"
library_client_mock.return_value.attribute.method.side_effect = TypeError
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'm writing unit tests to validate my project functionalities. I need to replace some of the functions with mock function and I thought to use the Python mock library. The implementation I used doesn't seem to work properly though and I don't understand where I'm doing wrong. Here a simplified scenario:
root/connector.py
from ftp_utils.py import *
def main():
config = yaml.safe_load("vendor_sftp.yaml")
downloaded_files = []
downloaded_files = get_files(config)
for f in downloaded_files:
#do something
root/utils/ftp_utils.py
import os
import sys
import pysftp
def get_files(config):
sftp = pysftp.Connection(config['host'], username=config['username'])
sftp.chdir(config['remote_dir'])
down_files = sftp.listdir()
if down_files is not None:
for f in down_files:
sftp.get(f, os.path.join(config['local_dir'], f), preserve_mtime=True)
return down_files
root/tests/connector_tester.py
import unittest
import mock
import ftp_utils
import connector
def get_mock_files():
return ['digital_spend.csv', 'tv_spend.csv']
class ConnectorTester(unittest.TestCase)
#mock.patch('ftp_utils.get_files', side_effect=get_mock_files)
def test_main_process(self, get_mock_files_function):
# I want to use a mock version of the get_files function
connector.main()
When I debug my test I expect that the get_files function called inside the main of connector.py is the get_mock_files(), but instead is the ftp_utils.get_files(). What am I doing wrong here? What should I change in my code to properly call the get_mock_file() mock?
Thanks,
Alessio
I think there are several problems with your scenario:
connector.py cannot import from ftp_utils.py that way
nor can connector_tester.py
as a habit, it is better to have your testing files under the form test_xxx.py
to use unittest with patching, see this example
In general, try to provide working minimal examples so that it is easier for everyone to run your code.
I modified rather heavily your example to make it work, but basically, the problem is that you patch 'ftp_utils.get_files' while it is not the reference that is actually called inside connector.main() but probably rather 'connector.get_files'.
Here is the modified example's directory:
test_connector.py
ftp_utils.py
connector.py
test_connector.py:
import unittest
import sys
import mock
import connector
def get_mock_files(*args, **kwargs):
return ['digital_spend.csv', 'tv_spend.csv']
class ConnectorTester(unittest.TestCase):
def setUp(self):
self.patcher = mock.patch('connector.get_files', side_effect=get_mock_files)
self.patcher.start()
def test_main_process(self):
# I want to use a mock version of the get_files function
connector.main()
suite = unittest.TestLoader().loadTestsFromTestCase(ConnectorTester)
if __name__ == "__main__":
unittest.main()
NB: what is called when running connector.main() is 'connector.get_files'
connector.py:
from ftp_utils import *
def main():
config = None
downloaded_files = []
downloaded_files = get_files(config)
for f in downloaded_files:
print(f)
connector/ftp_utils.py unchanged.