I am trying to mock an existing object from another library's class for unit tests with pytest.
However, the attributes (not methods) from the other library are mostly set during runtime.
What I want to achieve
Get all the benefits of mocking the object with spec
Set the (nested) attributes (not methods) needed for my unittests to simulate as if they were set during object creation
from unittest.mock import Mock
from otherlib import ClassName
def test_stuff():
mock_object = Mock(spec=ClassName)
mock_object.data.outward.key = 12345
assert mock_object.data.outward.key == 12345 # AttributeError: Mock object has no attribute 'data'
I made these attempts in code changes but with no success
...
def test_stuff():
mock_object = Mock(spec=ClassName, **{'data.outward.key': 12345})
assert mock_object.data.outward.key == 12345
...
def test_stuff():
mock_object = Mock(spec=ClassName)
attrs = {'data.outward.key': 12345}
mock_object.configure_mock(**attrs)
assert mock_object.data.outward.key == 12345
The best I came up with is using another Mock object to use when setting the main mock object's attributes. It works, but I guess there is a better solution for this...?
from unittest.mock import Mock
from otherlib import ClassName
def test_stuff():
mock_data = Mock(spec=["outward"], key=12345)
mock_object = Mock(spec=ClassName, data=mock_data)
mock_object.data.outward.key = 12345
assert mock_object.data.outward.key == 12345 # tests.py::test_stuff PASSED [100%]
Related
I have a class that contains another class in a variable. Now I want to write a unit-test and define a mock object. Therefore I define a fixture in conftest.py and monkeypatch it with the mock object. I now get a the desired mock object but the inner object is noch the mock object which I defined. The problem ist that my mock that I have created in the fixture (so that the mock returns "Bye") will not be applied. How can I fix this and is there any better solution? See my minimal example below:
module_a.py
class Connection:
def get(self, name):
return f"Hello {name}"
utils.py
from main.module_a import Connection
class Util:
def __int__(self):
self.conn: Connection = Connection()
module_main.py
from main.utils import Util
def main() -> str:
util: Util = Util()
msg: str = util.conn.get(name="Joe")
return msg
conftest.py
from unittest.mock import Mock
import pytest
from main.module_a import Connection
from main.utils import Util
#pytest.fixture(scope="function")
def util_mock(monkeypatch):
conn_mock: Mock = Mock(spec=Connection)
conn_mock.get.return_value = "Bye"
util_mock: Mock = Mock(spec=Util, conn=conn_mock)
monkeypatch.setattr(target="main.module_main.Util", name=util_mock)
return util_mock
test_module_main.py
from unittest.mock import Mock
from main import module_main
def test_main(util_mock: Mock):
msg: str = module_main.main()
test: str = util_mock.conn.get(name="foot")
assert test == "Bye" # work right afer fixture insertion
assert msg == "Bye" # fails after a new object is created
Found a solution myself. When a new object is created (Util()) __call__ is triggered and returns a new object of the mock, hence all defined properties are lost. We just need to return the mock object itself with util_mock.return_value = util_mock.
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
In my object, I have several "Loader" functions for different types of data that might require loading. They're stored as:
import foo
import bar
class MyClass
LOADERS = {
'foo': {
'context_manager': False,
'func': foo.data.loader,
'kwargs': {'data_orientation': 'columns'}
},
'bar': {
'context_manager': True,
'func': bar.load_data
'kwargs': {}
}
}
def load_data(self, load_type, path):
func = self.LOADERS[load_type]['func']
kwargs = self.LOADERS[load_type]['kwargs']
if self.LOADERS[load_type]['context_manager']:
with open(path, 'rb') as f:
self.data=func(f, **kwargs)
else:
self.data = func(path, **kwargs)
This is working very well in practice, but I'm finding it's murder to test.
When I write my test:
from mock import MagicMock, patch
import sys
sys.modules['foo'] = MagicMock()
sys.modules['bar'] = MagicMock()
from mypackage import MyClass
#patch('foo.data.loader')
def test_load_foo(mock_loader):
my_obj = MyClass()
my_obj.load_data('foo', 'path/to/data')
mock_loader.assert_called_once()
it fails. Called 0 times.
I halfway suspect that it's because it's not being called directly. But that shouldn't really matter.
Can anyone offer any suggestions? I'm using pytest as my testing engine, but I've found it plays nicely with mock in the past.
Thanks!
The issue arises because you are trying to patch a function within a class attribute as opposed to an instance attribute. The behavior is not the same. This does however offer us some flexibility as it is a class attribute, thus we can modify the object directly on the class we instantiate in our test.
from unittest.mock import MagicMock
from mypackage import MyClass
def test_load_foo():
key_to_test = "foo"
mock_loader = MagicMock()
my_obj = MyClass()
my_obj.LOADERS[key_to_test]["func"] = mock_loader
my_obj.load_data(key_to_test, 'path/to/data')
mock_loader.assert_called_once()
Instead of trying to patch the function, we modify our objects instance to use the MagicMock.
Using this approach I receive the following message:
collected 1 item
tests/test_mypackage.py . [100%]
======================================================================= 1 passed in 0.01s =======================================================================
How do I mock a class with this path ablib.Pin? And how do I mock the attributes of an instance off Pin? I would say it should work like this:
mock = MagicMock()
mock.Pin = MagicMock()
mock.Pin.kernel_id = 'N21'
mock.Pin.set_value.return_value = True
mock.Pin.get_value.return_value = 3
modules = {
'ablib': mock,
'ablib.Pin': mock.Pin,
}
patcher = patch.dict('sys.modules', modules)
patcher.start()
When I create a Pin instance I and call get_value or set_value I get a MockInstance instead of True or 3.
>>> p = Pin()
>>> p.set_value(3)
<MagicMock name='mock.Pin().set_value(3)' id='47965968'>
>>> p.kernel_id
<MagicMock name='mock.Pin().kernel_id' id='49231056'>
When I call kernel_id directly on Pin I get the result I need.
>>> Pin.kernel_id
'N21'
How do I mock ablib.Pin in a way that I get the values I want from an instance ablib.Pin()
Here is the working version with minimal changes to your code:
from unittest.mock import patch, MagicMock
mock = MagicMock()
mock.Pin().kernel_id = 'N21'
mock.Pin().set_value.return_value = True
mock.Pin().get_value.return_value = 3
modules = {
'ablib': mock
}
patcher = patch.dict('sys.modules', modules)
patcher.start()
Test:
>>> import ablib
>>> ablib.Pin
<MagicMock name='mock.Pin' id='139917634188240'>
>>> ablib.Pin
<MagicMock name='mock.Pin' id='139917634188240'>
>>> ablib.Pin()
<MagicMock name='mock.Pin()' id='139917634233616'>
>>> ablib.Pin()
<MagicMock name='mock.Pin()' id='139917634233616'>
>>> p = ablib.Pin()
>>> p.set_value(3)
True
>>> p.kernel_id
'N21'
Notice that I've removed mock.Pin = MagicMock() and removed mock.Pin from the dictionary. New MagicMock instance is created by default when you access existing MagicMock attribute or call it. The same instance will be returned next time you do that action. So there is no need to explicitly create MagicMock for mock.Pin (for Pin class), mock.Pin() (for Pin instance) and to register them with patch.dict(). Just registering mock is enough.
Thanks to patch.dict() it works even when module ablib doesn't exist. I believe that it is not possible to achieve with patch() and patch.object(). Nevertheless for real unit-testing I use patch() to mock a class and patch.object() to mock attributes of an instance for some particular test or group of tests. I have not been in a situation when I needed to mock a whole module, but I'm quite new to unit-testing and Python.
I'm trying to get the mock library to return a specific mock object with patch. No matter what I try, if I instantiate a new object inside my 'with' clause, I get a generic Mock object, not my customised one.
Here's my code using logging as an example:
import logging
my_mock = Mock()
my_mock.id = 'i-12341234'
with patch('logging.PlaceHolder', new=my_mock) as mockobj:
print mockobj.id # correctly prints 'i-12341234'
newobj = logging.PlaceHolder(None)
print newobj.id # prints <Mock name='mock().id' id='47607184'>
How can I get mock to return my_mock every time a new instance of logging.Placeholder is instantiated?
As it turns out subclassing Mock and passing it using new_callable works. Since I've spent the time trying to get the version above working I'd still appreciate and answer.
import logging
from mock import Mock, patch
my_mock = Mock()
my_mock.id = 'i-12341234'
with patch('logging.PlaceHolder', new=my_mock) as mockobj:
mockobj.return_value = mockobj # <-- added this line.
print mockobj.id
newobj = logging.PlaceHolder(None)
print newobj.id
Alternative
import logging
from mock import Mock, patch
my_mock = Mock()
my_mock.id = 'i-12341234'
my_mock.return_value = my_mock # <---
with patch('logging.PlaceHolder', new=my_mock) as mockobj:
assert mockobj.id == 'i-12341234'
newobj = logging.PlaceHolder(None)
assert newobj.id == 'i-12341234'