# Testing tips & examples ## General philosophy https://pythontesting.net/strategy/given-when-then-2/ ## Unit testing API client code ### Mocking and testing "wrapper" code for an API client Let's say we're using a Python package that provides client functionality for the [**Workflow Execution Service**]() API. This library includes the `WESClient` class that we can instantiate and use to interact with a WES server. However, if we have code that *wraps* and does something with the `WESClient` object, we want to be able to test just that added functionality (not the behavior of the `WESClient` class itself). In this [example](https://github.com/Sage-Bionetworks/workflow-interop/blob/develop/wfinterop/wes/client.py#L62-L75), the wrapping method is actually part of an "adapter" class for the WES API; the `WESAdapter` is designed to pass and translate inputs/outputs to and from the `WESClient`, but through a more standard interface. So, we have a method called `GetServiceInfo()` that wraps `get_service_info()` from our client library. How do we test that `GetServiceInfo()` as behaving correctly? ```python class WESAdapter(WESInterface): """ Adapter class for the WES client functionality from the workflow-service library. :param wes_client: """ _wes_client = None def __init__(self, wes_client): self._wes_client = wes_client def GetServiceInfo(self): return self._wes_client.get_service_info() ``` Here is the [test code](https://github.com/Sage-Bionetworks/workflow-interop/blob/develop/tests/test_wes.py#L62-L72) for the method. We instanstiate a `WESAdapter` object (`wes_adapter`), make an example request, and verify that the underlying `get_service_info()` method is called with the expected arguments. ```python    def test_GetServiceInfo(self, mock_client_lib): mock_response = {} mock_client_lib.get_service_info.return_value = mock_response wes_adapter = WESAdapter(wes_client=mock_client_lib) test_args = {arg: '' for arg in inspect.getargspec(WESClient.get_service_info)[0][1:]} test_response = wes_adapter.GetServiceInfo() mock_client_lib.get_service_info.assert_called_once_with(**test_args) assert test_response == mock_response ``` Some things to note: + We don't care about the output of `get_service_info()` — that's not what we're testing here. It's not obvious here, but the `mock_client_lib` variable is a `mock.Mock` object. This object includes the `return_value()` setter, which allows us to pre-define the output for any given method. In this case, we'll just set the response to be an empty dict. + Rather than manually defining test values for all the arguments to `get_service_info()`, we're a list comprehension to automatically assign a blank string `''` for any arguments captured by `inspect.getargspec()` — in hindsight, this might be problematic if any of the arguments expected non-string types... + But where does `mock_client_lib` actually come from? We'll get to that in a minute. First, a brief digression on naming. This is totally a matter of personal preference, but I tend to use the `mock_` prefix to denote items that are part of my *state* (or "environment") for the test. I use `test_` to indicate the inputs and outputs of the test itself. Using the Given-When-Then structure from the article linked above, the test would look something like this: ```python    def test_GetServiceInfo(self, mock_client_lib): # GIVEN a mock client library and a dummy response mock_response = {} mock_client_lib.get_service_info.return_value = mock_response # WHEN a WESAdapter object is initialized to wrap the client library # (I maybe should have named this 'test_wes_adapter'...) wes_adapter = WESAdapter(wes_client=mock_client_lib) # AND the GetServiceInfo method of the object is called with a # known set of arguments test_args = {arg: '' for arg in inspect.getargspec(WESClient.get_service_info)[0][1:]} test_response = wes_adapter.GetServiceInfo() # THEN the client library should have been called with the expected # arguments mock_client_lib.get_service_info.assert_called_once_with(**test_args) # AND the response should match the expected dummy response assert test_response == mock_response ``` It's not a perfect naming convention by any means, so no need to be overly strict about it — but it can help to keep track of things. Back to `mock_client_lib`... For convenience and reuse across our tests, we can define a "fixture"[example](https://github.com/Sage-Bionetworks/workflow-interop/blob/develop/tests/conftest.py#L134-L139) ```python @pytest.fixture() def mock_client_lib(request): mock_wes_client = mock.Mock(name='mock WESClient') with mock.patch('wes_client.util.WESClient', autospec=True, spec_set=True): yield mock_wes_client ``` Mocking API responses: [example](https://github.com/Sage-Bionetworks/workflow-interop/blob/develop/tests/conftest.py#L150-L161) ```python @pytest.fixture(params=[None, 'workflow-service']) def mock_wes_client(request): if request.param is None: mock_api_client = mock.Mock(name='mock SwaggerClient') with mock.patch.object(SwaggerClient, 'from_spec', return_value=mock_api_client): yield mock_api_client else: mock_api_client = mock.Mock(name='mock WESAdapter') with mock.patch('wfinterop.wes.client.WESAdapter', autospec=True): yield mock_api_client ``` [example](https://github.com/Sage-Bionetworks/workflow-interop/blob/develop/tests/test_wes.py#L184-L194) ```python    def test_get_service_info_direct(self, mock_wes_client): mock_service_info = {'workflow_type_versions': ['CWL', 'WDL']} mock_wes_client.GetServiceInfo.return_value = mock_service_info wes_instance = WES(wes_id='mock_wes', api_client=mock_wes_client) test_service_info = wes_instance.get_service_info() assert isinstance(test_service_info, dict) assert test_service_info == mock_service_info ``` Testing business logic & monkeypatching ```python def test_run_submission(mock_submission, mock_run_log, mock_wes, monkeypatch): monkeypatch.setattr('wfinterop.orchestrator.get_submission_bundle', lambda x,y: mock_submission['mock_sub']) monkeypatch.setattr('wfinterop.orchestrator.update_submission', lambda w,x,y,z: None) monkeypatch.setattr('wfinterop.orchestrator.run_job', lambda **kwargs: mock_run_log) test_run_log = run_submission(queue_id='mock_queue', submission_id='mock_sub') assert test_run_log == mock_run_log ```