Intercept function call with mock without pytest/unittest/etc - python

I want to avoid using a test runner such as pytest because of slow test discovery and inconvenient logging output in lieu of setting up my test and calling it as a script however I am having trouble intercepting a function call within my test objects and amending its behavior. I have been able to do this in the past with pytest but I can't seem to get access to the function the way I have things setup. Side note: In part, this is an attempt to mock out a call to a RESTful server which should explain why I am interested in getting this to work.
Here is my sample application structure:
# entry_object.py
import inner_object, time
class EntryObject():
def __init__(self, client):
self.inner_obj = inner_object.InnerObject(client)
pass
def time_injector(self, posix_time : int):
self.inner_obj.doit(posix_time)
pass
def the_time_injector(length_in_secs, client) -> None:
entryobj = EntryObject(client)
i = 0;
posix_time = time.time()
while (i < length_in_secs):
entryobj.time_injector(posix_time+i)
i += 1
# inner_object.py
import pretendapi
class InnerObject():
def __init__(self, client):
self.pretend_api_object = pretendapi.PretendApi(client)
def doit(self, posix_time : int) -> None:
logmessage = self.pretend_api_object.call_api_endpoint(posix_time=posix_time)
# pretendapi.py
import bravado
class PretendApi():
def __init__(self, client=None):
print("Created PretendApi")
self.client = bravado.client.SwaggerClient(None) if client is None else client
self.ex = "pretendapi"
def extract_and_verify_apiresult(self, posix_time : int, apiresult : object) -> (bool, object):
return (True, apiresult[0])
def call_api_endpoint(self, posix_time : int) -> object:
future = self.client.Api.ApiEndpoint()
(is_cmd_success, apiresult) = self.extract_and_verify_apiresult(posix_time, future.result())
if not is_cmd_success:
# print(f"is_cmd_success={dollup(is_cmd_success)}")
return is_cmd_success
# print(f"{apiresult}")
return is_cmd_success
# test_entry_object.py
rom bravado.testing.response_mocks import BravadoResponseMock
import mock
import entry_object, pretendapi
def amend_extract_and_verify_apiresult(original_func):
def amended_func(self, posix_time : int, apiresult : object) -> (bool, object):
print(f"{posix_time} {apiresult[0]['ret_msg']}")
return original_func(self, posix_time, object)
def test_time_injector():
# mock the necessary objects
# setup up the unit under test
# run the test
mock_client = mock.Mock(name='mock SwaggerClient')
mock_client.Api.ApiEndpoint.return_value = \
BravadoResponseMock(result=lambda : \
({'ret_code': 0, 'ret_msg': 'OK', 'ext_code': '', 'ext_info': ''}, '<bravado.requests_client.RequestsResponseAdapter object at 0x0000028385176518>')
)
mock.patch.object(pretendapi.PretendApi, 'extract_and_verify_apiresult',
side_effect=amend_extract_and_verify_apiresult(
pretendapi.PretendApi.extract_and_verify_apiresult),
autospec=True) # Needed for the self argument
entry_object.the_time_injector(10, mock_client)
if __name__ == "__main__":
test_time_injector()
Does anybody understand why I can't seem to decorate the behavior of extract_and_verify_apiresult?

Related

replace python class with mocked class

I am new to pytest.
I am trying to mock/replace my client.py with fake_client.py for the testing.
The fake_client class contains the same methods as the client class.
Here is my project structure and code:-
abc/base_class.py
from .client import Client
class PushBase:
def __init__(self, hostname):
self.client = Client(hostname)
def process_product(self, item): # item type is dict {}
product_id = item.get('product_id')
if item.get('state') == 'present':
if not product_id:
# creating product
product_id = self.client.create_product(item.get('data'))
return product_id
# update product
self.client.update_product(item.get('data'))
elif item.get('state') == 'absent':
# delete product
self.client.delete_product(product_id)
This is my client.py with API calls
in abc/client.py
class Client:
def __init__(self, hostname):
self.hostname = hostname
# some other stuff
def create_product(self, params=None):
# some code
def update_product(self, params=None):
# some code
def delete_product(self, params=None):
# some code
I have created a fake client to test against the actual client.py
and it has the same methods as the client.py
in tests/fake_client.py
class FakeClient:
def __init__(self, *args, **kwargs):
pass
def create_product(self):
# some code
def update_product(self):
# some code
def delete_product(self):
# some code
in tests/test_base_class.py
from tests.fake_client import FakeClient
import unittest
from abc.base_class import BaseClass
import pytest
try:
import mock
except ImportError:
from unittest import mock
class TestBaseClassOperations(unittest.TestCase):
def setUp(self):
self.push_base = BaseClass("http://fake_host_nmae/test", "foo", "bar")
self.push_base.client = mock.patch('abc.base_class.Client', new=FakeClient()).start()
def test_create_valid_product(self):
product_dict = { # some stuff }
created_product_id = self.push_base.process_product(product_dict)
# process_product() will call create_product from fake client
# will return 1 if FakeClient().create_product() called
assert created_product_id == 1
I tried it another way.
#pytest.fixture
def fixture_product_creation():
return { # product fixture
}
#mock.patch('abc.base_class.Client', return_value=FakeClient())
class TestBaseClassAgain:
def test_create_valid_product(self, mock_client, fixture_product_creation):
push_base = BaseClass("http://fake_host_nmae/test", "foo", "bar")
created_product_id = push_base.process_product(fixture_product_creation)
expected = 1
assert created_product_id == expected
# how can I use this mock_client here?
Although I can replace the client with the FakeClient, but I am unsure how to arrange all the mock things to get it tested with the assert or assert_called_with calls.
I referred this but not able to arrange it in a proper pythonic way.
Can anyone help me rearrange this and suggest to me any better way to replace the client class with the fake client class by using pytest mock?
Thanks.
In order to properly test this you should make a change to your PushBase class. It is not good practice to instantiate a dependent object in the __init__ method of your class, instead consider passing the object in. This makes testing easier as you can just inject the dependency as needed. Another option would be to make a #classmethod that instantiates the object with the client. In the code below I illustrate how to do the former.
It also appears you have an indentation error as the update_product method can never be called based on the logic you currently have.
# base.py
class PushBase:
def __init__(self, client):
self.client = client
def process_product(self, item): # item type is dict {}
product_id = item.get('product_id')
if item.get('state') == 'present':
if not product_id:
# creating product
product_id = self.client.create_product(item.get('data'))
return product_id
# update product
self.client.update_product(item.get('data'))
elif item.get('state') == 'absent':
# delete product
self.client.delete_product(product_id)
# test_base.py
import pytest
from src.base import PushBase
def test_create_product_id(mocker):
mock_client = mocker.MagicMock()
base = PushBase(mock_client)
item = {
"state": "present",
"data": "fizz"
}
mock_client.create_product.return_value = "ok"
product_id = base.process_product(item)
assert product_id == "ok"
mock_client.create_product.assert_called_once_with("fizz")
def test_update_product(mocker):
mock_client = mocker.MagicMock()
base = PushBase(mock_client)
item = {
"state": "present",
"data": "bang",
"product_id": "baz"
}
base.process_product(item)
mock_client.update_product.assert_called_once_with("bang")
def test_delete_product(mocker):
mock_client = mocker.MagicMock()
base = PushBase(mock_client)
item = {
"state": "absent",
"product_id": "vroom"
}
base.process_product(item)
mock_client.delete_product.assert_called_once_with("vroom")
============================================== test session starts ===============================================
platform darwin -- Python 3.8.9, pytest-7.0.1, pluggy-1.0.0
rootdir: ***
plugins: asyncio-0.18.3, hypothesis-6.48.1, mock-3.7.0
asyncio: mode=strict
collected 3 items
tests/test_base.py ...
=============================================== 3 passed in 0.01s ================================================
I am using the pytest-mock package, which is where the mocker fixture comes from. The nice thing about being able to inject a dependency into your class is you don't need to configure all the methods beforehand, you can modify what you need within each test function. There are improvements you can make to the tests above, but that exercise is left to you. Hopefully this should help you understand the direction you should go in.

Pytest Mock AWS SecurityManager

my project has a file called config.py which has, among others, the following code:
class Secret(Enum):
DATABASE_A = 'name_of_secret_database_A'
DATABASE_A = 'name_of_secret_database_A'
def secret(self):
if self.value:
return get_secret(self.value)
return {}
def get_secret(secret_name):
session = Session()
client = session.client(
service_name='secretsmanager',
region_name='us-east-1',
)
secret_value = client.get_secret_value(SecretId=secret_name)
return loads(secret_value.get('SecretString', "{}"))
I need to somehow mock get_secret in tests with pytest for all enum calls, for example Secret.DATABASE_A.secret ()
You can use monkeypatch to override the behaviour of get_secret(). I have made the get_secret() method a static method of the Secret class, but you can make it part of any module you want and import it as well. Just make sure you change in in the monkeypatch.setattr() call as well.
import pytest
from enum import Enum
class Secret(Enum):
DATABASE_A = 'name_of_secret_database_A'
DATABASE_B = 'name_of_secret_database_B'
def secret(self):
if self.value:
return Secret.get_secret(self.value)
return {}
#staticmethod
def get_secret(secret_name):
session = Session()
client = session.client(
service_name='secretsmanager',
region_name='us-east-1',
)
secret_value = client.get_secret_value(SecretId=secret_name)
return loads(secret_value.get('SecretString', "{}"))
def test_secret_method(monkeypatch):
def get_secret(secret_name):
return "supersecret"
monkeypatch.setattr(Secret, "get_secret", get_secret)
s = Secret.DATABASE_A
assert s.secret() == "supersecret"
This returns into 1 passed test.
What is happening here is, that I created a function get_secret() in my test_secret_method as well, and then overwrite the Secret.get_secret() with that new method. Now, you can use the Secret class in your test_method and be sure what the 'get_secret()' method will return without actually running the original code.

How to properly mock private members of a class

I am trying to write some unit tests for a method that depends on another private method. - As shown in the example below:
def is_member_of(self, group_name):
members = self.__get_group_members(group_name)
The private method that I'd like to mock is __get_group_members; I'd also like to mock the private attribute __user_id since it will be used in the is_member_of function (not shown in the example above).
What I have so far:
import unittest
from unittest import mock
class Test(unittest.TestCase):
group_data = []
user_id = 'test_user_id'
def mock_dependencies(self, x):
x.__user_id = mock.PropertyMock(return_value=self.user_id)
x.__get_group_members = mock.MagicMock(return_value=self.group_data)
def first_test(self):
x = A(('name', 'group'))
self.mock_dependencies(x)
x.is_member_of('test_group')
When I invoke x.is_member_of() the mocking doesn't work as anticipated.
You can access a private attribute in Python since private and protected are by convention. - What you're looking for is basically using _ClassName__private_attribute_name since, python carries out the renaming in order to achieve the convention agreed upon.
Example (returning a MagicMock):
with mock.patch.object(Class, '_ClassName__private_attribute_name', return_value='value') as obj_mock:
pass
Example (returning a raw value):
with mock.patch.object(Class, '_ClassName__private_attribute_name', new_callable=PropertyMock) as obj_mock:
obj_mock.return_value = 'string value'
Class is a reference to the class itself - not the instance.
Complete Example:
from unittest.mock import patch, PropertyMock
from unittest import TestCase, main
class Private:
__attr = 'hello'
class PrivateTest(TestCase):
#patch.object(Private, '_Private__attr', new_callable=PropertyMock)
def test_private_attribute_value_change_decorator_success(self, private_mock):
obj = Private()
private_mock.return_value = 'string'
self.assertEqual('string', obj._Private__attr)
def test_private_attribute_value_change_context_manager_success(self):
with patch.object(Private, '_Private__attr', new_callable=PropertyMock) as o_mock:
obj = Private()
o_mock.return_value = 'mocked value'
self.assertEqual('mocked value', obj._Private__attr)
if __name__ == '__main__':
main()
Modifications to your example:
from unittest import TestCase, mock, main
class A:
__user_id = 3
def __init__(self, user, group):
"""
Your logic is missing - obviously
:param user:
:param group:
"""
def __get_group_members(self):
"""
Your logic is missing - obviously
:return:
"""
return ['user_1', 'user_2']
def is_member_of(self, group_name):
members = self.__get_group_members(group_name)
# will return if the user is a member of the group
return self.__user_id in members
class GroupTest(TestCase):
group_data = [1, 2]
user_id = 'test_user_id'
#mock.patch.object(A, '_A__get_group_members')
#mock.patch.object(A, '_A__user_id', new_callable=mock.PropertyMock)
def test_this_is_my_first_success(self, user_id_mock: mock.PropertyMock, get_group_members_mock: mock.MagicMock):
get_group_members_mock.return_value = self.group_data
user_id_mock.return_value = 3
x = A('user_3', 'this_group')
self.assertEqual(False, x.is_member_of('test_group'))
#mock.patch.object(A, '_A__get_group_members')
#mock.patch.object(A, '_A__user_id', new_callable=mock.PropertyMock)
def test_this_is_my_first_failure(self, user_id_mock: mock.PropertyMock, get_group_members_mock: mock.MagicMock):
get_group_members_mock.return_value = self.group_data
user_id_mock.return_value = 1
x = A('user_1', 'this_group')
self.assertEqual(True, x.is_member_of('test_group'))
if __name__ == '__main__':
main()
If you know you'll mock these two attributes in all test cases you can add the decorators on the class level and expect the arguments like-wise.
In the case where the attribute is set through the __init__ or any other method, you could simply alter it as shown below.
from unittest import TestCase, mock, main
class A:
def __init__(self, user, group):
"""
Your logic is missing - obviously
:param user:
:param group:
"""
def __get_group_members(self):
"""
Your logic is missing - obviously
:return:
"""
return ['user_1', 'user_2']
def is_member_of(self, group_name):
members = self.__get_group_members(group_name)
# will return if the user is a member of the group
return self.__user_id in members
class GroupTest(TestCase):
group_data = [1, 2]
user_id = 'test_user_id'
#mock.patch.object(A, '_A__get_group_members')
def test_this_is_my_first_success(self, get_group_members_mock: mock.MagicMock):
x = A('user_3', 'this_group')
x._A__user_id = 5
get_group_members_mock.return_value = self.group_data
self.assertEqual(False, x.is_member_of('test_group'))
#mock.patch.object(A, '_A__get_group_members')
def test_this_is_my_first_failure(self, get_group_members_mock: mock.MagicMock):
get_group_members_mock.return_value = self.group_data
x = A('user_1', 'this_group')
x._A__user_id = 1
self.assertEqual(True, x.is_member_of('test_group'))
if __name__ == '__main__':
main()

How can I pass a ctx (Context) to CliRunner?

CliRunner lists no parameter to provide a context in its documentation.
The following should qualify as a minimum working example.
The real problem is a bit different.
It could be solved by moving the click decorated function into its own function for test coverage. Then the click function would be rendered almost useless.
import click
from click.testing import CliRunner
class Config():
def __init__(self):
self.value = 651
#click.command()
#click.pass_context
def print_numberinfo(ctx):
if not hasattr(ctx.obj, 'value'):
ctx.obj = Config()
click.echo(ctx.obj.value)
def test_print_numberinfo():
ctx = click.Context(print_numberinfo, obj = Config())
ctx.obj.value = 777
runner = CliRunner()
# how do I pass ctx to runner.invoke?
result = runner.invoke(print_numberinfo)
assert result.output == str(ctx.obj.value) + '\n'
You would directly pass your Config instance as keyword argument obj to runner.invoke:
import click
from click.testing import CliRunner
class Config():
def __init__(self):
self.value = 651
#click.command()
#click.pass_obj
def print_numberinfo(obj):
if not hasattr(obj, 'value'):
obj = Config()
click.echo(obj.value)
def test_print_numberinfo():
obj = Config()
obj.value = 777
runner = CliRunner()
# how do I pass ctx to runner.invoke?
result = runner.invoke(print_numberinfo, obj=obj)
assert result.output == str(obj.value) + '\n'
For someone who just want to make context.obj works like call from command-line:
CliRunner().invoke(commands.cli, ['sayhello'], catch_exceptions=False)
The first argument should be the the root group of click, then you can pass the command you want to call to the second argument(that is sayhello).
How commands.py like:
# !/usr/bin/env python
# coding: utf-8
import click
#click.group()
#click.pass_context
def cli(ctx):
ctx.obj = {
'foo': 'bar'
}
#cli.command()
#click.pass_context
def sayehello(ctx):
click.echo('hello!' + ctx.obj)
Appreciate to geowurster providing the solution.

How to access the py.test capsys from inside a test?

py.test documentations says that I should add capsys parameter to my test methods but in my case this doesn't seem to be possible.
class testAll(unittest.TestCase):
def setUp(self):
self.cwd = os.path.abspath(os.path.split(inspect.getfile(inspect.currentframe()))[0])
os.chdir(self.cwd)
def execute(self, cmd, result=0):
"""
Helper method used by many other tests, that would prevent replicating too much code.
"""
# cmd = "%s > /dev/null 2>&1" % cmd
ret = os.system(cmd) >> 8
self.assertEqual(ret, result, "`%s` returned %s instead of %s (cws=%s)\n\t%s" % (cmd, ret, result, os.getcwd(), OUTPUT)) ### << how to access the output from here
def test_1(self):
self.execute("do someting", 0)
You could define a helper function in the class that inherits the capsys fixture:
#pytest.fixture(autouse=True)
def capsys(self, capsys):
self.capsys = capsys
Then call this function inside the test:
out,err = self.capsys.readouterr()
assert out == 'foobar'
Kudos to MichaƂ Krassowski for his workaround which helped me work through a similar problem.
https://github.com/pytest-dev/pytest/issues/2504#issuecomment-309475790
Thomas Wright's answer is perfect. I'm just sticking this code block here for my own reference as my search led me here and I'll likely forget this in future! [doing a few things in this so useful reference for me]. If anyone is looking and sees where it can be improved - suggest away!
import os
import pytest
from _pytest.monkeypatch import MonkeyPatch
from unittest import TestCase
# -----------------------------------------------------------------------------
def foo_under_test(inp1):
"""Example of a Method under test"""
do_some_calcs_here = inp1*2
get_a_return = ClassCalled.foo_called(do_some_calcs_here)
return get_a_return
# -----------------------------------------------------------------------------
class ClassUnderTest():
"""Example of a Class contained Method under test"""
def __init__(self):
"""Instantiate the class"""
self.var1 = "TestVar"
def foo_under_test2(self, inp11):
"""The class method under test"""
return self.var1 + self.foo_called2(inp11)
def foo_called2(self, inp12):
"""Nominal sub-foo to foo_under_test2"""
return str(inp12*5)
# -----------------------------------------------------------------------------
class ClassCalled:
"""Example of a class that could be called by foo_under_test"""
def foo_called(inp2):
"""Sub-foo to foo_under_test"""
return inp2 * 2
# -----------------------------------------------------------------------------
class MockResponses:
"""Class for holding the mock responses"""
def foo_called(inp2):
"""**Mock of foo_called**"""
return inp2*3
def foo_called2(inp12):
"""**Mock of foo_called**"""
return str(inp12*4)
# -----------------------------------------------------------------------------
class Test_foo_under_test(TestCase):
"""Test class - means of grouping up tests for a target function
This one is addressing the individual function (not within a class)
"""
# ---------------------------------------------------------------
#pytest.fixture(autouse=True)
def capsys(self, capsys):
"""Capsys hook into this class"""
self.capsys = capsys
def print_to_console(self, strOut):
"""Print strOut to console (even within a pyTest execution)"""
with self.capsys.disabled():
print(strOut)
def setUp(self):
"""Ran by pyTest before running any test_*() functions"""
self.monkeypatch = MonkeyPatch()
# ---------------------------------------------------------------
def test_1(self):
"""**Test case**"""
def mock_foo_called(inp2):
return MockResponses.foo_called(inp2)
mockedFoo = ClassCalled.foo_called # Need to get this handle here
self.monkeypatch.setattr(ClassCalled, "foo_called", mock_foo_called)
x = foo_under_test(1)
self.print_to_console("\n")
strOut = "Rtn from foo: " + str(x)
self.print_to_console(strOut)
assert x == 6
# Manually clear the monkey patch
self.monkeypatch.setattr(
ClassCalled, "foo_called", mockedFoo)
"""I've noticed with me having monkeypatch inside the
class, the damn thing persists across functions.
This is the only workaround I've found so far"""
# -----------------------------------------------------------------------------
class Test_ClassUnderTest_foo_under_test(TestCase):
"""Test class - means of grouping up tests for a target function
This one is addressing the function within a class
"""
# ---------------------------------------------------------------
#pytest.fixture(autouse=True)
def capsys(self, capsys):
"""Capsys hook into this class"""
self.capsys = capsys
def print_to_console(self, strOut):
"""Print strOut to console (even within a pyTest execution)"""
with self.capsys.disabled():
print(strOut)
def setUp(self):
"""Ran by pyTest before running any test_*() functions"""
self.monkeypatch = MonkeyPatch()
# ---------------------------------------------------------------
def test_1(self):
"""**Test case**"""
def mock_foo_called2(self, inp2):
"""
Mock function
Defining a mock function, note this can be dealt with directly
here, or if its more comprehensible, put it in a separate class
(i.e. MockResponses)
"""
# return MockResponses.foo_called2(inp2) # Delegated approach
return str(inp2*4) # Direct approach
"""Note that the existence of self within this test class forces
a wrapper around calling a MockClass - so we have to go through
both the line below and the function mock_foo_called2() above to
properly invoke MockResponses.foo_called2()
"""
mockedFoo = ClassUnderTest.foo_called2
self.monkeypatch.setattr(
ClassUnderTest, "foo_called2", mock_foo_called2)
x = ClassUnderTest().foo_under_test2(1)
strOut = "Rtn from foo: " + str(x)
self.print_to_console("\n")
self.print_to_console(strOut)
assert x == "TestVar" + str(4)
self.monkeypatch.setattr(
ClassUnderTest, "foo_called2", mockedFoo)
# -----------------------------------------------------------------------------
# ---- Main
if __name__ == "__main__":
#
# Setup for pytest
outFileName = os.path.basename(__file__)[:-3] # Remove the .py from end
currScript = os.path.basename(__file__)
# -------------------------------------------------------------------------
# PyTest execution
pytest.main([currScript, "--html", outFileName + "_report.html"])
rtnA = foo_under_test(1)
print(rtnA == 4)
# This should output 4, demonstrating effect of stub (which produced 6)
rtnB = ClassUnderTest().foo_under_test2(1)
print(rtnB == "TestVar"+str(5))
# This should output "TestVar5", demonstrating effect of stub
# conftest.py
class TTY:
def communicate(self):
with self.trace():
print('wow!')
#pytest.fixture(autouse=True)
def set_capsys(capsys):
TTY.trace = capsys.disabled
#pytest.fixture
def get_tty():
_get_tty():
return TTY()
return _get_tty
# test_wow.py
def test_wow(get_tty):
get_tty().communicate()

Categories

Resources