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.
Related
am trying to mock a class instance while testing a method something like below
source
main_proc.py
devinstance.py
prodinstance.py
requirements.txt
host.json
main_proc.py
def get_instance(self)
ins = None
env = os.getenv('env', 'dev')
if env == 'dev':
ins = DevInstance()
else:
ins = ProdInstance()
return ins
Sample DevInstance class
devinstance.py
class DevInstance:
def __init__(self):
self.eh_client = dict()
self.initialize()
def initialize(self):
try:
client = EventHubProducerClient.from_connection_string(conn_str=self.secrets_dict[value],
eventhub_name=names[i], http_proxy=HTTP_PROXY)
except Exception as e:
logging.error(e)
raise e
testing the get instance like below as my intension is to mock the entire DevInstance class obj. both files are in the same module.
#mock.patch("devinstance.DevInstance")
def test_get_instance(self, devins):
# Act
devins.return_value = MagicMock()
result = get_instance()
# Assert
assert result is not None
Can anyone help me how this can be acheived?
You need to patch where the object is being looked up (see the Where to patch documentation).
If you're testing get_instance in main_proc, then you need to patch where DevInstance is imported in main_proc.
For example, if you're importing it with from devinstance import DevInstance, then you need to patch it with #mock.patch("main_proc.DevInstance").
Otherwise, if you're importing it with import devinstance, then you need to patch it with #mock.patch("main_proc.devinstance.DevInstance").
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?
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.
When I patch a method directly using unittest.mock.patch, I can use the mock.assert_called_with() function to correctly assert whether that method was called with certain values. However, when I patch a class directly, any asserts I use on the class itself or any of the class methods come back with "AssertionError: Not Called" even though the class and methods are called.
There are examples in the unittest documentation of successfully patching a class and using assertions: unittest mock doc
Here is my test with the SiteMetaData class methods patched directly and EdgeAPIRoutes class patched:
Test
#patch("repo.routes.EdgeAPIRoutes")
#patch("repo.site_meta_data.SiteMetaData.get_repo_type")
#patch("repo.site_meta_data.SiteMetaData.get_edge_api_inputs")
def test_get_route_list_repo_interface_edge(self, mock_edge_api_inputs,
mock_get_repo_type, mock_edge_api_routes_class):
"""
Test that SiteMetaData.get_route_list is used
appropriately and returns expected values from
mock as a valid response object
"""
from usecase import interface
from usecase import request_objects
from usecase.response_objects import ResponseSuccess
from utils.utils import DatetimeString
all_filters = {"filters" : {"parent_company" : "parent_1",
"site" : "site_1",
"start_date" : DatetimeString("2020-01-01 00:00:00.000"),
"end_date" : DatetimeString("2020-05-01 00:00:00.000")
}
}
example_route_list = [
"route_1",
"route_2",
"route_3"
]
edge_inputs = {
"edge_site" : "edge_site_name",
"edge_key" : "ffff-ffff-ffff"
}
error_response = None
mock_edge_api_inputs.return_value = [
edge_inputs["edge_site"],
edge_inputs["edge_key"],
error_response
]
mock_get_repo_type.return_value = [
"Minestar Edge",
error_response
]
mock_edge_api_routes_class.get_route_list.return_value = [
example_route_list,
error_response
]
resp = interface.get_route_list(request=request_objects.RouteList.request_wfilters(all_filters))
mock_get_repo_type.assert_called_with(all_filters["filters"]["parent_company"],
all_filters["filters"]["site"])
mock_edge_api_inputs.assert_called_with(all_filters["filters"]["parent_company"],
all_filters["filters"]["site"])
mock_edge_api_routes_class.get_route_list.assert_called_with(all_filters["filters"]["start_date"],
all_filters["filters"]["end_date"])
mock_edge_api_routes_class.assert_called_once_with(edge_inputs["edge_site"], edge_inputs["edge_key"])
self.assertTrue(bool(resp))
self.assertEqual(resp.type_, ResponseSuccess.SUCCESS)
self.assertEqual(resp.value["routes"], example_route_list)
Method
def get_route_list(request):
"""
"""
interfacelog.info("running get_route_list")
if bool(request):
repo_type, repo_type_error = site_meta_data.get_repo_type(request.filters["parent_company"],
request.filters["site"])
if repo_type_error is not None:
'''
handle any get repo type system errors
'''
pass
if repo_type == "Minestar Edge":
edge_site, edge_key, edge_error = site_meta_data.get_edge_api_inputs(request.filters["parent_company"],
request.filters["site"])
ear = EdgeAPIRoutes(edge_site, edge_key)
rl, routes_error = ear.get_route_list(request.filters["start_date"],
request.filters["end_date"])
if routes_error is not None:
'''
handle any get routes system error
'''
pass
success_resp = response_objects.ResponseSuccess()
success_resp.value = {"routes" : rl}
return success_resp
else:
'''
handle and errors due to an invalid request
'''
return response_objects.ResponseFailure.build_from_invalid_request_object(request)
Result
The assertions for the SiteMetaData patched methods pass while the assertions for the EdgeAPIRoutes class and methods fail. Based on the documentation, its seems both means of patching should have their assertions passing.
As #MrBeanBremen pointed out in his comment, where the classes are being patched is key. In my question, I was patching them at the source, but since they were imported into my interface.py file, they need to be patched in that location.
Also, since SiteMetaData is instantiated at the global level, its instance needs to be patched rather than the class itself. Since, EdgeAPIRoutes is instantiated within the get_route_list function, the class itself needs to be patched. Then to assign return values to the EdgeAPIRoutes's instance methods, the return_value of the patched class needs to be used:
mock_edge_api_routes_class.return_value.get_route_list.return_value = [
example_route_list,
error_response
]
Since the site_meta_data instance is patched directly, the instance method values can be assigned directly in the patch:
mock_site_meta_data.get_edge_api_inputs.return_value = [
edge_inputs["edge_site"],
edge_inputs["edge_key"],
error_response
]
interface.py
import logging
from repo.site_meta_data import SiteMetaData
from repo.routes import EdgeAPIRoutes
from usecase import response_objects
interfacelog = logging.getLogger(__name__)
site_meta_data = SiteMetaData()
def get_route_list(request):
"""
"""
interfacelog.info("running get_route_list")
if bool(request):
repo_type, repo_type_error = site_meta_data.get_repo_type(request.filters["parent_company"],
request.filters["site"])
if repo_type_error is not None:
'''
handle any get repo type system errors
'''
pass
if repo_type == "Minestar Edge":
edge_site, edge_key, edge_error = site_meta_data.get_edge_api_inputs(request.filters["parent_company"],
request.filters["site"])
ear = EdgeAPIRoutes(edge_site, edge_key)
rl, routes_error = ear.get_route_list(request.filters["start_date"],
request.filters["end_date"])
if routes_error is not None:
'''
handle any get routes system error
'''
pass
success_resp = response_objects.ResponseSuccess()
success_resp.value = {"routes" : rl}
return success_resp
else:
'''
handle and errors due to an invalid request
'''
return response_objects.ResponseFailure.build_from_invalid_request_object(request)
test_interface function patch setup
#patch("usecase.interface.EdgeAPIRoutes")
#patch("usecase.interface.site_meta_data")
def test_get_route_list_repo_interface_edge(self, mock_site_meta_data,
mock_edge_api_routes_class):
"""
Test that SiteMetaData.get_route_list is used
appropriately and returns expected values from
mock as a valid response object
"""
...
I am writing a unittest. How can I patch self.conf in the init method in class MyValidator? In my unittest, I want to create a fake self.conf and get the response to make assertion of each element in self.conf.
class MyValidator(wsgi.Middleware):
def __init__(self, app):
self.app = app
self.conf = {
'auth_uri': CONF.someuri
'admin_domain_name': CONF.somedomainname,
'admin_user': CONF.someuser,
'admin_password': CONF.get_admin_password(),
'domain_name': CONF.somedomainname
}
For unittest, I am thinking to do.. (I know this is wrong.. but you get the idea)
#mock.patch('my_module.MyValidator.__init__.conf')
def setUp(self, mock_config):
#webob.dec.wsgify()
def fake_app(req):
return webob.Response()
self.request = webob.Request.blank('/')
mock_config = {
'auth_uri': 'testuri'
....
....
}
self.middleware = MyValidator(fake_app)
def test_auth_uri(self):
auth_uri = 'testuri'
env_auth_uri = self.request.environ.get('auth_uri', None)
self.assertEqual(auth_uri, env_auth_uri)
What should be done to patch self.conf to get intended response?
Even I'm using mocking and patching extensively I don't think your case need it. conf is a MyValidator's public attribute and you don't need anything more than change it as you need.
def setUp(self):
#webob.dec.wsgify()
def fake_app(req):
return webob.Response()
self.request = webob.Request.blank('/')
self.middleware = MyValidator(fake_app)
self.middleware.conf = {
'auth_uri': 'testuri'
....
....
}
In this case patch cannot give to you nothing more because you are not interested about dict access and some smaller context where changes are made. If in some other test you need some other values you can use either self.middleware.conf.update(...) or self.middleware.conf[...]=... without change the behavior of other tests because setUp() configure it in the same way for every tests.
Things become different if conf is a read only property of MyValidator (a little bit better design). In this case you need to patch it to make your test:
class MyValidator(wsgi.Middleware):
def __init__(self, app):
self.app = app
#property
def conf(self):
return {
'auth_uri': CONF.someuri
'admin_domain_name': CONF.somedomainname,
'admin_user': CONF.someuser,
'admin_password': CONF.get_admin_password(),
'domain_name': CONF.somedomainname
}
Where the test class should be
#mock.patch('my_module.MyValidator.conf', new_callable=PropertyMock)
def setUp(self, mock_conf):
#webob.dec.wsgify()
def fake_app(req):
return webob.Response()
self.request = webob.Request.blank('/')
mock_conf.return_value = {
'auth_uri': 'testuri'
....
....
}
self.middleware = MyValidator(fake_app)
def test_auth_uri(self):
auth_uri = 'testuri'
env_auth_uri = self.request.environ.get('auth_uri', None)
self.assertEqual(auth_uri, env_auth_uri)
Patch __init__() is useful in rare cases. For instance when __init__ do some work and need resources that cannot be used or will not be used in test environment. An example of it is when init try to access to databases, use network resources, start new thread and so on.