How to mock a python class dependency using pytest-mock? - python

I am struggling to understand how to stub a class / mock all the methods from a class dependency in Python & pytest. The listing below shows the class that I am testing. It has two internal dependencies: OWMProxy and PyOwmDeserializer.
Class Under Test
class OWM:
def __init__(self, api_key: str, units: WeatherUnits) -> None:
self._api_key = api_key
self._units = units
self._pyorm = OWMProxy.from_api_key(api_key)
self._deserializer = PyOwmDeserializer()
def at(self, city: str, iso_datetime: str) -> ForecastModel:
weather = self._pyorm.for_time(city, iso_datetime)
return self._deserializer.deserialize(weather, self._units)
def day(self, city: str, day: str) -> ForecastModel:
weather = self._pyorm.for_day(city, day)
return self._deserializer.deserialize(weather, self._units)
def now(self, city: str) -> ForecastModel:
weather = self._pyorm.now(city)
return self._deserializer.deserialize(weather, self._units)
My question is, is it possible to mock an entire class dependency when unit test with PyTest?
Currently, the unit test that I have uses mocker to mock each class method, including init method.
I could use a dependency injection approach, i.e. create an interface for the internal deserializer and proxy interface and add these interfaces to the constructor of the class under test.
Alternatively I could test using unittest.mock module, as suggested here. Is there an equivalent functionality in pytest-mock???
Unit Test So Far...
#pytest.mark.skip(reason="not implemented")
def test_owm_initialises_deserializer(
default_weather_units: WeatherUnits, mocker: MockFixture
) -> None:
api_key = "test_api_key"
proxy = OWMProxy(py_OWM(api_key))
patch_proxy = mocker.patch(
"wayhome_weather_api.openweathermap.client.OWMProxy.from_api_key",
return_value=proxy,
)
patch_val = mocker.patch(
"wayhome_weather_api.openweathermap.deserializers.PyOwmDeserializer",
"__init__",
return_value=None,
)
owm = OWM(api_key, default_weather_units)
assert owm is not None

You can just mock the whole class and control the return values and/or side effects of its methods as how its done in the docs.
>>> def some_function():
... instance = module.Foo()
... return instance.method()
...
>>> with patch('module.Foo') as mock:
... instance = mock.return_value
... instance.method.return_value = 'the result'
... result = some_function()
... assert result == 'the result'
Assuming the class under test is located in src.py.
test_owm.py
import pytest
from pytest_mock.plugin import MockerFixture
from src import OWM, WeatherUnits
#pytest.fixture
def default_weather_units():
return 40
def test_owm_mock(
default_weather_units: WeatherUnits, mocker: MockerFixture
) -> None:
api_key = "test_api_key"
# Note that you have to mock the version of the class that is defined/imported in the target source code to run. So here, if the OWM class is located in src.py, then mock its definition/import of src.OWMProxy and src.PyOwmDeserializer
patch_proxy = mocker.patch("src.OWMProxy.from_api_key")
patch_val = mocker.patch("src.PyOwmDeserializer")
owm = OWM(api_key, default_weather_units)
assert owm is not None
# Default patch
print("Default patch:", owm.day("Manila", "Today"))
# Customizing the return value
patch_proxy.return_value.for_day.return_value = "Sunny"
patch_val.return_value.deserialize.return_value = "Really Sunny"
print("Custom return value:", owm.day("Manila", "Today"))
patch_proxy.return_value.for_day.assert_called_with("Manila", "Today")
patch_val.return_value.deserialize.assert_called_with("Sunny", default_weather_units)
# Customizing the side effect
patch_proxy.return_value.for_day.side_effect = lambda city, day: f"{day} in hot {city}"
patch_val.return_value.deserialize.side_effect = lambda weather, units: f"{weather} is {units} deg celsius"
print("Custom side effect:", owm.day("Manila", "Today"))
patch_proxy.return_value.for_day.assert_called_with("Manila", "Today")
patch_val.return_value.deserialize.assert_called_with("Today in hot Manila", default_weather_units)
def test_owm_stub(
default_weather_units: WeatherUnits, mocker: MockerFixture
) -> None:
api_key = "test_api_key"
class OWMProxyStub:
#staticmethod
def from_api_key(api_key):
return OWMProxyStub()
def for_day(self, city, day):
return f"{day} in hot {city}"
class PyOwmDeserializerStub:
def deserialize(self, weather, units):
return f"{weather} is {units} deg celsius"
patch_proxy = mocker.patch("src.OWMProxy", OWMProxyStub)
patch_val = mocker.patch("src.PyOwmDeserializer", PyOwmDeserializerStub)
owm = OWM(api_key, default_weather_units)
assert owm is not None
# Default patch
print("Default patch:", owm.day("Manila", "Today"))
# If you want to assert the calls made as did in the first test above, you can use the mocker.spy() functionality
Output
$ pytest -q -rP
================================================================================================= PASSES ==================================================================================================
______________________________________________________________________________________________ test_owm_mock ______________________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Default patch: <MagicMock name='PyOwmDeserializer().deserialize()' id='139838844832256'>
Custom return value: Really Sunny
Custom side effect: Today in hot Manila is 40 deg celsius
______________________________________________________________________________________________ test_owm_stub ______________________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Default patch: Today in hot Manila is 40 deg celsius
2 passed in 0.06s
As you can see, we are able to control the return values of the methods of the mocked dependencies.

Related

How can I mock an attribute of a class using the mock decorator?

#these classes live inside exchanges/impl/tse/mixins.py
class PacketContext:
capture_tstamp = None
def __init__(self, capture_tstamp=None):
self.capture_tstamp = capture_tstamp
class SubParserMixin():
def __init__(self):
self.context = PacketContext()
def on_packet(self, packet):
self.context.capture_tstamp = packet.capture_timestamp
self.parse_er_data(packet.payload)
#this mock test lives in another python file
from exchanges.impl.tse.mixins import PacketContext
#patch.object(PacketContext, 'capture_tstamp', 1655417400314635000)
def test_receive_timestamp(self):
"""
test receive_timestamp is passed down correctly from PacketContext to on_packet()
"""
assert self.context.capture_tstamp == 1655417400314635000
I am trying to mock the self.capture_tstamp attribute in the PacketContext() class.
But in the above, I am getting an error that says
AssertionError: assert None == 1655417400314635000
E + where None = <exchanges.impl.tse.mixins.PacketContext object at 0x7fb324ac04c0>.capture_tstamp
E + where <exchanges.impl.tse.mixins.PacketContext object at 0x7fb324ac04c0> = <tests.unit.exchanges.tse.test_quote_write.TestTSE testMethod=test_receive_timestamp>.context
It seems very strange that the program is not recognising PacketContext().
You can make use of the patch.object decorator as below
class PacketContext:
capture_tstamp = None
def __init__(self, capture_tstamp=None):
self.capture_tstamp = capture_tstamp
<import_PacketContext_here>
#patch.object(PacketContext, 'capture_tstamp', 1655417400314635000)
def test_receive_timestamp():
test_instance = PacketContext()
assert test_instance.capture_tstamp == 1655417400314635000

Not sure why MyMock.env["key1"].search.side_effect=["a", "b"] works but MyMock.env["key1"] = ["a"] with MyMock.env["key2"] = ["b"] does not work

I had created a simple example to illustrate my issue. First is the setup say mydummy.py:
class TstObj:
def __init__(self, name):
self.name = name
def search(self):
return self.name
MyData = {}
MyData["object1"] = TstObj("object1")
MyData["object2"] = TstObj("object2")
MyData["object3"] = TstObj("object3")
def getObject1Data():
return MyData["object1"].search()
def getObject2Data():
return MyData["object2"].search()
def getObject3Data():
return MyData["object3"].search()
def getExample():
res = f"{getObject1Data()}{getObject2Data()}{getObject3Data()}"
return res
Here is the test that failed.
def test_get_dummy1():
dummy.MyData = MagicMock()
mydummy.MyData["object1"].search.side_effect = ["obj1"]
mydummy.MyData["object2"].search.side_effect = ["obj2"]
mydummy.MyData["object3"].search.side_effect = ["obj3"]
assert mydummy.getExample() == "obj1obj2obj3"
The above failed with run time error:
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/mock.py:1078: StopIteration
Here is the test that passed:
def test_get_dummy2():
dummy.MyData = MagicMock()
mydummy.MyData["object1"].search.side_effect = ["obj1", "obj2", "obj3"]
assert mydummy.getExample() == "obj1obj2obj3"
Am I missing something? I would have expected test_get_dummy1() to work and test_get_dummy2() to fail and not vice versa. Where and how can I find/learn more information about mocking to explain what is going on...
MyData["object1"] is converted to this function call: MyData.__getitem__("object1"). When you call your getExample method, the __getitem__ method is called 3 times with 3 parameters ("object1", "object2", "object3").
To mock the behavior you could have written your test like so:
def test_get_dummy_alternative():
mydummy.MyData = MagicMock()
mydummy.MyData.__getitem__.return_value.search.side_effect = ["obj1", "obj2", "obj3"]
assert mydummy.getExample() == "obj1obj2obj3"
Note the small change from your version: mydummy.MyData["object1"]... became: mydummy.MyData.__getitem__.return_value.... This is the regular MagicMock syntax - we want to to change the return value of the __getitem__ method.
BONUS:
I often struggle with mock syntax and understanding what's happening under the hood. This is why I wrote a helper library: the pytest-mock-generator. It can show you the actual calls made to the mock object.
To use it in your case you could have added this "exploration test":
def test_get_dummy_explore(mg):
mydummy.MyData = MagicMock()
mydummy.getExample()
mg.generate_asserts(mydummy.MyData, name='mydummy.MyData')
When you execute this test, the following output is printed to the console, which contains all the asserts to the actual calls to the mock:
from mock import call
mydummy.MyData.__getitem__.assert_has_calls(calls=[call('object1'),call('object2'),call('object3'),])
mydummy.MyData.__getitem__.return_value.search.assert_has_calls(calls=[call(),call(),call(),])
mydummy.MyData.__getitem__.return_value.search.return_value.__str__.assert_has_calls(calls=[call(),call(),call(),])
You can easily derive from here what has to be mocked.

Mock function that is inherited while using pytest.mark.parametrize

I am new to unit testing. I created some classes to get from an API, deserialize it to JSON and input its values to a DB. All classes are working and I'm writing the unit tests right now. I have the following classes:
import requests
class GetResponse():
def getDeserialize(self, url: str):
ApiResponse = requests.get(f'{url}')
toJson = ApiResponse.json()
return toJson
class MercadoBitcoin(GetResponse):
def __init__(self) -> None:
#super().__init__()
self.beginningOfUrl = 'https://www.mercadobitcoin.net/api'
# This method is to get generally from the API
def standardGet(self, coin: str, method: str):
self.URL = f'{self.beginningOfUrl}/{coin}/{method}/'
urlGet = super().getDeserialize(self.URL)
return urlGet
# This method is to get specifically from the API Day Summary
def daySummary(self, year: int, month: int, day: int, coin: str):
method = 'day-summary'
self.URL = f'{self.beginningOfUrl}/{coin}/{method}/{year}/{month}/{day}'
urlGet = super().getDeserialize(self.URL)
return urlGet
I wrote this test:
class TestMercadoBitcoin():
#pytest.mark.parametrize(
"coin, method, expected",
[
("BTC", "ticker", "https://www.mercadobitcoin.net/api/BTC/ticker/"),
("ETH", "ticker", "https://www.mercadobitcoin.net/api/ETH/ticker/")
]
)
def test_standardGet(self, coin, method, expected):
actual = MercadoBitcoin()
actual.standardGet(coin=coin, method=method)
assert actual.URL == expected
I only want to test if the URL is the same as expected, not to run the request to the API. I tried these approaches for mockings and both didn't work:
#patch(requests.get)
#pytest.mark.parametrize(
"coin, method, expected",
[
("BTC", "ticker", "https://www.mercadobitcoin.net/api/BTC/ticker/"),
("ETH", "ticker", "https://www.mercadobitcoin.net/api/ETH/ticker/")
]
)
def test_standardGet(self, coin, method, expected, mock_requests):
actual = MercadoBitcoin()
actual.standardGet(coin=coin, method=method)
assert actual.URL == expected
#patch(super.getDeserialize)
#pytest.mark.parametrize(
"coin, method, expected",
[
("BTC", "ticker", "https://www.mercadobitcoin.net/api/BTC/ticker/"),
("ETH", "ticker", "https://www.mercadobitcoin.net/api/ETH/ticker/")
]
)
def test_standardGet(self, coin, method, expected, mock_requests):
actual = MercadoBitcoin()
actual.standardGet(coin=coin, method=method)
assert actual.URL == expected

pytest: how to NOT run all test combinations of a parametrized class method?

I have a test class with a parametrization in all methods. I also want to parametrize some methods with other variables. However, I do not want to run all combination (as some of them will always fail).
Consider the following:
#pytest.mark.parametrize("base_url", ["api/v1/actions/", "api/v1/quotes/"])
class TestAPIResponse:
#pytest.mark.parametrize("api_verbose_name", ["ação", "declaração"])
def test_example(self, api_client, base_url, api_verbose_name):
...
With this configuration we have 4 tests:
base_url = "api/v1/actions/" and api_verbose_name = "ação"
base_url = "api/v1/quotes/" and api_verbose_name = "declaração"
base_url = "api/v1/actions/" and api_verbose_name = "declaração"
base_url = "api/v1/quotes/" and api_verbose_name = "ação"
How can I make test_example run only the 3rd and 4th test described above?
Right now I'm doing getting the api_verbose_name through a helper function...
def get_api_verbose_name(base_url: str) -> str:
if "quotes" in base_url:
api_verbose_name = "declaração"
if "actions" in base_url:
api_verbose_name = "ação"
return api_verbose_name
#pytest.mark.parametrize("base_url", ["api/v1/actions/", "api/v1/quotes/"])
class TestAPIResponse:
def test_example(self, api_client, base_url):
api_verbose_name = get_api_verbose_name(base_url=base_url)
...
... but this does not seem to be the ideal way for me.
Can I execute this set of tests without this helper function?
Going slightly off what #AnthonySottile said, if you know the endpoint you want to skip you can mark it in your call to pytest.mark.parametrize. The example below shows how to do that by taking advantage of pytest.param.
import pytest
def example(base, api):
return f"{base}{api}"
#pytest.mark.parametrize("base_url", ["api/v1/actions/", "api/v1/quotes/"])
class TestAPI:
#pytest.mark.parametrize("api_verbose_name",
["ação", pytest.param("declaração", marks=pytest.mark.skip)]
)
def test_example(self, base_url, api_verbose_name):
result = example(base_url, api_verbose_name)
assert result == f"{base_url}{api_verbose_name}"
And when we run the tests we can see that four tests are collected by only two are ran since the rest are skipped.
collected 4 items
test_foo.py ..ss [100%]
================================================================= 2 passed, 2 skipped in 0.02s ==================================================================

how to do argument matching, capturing in python

I am trying to understand how to mock in python external dependencies while doing mock methods argument matching and argument capture.
1) Argument matching:
class ExternalDep(object):
def do_heavy_calc(self, anInput):
return 3
class Client(object):
def __init__(self, aDep):
self._dep = aDep
def invokeMe(self, aStrVal):
sum = self._dep.do_heavy_calc(aStrVal)
aNewStrVal = 'new_' + aStrVal
sum += self._dep.do_heavy_calc(aNewStrVal)
class ClientTest(unittest.TestCase):
self.mockDep = MagicMock(name='mockExternalDep', spec_set=ExternalDep)
###
self.mockDep.do_heavy_calc.return_value = 5
### this will be called twice regardless of what parameters are used
### in mockito-python, it is possible to create two diff mocks (by param),like
###
### when(self.mockDep).do_heavy_calc('A').thenReturn(7)
### when(self.mockDep).do_heavy_calc('new_A').thenReturn(11)
###
### QUESTION: how could I archive the same result in MagicMock?
def setUp(self):
self.cut = Client(self.mockDep)
def test_invokeMe(self):
capturedResult = self.cut.invokeMe('A')
self.assertEqual(capturedResult, 10, 'Unexpected sum')
# self.assertEqual(capturedResult, 18, 'Two Stubs did not execute')
2) Argument Capturing
I cannot find good docs or examples on neither MagicMock or mockito-python able to accommodate the following mocking scenario:
class ExternalDep(object):
def save_out(self, anInput):
return 17
class Client(object):
def __init__(self, aDep):
self._dep = aDep
def create(self, aStrVal):
aNewStrVal = 'new_' + aStrVal if aStrVal.startswith('a')
self._dep.save_out(aNewStrVal)
class ClientTest(unittest.TestCase):
self.mockDep = MagicMock(name='mockExternalDep', spec_set=ExternalDep)
###
self.mockDep.save_out.return_value = 5
### this will be called with SOME value BUT how can I capture it?
### mockito-python does not seem to provide an answer to this situation either
### (unline its Java counterpart with ArgumentCaptor capability)
###
### Looking for something conceptually like this (using MagicMock):
### self.mockDep.save_out.argCapture(basestring).return_value = 11
###
### QUESTION: how could I capture value of parameters with which
### 'save_out' is invoked in MagicMock?
def setUp(self):
self.cut = Client(self.mockDep)
def test_create(self):
capturedResult = self.cut.create('Z')
self.assertEqual(capturedResult, 5, 'Unexpected sum')
### now argument will be of different value but we cannot assert on what it is
capturedResult = self.cut.create('a')
self.assertEqual(capturedResult, 5, 'Unexpected sum')
If anyone could show me how to accomplish these two mocking scenarios (using MagicMock), I would be very grateful! (Please ask if something is unclear.)
Something that might help you is to use assert_called_with with a Matcher.
This will allow you to have a finer grain access to the arguments on your calls. i.e.:
>>> def compare(self, other):
... if not type(self) == type(other):
... return False
... if self.a != other.a:
... return False
... if self.b != other.b:
... return False
... return True
>>> class Matcher(object):
def __init__(self, compare, some_obj):
self.compare = compare
self.some_obj = some_obj
def __eq__(self, other):
return self.compare(self.some_obj, other)
>>> match_foo = Matcher(compare, Foo(1, 2))
>>> mock.assert_called_with(match_foo)

Categories