Mock testing with nested function changes - python

I am adding testing to a pipeline project, code is already written and in production so it cannot be changed to accommodate the tests.
In simplest terms, if I have a function like so:
def other_foo():
return 1
def foo():
res = other_foo()
return res
In practicality, the other_foo call will return a variety of responses, but for testing, I want to create a fixed response to test foo.
So in my test I want to create a fixed response to other_foo of 2. and my test evaluation to be something like:
def test_foo():
# some mocking or nesting handle here for other_foo
res = foo()
assert res == 2

Use the patch decorator from unitest.mock and patch your module local variable.
from your.module import foo
from unitest.mock import patch
#patch('your.module.other_foo')
def test_foo(mock_other_foo):
mock_other_foo.return_value = 3
assert foo() == 3
mock_other_foo.return_value = 42
assert foo() == 42
You can find more information here and there.

Related

Test assert function order with mocks (pytest -> assert_has_calls)

I'm trying to test the order of the sub-functions inside of the main function:
def get_data():
pass
def process_data(data):
pass
def notify_admin(action):
pass
def save_data(data):
pass
def main_func():
notify_admin('start')
data = get_data()
processed_data = process_data(data)
save_data(processed_data)
notify_admin('finish')
I'm using pytest, so far I've come up with this:
import pytest
from unittest.mock import patch, Mock, call
from main_func import main_func
#patch('main_func.notify_admin')
#patch('main_func.get_data')
#patch('main_func.process_data')
#patch('main_func.save_data')
def test_main_func(mock_4, mock_3, mock_2, mock_1):
execution_order = [mock_1, mock_2, mock_3, mock_4]
order_mock = Mock()
for order, mock in enumerate(execution_order):
order_mock.attach_mock(mock, f'f_{order}')
main_func()
order_mock.assert_has_calls([
call.f_1(),
call.f_2(),
call.f_3(),
call.f_4(),
call.f_1(),
])
This is an error, which I'm not sure how to resolve:
E AssertionError: Calls not found.
E Expected: [call.f_1(), call.f_2(), call.f_3(), call.f_4(), call.f_1()]
E Actual: [call.f_1('start'),
E call.f_2(),
E call.f_3(<MagicMock name='mock.f_3()' id='2049968460848'>),
E call.f_4(<MagicMock name='mock.f_2()' id='2049968489424'>),
E call.f_1('finish')]
Could you please suggest ways to resolve it or maybe implement it in a different way?
I've read documentation of assert_has_calls but I'm still not sure how to use it for this particular case.
If you want to check the call order without the argument list, you can use the method_calls attribute of the mock, which contains a list of calls in the order they are made, and only check their name:
...
main_func()
assert len(order_mock.method_calls) == 4
assert order_mock.method_calls[0][0] == "f_1"
assert order_mock.method_calls[1][0] == "f_2"
assert order_mock.method_calls[2][0] == "f_3"
assert order_mock.method_calls[3][0] == "f_4"
Each method call is a tuple of name, positional arguments and keyword arguments, so if you want to check only the name you can just use the first index.
Note that the output of your test does not seem to match this, but this is a matter of your actual application logic.
If you are using has_calls, you have to provide each argument, which is also possible. This time taking the actual result of your test, something like this should work:
...
main_func()
order_mock.assert_has_calls([
call.f_1('start'),
call.f_2(),
call.f_3(mock1),
call.f_4(mock2),
call.f_1('finish')
])

How can you mock a closure (inner function) in python?

I have a python function with a lot of functionality and several inner functions. I want to mock out the return value of one of those functions. Is it possible to use the mock package to mock out the inner function?
Here's an example
def outer(values):
a = 1
def inner():
return np.mean(values)
if inner() == 1:
return None
return inner()
Ok it's a strange example, but what I want is to mock out inner() to return a certain value. I tried to mock with #mock.patch('outer.inner') and I tried #mock.patch.object(outer, 'inner'), but neither works. Is it possible to mock a closure?
As far as I've found so far the answer is "you can't". Disappointing, but actionable.
In my case I was able to mock out some other call such that the closure returned what I wanted. In the example above it would be like
def test_outer_mean_1(self):
with mock.patch('np.mean', return_value=1):
self.assertIsNone(outer(None))
def test_outer_mean_not_1(self):
with mock.patch('np.mean', return_value=2):
self.assertEqual(2, outer(None))
If anybody comes up with a better answer, I'd be eager to hear it.
related question Mocking a local variable of a function in python
Disclaimer: I'm not saying this is the right approach, mocking np.mean is much better.
I have come up with a workaround: the idea is to change the code of the function at run time and execute the new function.
Here is the code:
from _pytest._code import Code
def convert_function_in_function(func):
context = getattr(func, "__globals__", {})
code = Code.from_function(func)
source = code.source()
new_body = ["from unittest import mock", "new_mock = mock.MagicMock()"] + source.lines[0:2] + [
" inner=new_mock"] + source.lines[4:]
compiled = compile("\n".join(new_body), str(code.path), "exec")
exec(compiled, context)
return context['outer'], context['new_mock']
def test_outer_mean_specific_value():
new_outer, mock_inner = convert_function_in_function(outer)
mock_inner.return_value = 2
assert 2 == new_outer(5)
Explanation: convert_function_in_function makes the code to be
from unittest import mock
new_mock = mock.MagicMock()
def outer(values):
a = 1
inner=new_mock
if inner() == 1:
return None
return inner()
Then it returns the new function and the matching mock. You can then change the mock behaviour and call the new function.

How to access marks list outside test script

I have a mark, let say, specific_case = pytest.mark.skipif(<CONDITION>) which I need to apply to some test-cases. I want property value to return different value in case mark applied. This is my simplified code:
module.py:
import pytest
class A():
#property
def value(self):
_marks = pytest.mark._markers # current code to get applied marks list
if 'specific_case' in _marks:
return 1
else:
return 2
test_1.py:
import pytest
from module import A
pytestmark = [pytest.mark.test_id.TC_1, pytest.mark.specific_case]
def test_1():
a = A()
assert a.value == 1
But that doesn't work as pytest.mark._markers returns set(['TC_1', 'skipif']) but not exact pytestmark list (I expect set(['TC_1', 'specific_case']) or at least pytestmark as it is - [pytest.mark.test_id.TC_1, pytest.mark.specific_case]).
So is there any way I can access exact pytestmark list outside test function?
P.S. I also found some tips of how to get mark list using fixtures, but I should stick to current implementation of module.py and test_1.py, so cannot use fixture.
Also there are many other marks with skip conditions (specific_case_2 = pytest.mark.skipif(<CONDITION_2>), specific_case_3 = pytest.mark.skipif(<CONDITION_3>),...), so I cannot use just if 'skipif' in _marks solution
Since your module.py accesses pytest marks, then it is safe to assume that it is part of the test code.
With that said, in case you are you open to changing the class property A.value into a pytest fixture, then this alternative solution might work fine for you. Otherwise, this wouldn't suffice.
Alternative Solution
Instead of using pytest.mark._markers to retrieve the marks list, use request.keywords.
class FixtureRequest
keywords
Keywords/markers dictionary for the underlying node.
import pytest
# Data
class A():
#property
def value(self):
_marks = pytest.mark._markers # Current code to get applied marks list
print("Using class property A.value:", list(_marks))
if 'specific_case' in _marks:
return 1
else:
return 2
#pytest.fixture
def a_value(request): # This fixture can be in conftest.py so all test files can see it. Or use pytest_plugins to include the file containing this.
_marks = request.keywords # Alternative style of getting applied marks list
print("Using pytest fixture a_value:", list(_marks))
if 'specific_case' in _marks:
return 1
else:
return 2
# Tests
pytestmark = [pytest.mark.test_id, pytest.mark.specific_case]
def test_first():
a = A()
assert a.value != 1 # 'specific_case' was not recognized as a marker
def test_second(a_value):
assert a_value == 1 # 'specific_case' was recognized as a marker
Output:
pytest -q -rP --disable-pytest-warnings
.. [100%]
================================================================================================= PASSES ==================================================================================================
_______________________________________________________________________________________________ test_first ________________________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Using class property A.value: ['parametrize', 'skipif', 'skip', 'trylast', 'filterwarnings', 'tryfirst', 'usefixtures', 'xfail']
_______________________________________________________________________________________________ test_second _______________________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout setup ------------------------------------------------------------------------------------------
Using pytest fixture a_value: ['specific_case', '2', 'test_1.py', 'test_second', 'test_id']
2 passed, 2 warnings in 0.01s

Reset class and class variables for each test in Python via pytest

I created a class to make my life easier while doing some integration tests involving workers and their contracts. The code looks like this:
class ContractID(str):
contract_counter = 0
contract_list = list()
def __new__(cls):
cls.contract_counter += 1
new_entry = super().__new__(cls, f'Some_internal_name-{cls.contract_counter:10d}')
cls.contract_list.append(new_entry)
return new_entry
#classmethod
def get_contract_no(cls, worker_number):
return cls.contract_list[worker_number-1] # -1 so WORKER1 has contract #1 and not #0 etc.
When I'm unit-testing the class, I'm using the following code:
from test_helpers import ContractID
#pytest.fixture
def get_contract_numbers():
test_string_1 = ContractID()
test_string_2 = ContractID()
test_string_3 = ContractID()
return test_string_1, test_string_2, test_string_3
def test_contract_id(get_contract_numbers):
assert get_contract_ids[0] == 'Some_internal_name-0000000001'
assert get_contract_ids[1] == 'Some_internal_name-0000000002'
assert get_contract_ids[2] == 'Some_internal_name-0000000003'
def test_contract_id_get_contract_no(get_contract_numbers):
assert ContractID.get_contract_no(1) == 'Some_internal_name-0000000001'
assert ContractID.get_contract_no(2) == 'Some_internal_name-0000000002'
assert ContractID.get_contract_no(3) == 'Some_internal_name-0000000003'
with pytest.raises(IndexError) as py_e:
ContractID.get_contract_no(4)
assert py_e.type == IndexError
However, when I try to run these tests, the second one (test_contract_id_get_contract_no) fails, because it does not raise the error as there are more than three values. Furthermore, when I try to run all my tests in my folder test/, it fails even the first test (test_contract_id), which is probably because I'm trying to use this function in other tests that run before this test.
After reading this book, my understanding of fixtures was that it provides objects as if they were never called before, which is obviously not the case here. Is there a way how to tell the tests to use the class as if it hasn't been used before anywhere else?
If I understand that correctly, you want to run the fixture as setup code, so that your class has exactly 3 instances. If the fixture is function-scoped (the default) it is indeed run before each test, which will each time create 3 new instances for your class. If you want to reset your class after the test, you have to do this yourself - there is no way pytest can guess what you want to do here.
So, a working solution would be something like this:
#pytest.fixture(autouse=True)
def get_contract_numbers():
test_string_1 = ContractID()
test_string_2 = ContractID()
test_string_3 = ContractID()
yield
ContractID.contract_counter = 0
ContractID.contract_list.clear()
def test_contract_id():
...
Note that I did not yield the test strings, as you don't need them in the shown tests - if you need them, you can yield them, of course. I also added autouse=True, which makes sense if you need this for all tests, so you don't have to reference the fixture in each test.
Another possibility would be to use a session-scoped fixture. In this case the setup would be done only once. If that is what you need, you can use this instead:
#pytest.fixture(autouse=True, scope="session")
def get_contract_numbers():
test_string_1 = ContractID()
test_string_2 = ContractID()
test_string_3 = ContractID()
yield

Mock nested method in Python

Wracking my brain on this. I want to mock generator methods self.api.redditor(username).comments.new(limit=num) and self.api.redditor(username).submissions.new(limit=num) below, in which self.api is assigned to a class instance, as in self.api = PrawReddit()
I'm trying to test the size of the result: self.assertEqual(len(result), 5)
So far, I tried MockPraw.return_value.redditor.return_value.comments.return_value.new.return_value.__iter__.return_value = iter(['c' * 10]) but the test fails with AssertionError: 0 != 5
Any tips much appreciated.
def get_comments_submissions(self, username, num=5):
"""Return max `num` of comments and submissions by `username`."""
coms = [
dict(
title=comment.link_title,
text=comment.body_html,
subreddit=comment.subreddit_name_prefixed,
url=comment.link_url,
created=datetime.fromtimestamp(comment.created_utc, pytz.utc),
)
for comment in self.api.redditor(username).comments.new(limit=num)
]
subs = [
dict(
title=submission.title,
text=submission.selftext_html,
subreddit=submission.subreddit_name_prefixed,
url=submission.url,
created=datetime.fromtimestamp(submission.created_utc, pytz.utc),
)
for submission in self.api.redditor(username).submissions.new(limit=num)
]
return coms + subs if len(coms + subs) < num else (coms + subs)[:num]
To mock a generator (unless you are using specific generator features) you can use an iterator as a stand-in eg
import unittest.mock as mock
generator_mock = Mock(return_value=iter(("foo", "bar")))
When you have nested structures like in your example this gets a little more complex, attribute access is automatically handled but return_value from a function must be defined. From your example:
# API mock
mock_api = Mock()
mock_api.redditor.return_value = mock_subs = Mock()
# Submissions mock
mock_subs.new.return_value = iter(("foo", "bar"))
This can then be called and asserted
for item in mock_api.api.redditor("user").submissions.new(limit=5):
print(item)
mock_api.redditor.assert_called_with("user")
mock_subs.new.assert_called_with(limit=5)
As the API is a member of the same class, this is going to have to be monkey patched eg:
target = Praw()
target.api = mock_api()
target.get_comments_submissions("user")
mock_api.redditor.assert_called_with("user")
mock_subs.new.assert_called_with(limit=5)
Note that the iterator in return value is a single instance and a second call to get the iterator will return the same instance.
Writting like you use pytest-mock and everything happens in mymodule (you imported the class at the top of the module like from xy import PrawReddit):
mocker.patch("datetime.fromtimestamp")
mocked_comment = mocker.MagicMock()
mocked_submission = mocker.MagicMock()
mocked = mocker.patch("mymodule.PrawReddit")
mocked.return_value.redditor.return_value.comments.new.return_value = [mocker.MagicMock(), mocked_comment]
mocked.return_value.redditor.return_value.submisions.new.return_value = [mocker.MagicMock(), mocked_submission]
returned = instance.get_comments_submissions("foo", num=2)
assert mocked.return_value.redditor.call_count = 2
mocked.return_value.assert_called_with("foo")
assert returned[-1]["link_title"] == mocked_comment.link_title
Another test call with the same intro:
# ...
returned = instance.get_comments_submissions("foo")
assert mocked.return_value.redditor.call_count = 2
mocked.return_value.assert_called_with("foo")
assert returned[1]["link_title"] == mocked_comment.link_title
assert returned[-1]["title"] == mocked_submission.title

Categories

Resources