Mocking Network Calls
Mocking data in unit tests will allow for validating code logic, and to quickly replicate and diagnose issues involving unexpected data values. In the SDK, there are a couple of tools available that allow for mocking network requests and for building expected return objects.
Mocking q2_request calls
In order to mock q2_request calls in a unit test, the SDK provides a mock_q2_request
decorator that can be added to
a unit test function.
In the example code, there are three q2_request
calls. This will require two different mock_q2_request
declarations
on the unit test function. The first decorator is for the post
calls, and a list of two payloads is given as the first
parameter value. The list index values used as the mock response correspond to the call number for that particular
request call type (request verb: ie. post, put, get). The first list item will be used for the first post call, and the
second list item will be used for the second post call. To better illustrate this, the example extension code does a
post, then a put, followed by a second post call. If the test were to fail, the log lines are captured and shown in the
CLI. The below snippets shows the order of the calls and their output.
"""
testing_example Extension
"""
from q2_sdk.core.http_handlers.tecton_server_handler import Q2TectonServerRequestHandler
from q2_sdk.core import q2_requests
from .install.db_plan import DbPlan
class TestingExampleHandler(Q2TectonServerRequestHandler):
DB_PLAN = DbPlan()
FRIENDLY_NAME = 'testing_example' # this will be used for end user facing references to this extension like Central and Menu items.
DEFAULT_MENU_ICON = 'landing-page' # this will be the default icon used if extension placed at top level (not used if a child element)
CONFIG_FILE_NAME = 'testing_example' # configuration/testing_example.py file must exist if REQUIRED_CONFIGURATIONS exist
TECTON_URL = 'https://cdn1.onlineaccess1.com/cdn/base/tecton/v1.38.0/q2-tecton-sdk.js'
def __init__(self, application, request, **kwargs):
super().__init__(application, request, **kwargs)
@property
def router(self):
router = super().router
router.update({
'default': self.default,
'submit': self.submit,
# Add new routes here
})
return router
async def default(self):
template = self.get_template('index.html.jinja2', {})
payload1 = await q2_requests.post(self.logger, "http://test.com")
self.logger.info(payload1.json())
payload2 = await q2_requests.put(self.logger, "http://test.com")
if not payload2.ok:
self.logger.error("Request failed")
self.logger.info(payload2.status_code)
self.logger.info(payload2.json())
payload3 = await q2_requests.post(self.logger, "http://test.com")
self.logger.info(payload3.json())
html = self.get_tecton_form(
"testing_example",
custom_template=template,
routing_key="submit",
hide_submit_button=True
)
return html
async def submit(self):
template = self.get_template(
'submit.html.jinja2',
{
'header': "testing_example",
'message': 'Hello World POST: From "testing_example".<br>',
'data': self.form_fields
}
)
html = self.get_tecton_form(
"testing_example",
custom_template=template,
hide_submit_button=True
)
return html
import pytest
from tornado.web import Application
from q2_sdk.tools.testing.decorators import mock_q2_request
from q2_sdk.tools.testing.models import RequestMock
from q2_sdk.ui.forms import Q2Form
from .. import extension
class MockTestingExampleHandler(extension.TestingExampleHandler):
def __init__(self):
super().__init__(Application(), RequestMock(), logging_level='INFO')
# two q2_requests calls in the default extension
@mock_q2_request([{"Key": "payload 1 value"}, {"Key": "payload 3 value"}], verb="post")
@mock_q2_request([{"Key": "payload 2 value"}], verb="put", status_code=404, return_success=False)
async def test_default_route():
handler = MockTestingExampleHandler()
actual = await handler.default()
assert isinstance(actual, Q2Form)
assert actual.routing_key == 'submit'
assert actual.hide_submit_button is True
If the test were to fail, the log lines are captured and shown in the CLI. The below snippets shows the order of the calls and their output.
-> % q2 test
q2_sdk.general INFO testing_example: Frontend dependency hash out of date. Rebuilding all JS packages
$ pytest --asyncio-mode=auto -W ignore::DeprecationWarning --ignore-glob=env --ignore-glob=venv --ignore-glob=**/node_modules --ignore-glob=**/.cache --ignore-glob=**/dist
======================================================================================================================================================================================================== test session starts =========================================================================================================================================================================================================
platform darwin -- Python 3.10.5, pytest-7.1.1, pluggy-1.0.0
plugins: asyncio-0.18.3
asyncio: mode=auto
collected 1 item
testing_example/tests/test_extension.py F [100%]
============================================================================================================================================================================================================== FAILURES ==============================================================================================================================================================================================================
_________________________________________________________________________________________________________________________________________________________________________________________________________ test_default_route _________________________________________________________________________________________________________________________________________________________________________________________________________
@mock_q2_request([{"Key": "payload 1 value"}, {"Key": "payload 3 value"}], verb="post")
@mock_q2_request([{"Key": "payload 2 value"}], verb="put", status_code=404, return_success=False)
async def test_default_route():
handler = MockTestingExampleHandler()
actual = await handler.default()
assert isinstance(actual, Q2Form)
assert actual.routing_key == 'submit'
> assert actual.hide_submit_button is False
E assert True is False
E + where True = <q2_sdk.ui.forms.Q2TectonForm object at 0x1107cae00>.hide_submit_button
testing_example/tests/test_extension.py:26: AssertionError
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Captured log call ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
INFO extension.foo:extension.py:39 {"Key": "payload 1 value"}
WARNING extension.foo:q2_requests.py:174 Response URL: http://test.com
WARNING extension.foo:q2_requests.py:175 Response Headers: {}
WARNING extension.foo:q2_requests.py:176 Response Body: {"Key": "payload 2 value"}
ERROR extension.foo:extension.py:42 Request failed
INFO extension.foo:extension.py:43 404
INFO extension.foo:extension.py:44 {"Key": "payload 2 value"}
INFO extension.foo:extension.py:46 {"Key": "payload 3 value"}
====================================================================================================================================================================================================== short test summary info =======================================================================================================================================================================================================
FAILED testing_example/tests/test_extension.py::test_default_route - assert True is False
==================================================================================================================================================================================================== 1 failed, 1 warning in 0.65s ====================================================================================================================================================================================================
Tests not passed
Mocking DB Object requests
Mocking a call to the platform can be done with a pytest tool called monkeypatch
. This tool will swap out the target
function call with a different function call. In this example, the setattr
function is used to swap the get
method
on the UserData class. The new method that is being called instead of the class method will use the `lxml.objectify
XML builder E
to create an XML shape that the model is expecting. The XML is then handed to the model class TableRow
along with the model representation class, which describes the expected return data shape.
"""
testing_example Extension
"""
from q2_sdk.core.http_handlers.tecton_server_handler import Q2TectonServerRequestHandler
from .install.db_plan import DbPlan
class TestingExampleHandler(Q2TectonServerRequestHandler):
DB_PLAN = DbPlan()
FRIENDLY_NAME = 'testing_example' # this will be used for end user facing references to this extension like Central and Menu items.
DEFAULT_MENU_ICON = 'landing-page' # this will be the default icon used if extension placed at top level (not used if a child element)
CONFIG_FILE_NAME = 'testing_example' # configuration/testing_example.py file must exist if REQUIRED_CONFIGURATIONS exist
TECTON_URL = 'https://cdn1.onlineaccess1.com/cdn/base/tecton/v1.38.0/q2-tecton-sdk.js'
def __init__(self, application, request, **kwargs):
super().__init__(application, request, **kwargs)
@property
def router(self):
router = super().router
router.update({
'default': self.default,
'submit': self.submit,
# Add new routes here
})
return router
async def default(self):
template = self.get_template('index.html.jinja2', {})
html = self.get_tecton_form(
"testing_example",
custom_template=template,
routing_key="submit",
hide_submit_button=False
)
return html
async def submit(self):
user_data = await self.db.user_data.get(self.online_user.user_id, "DemographicUpdate")
self.logger.info(f"Got the user data: {user_data[0].GTDataValue}")
self.logger.info(user_data[0].ShortName)
template = self.get_template(
'submit.html.jinja2',
{
'header': "testing_example",
'message': 'Hello World POST: From "testing_example".<br>',
'data': self.form_fields
}
)
html = self.get_tecton_form(
"testing_example",
custom_template=template,
hide_submit_button=True
)
return html
from lxml.objectify import E
from tornado.web import Application
from q2_sdk.hq.db.user_data import UserDataRow, UserData
from q2_sdk.hq.models.online_user import OnlineUser
from q2_sdk.hq.table_row import TableRow
from q2_sdk.tools.testing.models import RequestMock
from q2_sdk.ui.forms import Q2Form
from .. import extension
class MockTestingExampleHandler(extension.TestingExampleHandler):
def __init__(self):
super().__init__(Application(), RequestMock(), logging_level='INFO')
self.online_user = OnlineUser()
async def get_mock_user_data(*args, **kwargs):
return_xml = [
E.Row(
E.DataID(5),
E.ShortName("DemographicUpdate"),
E.UserID(2),
E.GTDataValue("2022-01-01 00:00:00.000")
)
]
return [
TableRow(lxml_element, UserDataRow)
for lxml_element in return_xml
]
async def test_submit_route(monkeypatch):
handler = MockTestingExampleHandler()
handler.form_fields = {
'foo': 'bar'
}
handler.online_user.user_id = 2
monkeypatch.setattr(UserData, "get", get_mock_user_data)
actual = await handler.submit()
assert isinstance(actual, Q2Form)
assert actual.routing_key == ''
assert actual.hide_submit_button is False
If the test were to fail, the log lines are captured and shown in the CLI. The below snippets shows the order of the calls and their output.
-> % q2 test
q2_sdk.general INFO testing_example: Frontend dependency hash up to date. Skipping rebuild
$ pytest --asyncio-mode=auto -W ignore::DeprecationWarning --ignore-glob=env --ignore-glob=venv --ignore-glob=**/node_modules --ignore-glob=**/.cache --ignore-glob=**/dist
======================================================================================================================================================================================================== test session starts =========================================================================================================================================================================================================
platform darwin -- Python 3.10.5, pytest-7.1.1, pluggy-1.0.0
plugins: asyncio-0.18.3
asyncio: mode=auto
collected 1 item
testing_example/tests/test_extension.py F [100%]
============================================================================================================================================================================================================== FAILURES ==============================================================================================================================================================================================================
_________________________________________________________________________________________________________________________________________________________________________________________________________ test_submit_route __________________________________________________________________________________________________________________________________________________________________________________________________________
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1092b2b90>
async def test_submit_route(monkeypatch):
handler = MockTestingExampleHandler()
handler.form_fields = {
'foo': 'bar'
}
handler.online_user.user_id = 2
monkeypatch.setattr(UserData, "get", get_mock_user_data)
actual = await handler.submit()
assert isinstance(actual, Q2Form)
assert actual.routing_key == ''
> assert actual.hide_submit_button is False
E assert True is False
E + where True = <q2_sdk.ui.forms.Q2TectonForm object at 0x10addff70>.hide_submit_button
testing_example/tests/test_extension.py:59: AssertionError
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Captured log call ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
INFO extension.foo:extension.py:55 Got the user data: 2022-01-01 00:00:00.000
INFO extension.foo:extension.py:56 DemographicUpdate
FAILED testing_example/tests/test_extension.py::test_submit_route - assert True is False
==================================================================================================================================================================================================== 1 failed, 1 warning in 0.72s ====================================================================================================================================================================================================
Tests not passed
Mocking HQ requests
In order to mock HQ calls in a unit test, the pytest library provides a mocking function known as monkeypatch
. In order
to properly mock an HQ request, a function needs to be provided to monkeypatch
that returns an instance of the HqResponse
object.
Note
Newer versions of the SDK have a modified version of MockHqResponse
that provide the mock_execute_function
function. This function can be provided to monkeypatch
to return the response object. If this is not available in
your current version, either upgrade to the latest version, or build your own function that does this.
In the example code, there is a WedgeOnlineBanking account list call. The response object is checked for success before parsing for account ids. The response shape was grabbed from a log when running the extension in the sandbox. Example responses can also be grabbed from the documentation if they are provided.
"""
testing_example Extension
"""
from q2_sdk.core.http_handlers.tecton_server_handler import Q2TectonServerRequestHandler
from q2_sdk.hq.hq_api.wedge_online_banking import GetUserAccountListAndDetails
from .install.db_plan import DbPlan
class TestingExampleHandler(Q2TectonServerRequestHandler):
DB_PLAN = DbPlan()
FRIENDLY_NAME = 'testing_example' # this will be used for end user facing references to this extension like Central and Menu items.
DEFAULT_MENU_ICON = 'landing-page' # this will be the default icon used if extension placed at top level (not used if a child element)
CONFIG_FILE_NAME = 'testing_example' # configuration/testing_example.py file must exist if REQUIRED_CONFIGURATIONS exist
TECTON_URL = 'https://cdn1.onlineaccess1.com/cdn/base/tecton/v1.38.0/q2-tecton-sdk.js'
def __init__(self, application, request, **kwargs):
super().__init__(application, request, **kwargs)
@property
def router(self):
router = super().router
router.update({
'default': self.default,
'submit': self.submit,
# Add new routes here
})
return router
async def default(self):
template = self.get_template('index.html.jinja2', {})
params = GetUserAccountListAndDetails.ParamsObj(
self.logger,
self.hq_credentials,
GetUserAccountListAndDetails.detailType.UseCurrentDetails
)
results = await GetUserAccountListAndDetails.execute(params)
if not results.success:
self.logger.error(f"HQ failure: {results.error_message}")
html = self.get_tecton_form(
"error",
custom_template=self.get_template("error.html.jinja2", {}),
hide_submit_button=True
)
return html
gathered_id_list = []
for account in results.result_node.Data.AccountListResponse.AccountListResponseRecord:
gathered_id_list.append(account.HostAccountID.text)
self.logger.info(f"Gathered account ids: {gathered_id_list}")
html = self.get_tecton_form(
"testing_example",
custom_template=template,
routing_key="submit",
hide_submit_button=False
)
return html
from tornado.web import Application
from q2_sdk.hq.models.online_user import OnlineUser
from q2_sdk.hq.hq_api.wedge_online_banking import GetUserAccountListAndDetails
from q2_sdk.tools.testing.models import RequestMock, HqResponseMock
from q2_sdk.ui.forms import Q2Form
from .. import extension
class MockTestingExampleHandler(extension.TestingExampleHandler):
def __init__(self):
super().__init__(Application(), RequestMock(), logging_level='INFO')
self.online_user = OnlineUser()
def get_mock_account_list():
return """
<Q2API AuditId="3482501" HqAssemblyVersion="4.5.0.0" HqVersion="4.5.0.6073" ServerDateTime="2022-12-13T17:53:28.1557764+00:00">
<Result>
<ErrorCode ErrorType="Success">0</ErrorCode>
<ErrorDescription/>
<HydraErrorReturnCode>0</HydraErrorReturnCode>
</Result>
<Data>
<AccountListResponse>
<AccountListResponseRecord>
<UserID>2</UserID>
<HostAccountID>5007</HostAccountID>
<AccountNumberInternal>S01</AccountNumberInternal>
<ProductTypeName>Q2 Savings</ProductTypeName>
<ProductName>Q2 BUSINESS SAVINGS</ProductName>
<AccountDesc/>
<LinkType>C</LinkType>
<DisplayOrder>0</DisplayOrder>
<Balance1>152841.28</Balance1>
<BalanceDescription1>Available Balance</BalanceDescription1>
<BalanceName1>AvailBal</BalanceName1>
<BalanceType1>Currency</BalanceType1>
<Balance2/>
<BalanceDescription2>Current Balance</BalanceDescription2>
<BalanceName2>CurBal</BalanceName2>
<BalanceType2>Currency</BalanceType2>
<ProductTypeVoiceFile>savings.wav</ProductTypeVoiceFile>
<ProductVoiceFile>sav.wav</ProductVoiceFile>
<HydraProductCode>S</HydraProductCode>
<HydraProductTypeCode>D</HydraProductTypeCode>
<BalanceToDisplay>152841.28</BalanceToDisplay>
<SortValue/>
<Cif>07212010</Cif>
<AccountNumberInternalUnmasked>S01</AccountNumberInternalUnmasked>
<AccountNumberExternalUnmasked>S01</AccountNumberExternalUnmasked>
<CIFInternalUnmasked>07212010</CIFInternalUnmasked>
<CIFExternalUnmasked>07212010</CIFExternalUnmasked>
</AccountListResponseRecord>
<Q2_AccountDataElements>
<HostAccountID>5007</HostAccountID>
<HADE_ID>1</HADE_ID>
<HADEName>AvailBal</HADEName>
<HADEDesc>Available Balance</HADEDesc>
<HADEDataType>Currency</HADEDataType>
<DataValue>152841.28</DataValue>
<DisplayOrder>7</DisplayOrder>
<IsEditable>false</IsEditable>
</Q2_AccountDataElements>
</AccountListResponse>
</Data>
</Q2API>
"""
async def test_default_hq_call(monkeypatch):
handler = MockTestingExampleHandler()
mock_hq = HqResponseMock(success=True, error_message=None, result_node=get_mock_account_list())
monkeypatch.setattr(GetUserAccountListAndDetails, "execute", mock_hq.mock_execute_function)
actual = await handler.default()
assert isinstance(actual, Q2Form)
assert actual.routing_key == 'submit'
assert actual.hide_submit_button is True
If the test were to fail, the log lines are captured and shown in the CLI. The below snippets shows the order of the calls and their output.
-> % q2 test
q2_sdk.general INFO testing_example: Frontend dependency hash up to date. Skipping rebuild
$ pytest --asyncio-mode=auto -W ignore::DeprecationWarning --ignore-glob=env --ignore-glob=venv --ignore-glob=**/node_modules --ignore-glob=**/.cache --ignore-glob=**/dist
======================================================================================================================================================================================================== test session starts =========================================================================================================================================================================================================
platform darwin -- Python 3.10.5, pytest-7.1.1, pluggy-1.0.0
plugins: asyncio-0.18.3
asyncio: mode=auto
collected 1 item
testing_example/tests/test_extension.py F [100%]
============================================================================================================================================================================================================== FAILURES ==============================================================================================================================================================================================================
________________________________________________________________________________________________________________________________________________________________________________________________________ test_default_hq_call ________________________________________________________________________________________________________________________________________________________________________________________________________
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x113afd0f0>
async def test_default_hq_call(monkeypatch):
handler = MockTestingExampleHandler()
mock_hq = HqResponseMock(success=True, error_message=None, result_node=get_mock_account_list())
monkeypatch.setattr(GetUserAccountListAndDetails, "execute", mock_hq.mock_execute_function)
actual = await handler.default()
assert isinstance(actual, Q2Form)
assert actual.routing_key == 'submit'
> assert actual.hide_submit_button is True
E assert False is True
E + where False = <q2_sdk.ui.forms.Q2TectonForm object at 0x115684d90>.hide_submit_button
testing_example/tests/test_extension.py:80: AssertionError
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Captured log call ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
INFO extension.foo:extension.py:58 Gathered account ids: ['5007']
====================================================================================================================================================================================================== short test summary info =======================================================================================================================================================================================================
FAILED testing_example/tests/test_extension.py::test_default_hq_call - assert False is True
==================================================================================================================================================================================================== 1 failed, 1 warning in 0.58s ====================================================================================================================================================================================================
Tests not passed