Unit testing has a feature called mocks. Mocking allows a test to replace a piece of code with another piece of code so the results are deterministic. For example, a database should be up 100% of the time but there may be network problems preventing the database from being reached. Mocks can be used to fake the response from the database so tests can be run.
Meraki uses HTTP response code 429 to tell the client their rate limit has been reached. There has been a lot of demand for my Ansible Meraki modules to support the rate limiter and intelligently handle the situation instead of aggressively retrying. Unfortunately (or fortunately depending on your perspective), it’s relatively hard to send enough requests to Meraki to reach the rate limiter unless you’re using asynchronous communication or have multiple systems requesting at the same time. To develop the rate limit handling functions, I developed a unit test to return a 429 response code.
Constructing basic unit tests using assert
isn’t very hard. Constructing unit tests using mocks can be a little wild if you haven’t used them before. The Meraki module utility has a request()
method which calls Ansible’s fetch_url()
function. request
takes two parameters we care about in this situation - URL and HTTP method. Really, it’s a path to an endpoint and not a URL, but that doesn’t matter for the sake of this explanation. We’ll pretend it’s a full URL. These values are passed to fetch_url
which makes the HTTP request and returns a tuple with the response and some information about the request. Because 429 is an error code and not a successful request, there is no real body and instead is information about the error.
Read tutorials explaining mocks and you will see a recurring statement:
Mock where use it and not where its from.
What does this mean exactly? It took months of on and off experimentation to understand. The fetch_url
function is imported to the module utility using from ansible.module_utils.urls import fetch_url
and the Meraki module utility (MerakiModule
) is imported using from ansible.module_utils.network.network.meraki import MerakiModule
. pytest
needs to not only what function to mock but also where it’s being used so it doesn’t mock it everywhere. This means the unit test needs to mock ansible.module_utils.network.network.meraki.fetch_url
.
Now that is established, lets write a unit test to test for a 404 error. First, an actual test is needed:
def test_request_404(module):
response = module.request("/404", method='GET')
assert module.status == 404
By itself this would work but requires an API key and network access. This is where mocks come in. First, we need to pass a mocker
object into the test function and then use that to “patch” the request.
def test_request_404(module, mocker): mocker.patch('ansible.module_utils.network.meraki.meraki.fetch_url')
response = module.request("/404", method='GET')
assert module.status == 404
When this test runs, Python will replace the regular fetch_url()
function when it is called at ansible.module_utils.network.meraki.meraki
with something else. What is that something else?
def request_404_response():
info = {'status': 404,
'msg': '404 - Page is missing',
'url': 'https://api.meraki.com/api/v0/404',
}
info['body'] = '404'
return (None, info)
Remember, the method only returns a tuple because that’s what fetch_url
returns. This function is referenced as a “side effect”. To set a side effect, add it to the patch()
method parameters. mocker.patch('ansible.module_utils.network.meraki.meraki.fetch_url', side_effect=request_404_response)
. Notice the function is referenced as an object, not as a function call with parenthesis after it.
Generally, this is how you can mock an HTTP call in your unit test. If you’re using the requests library, it would be the same concept, just with a little different structure. In my case however, a 404 error code causes the module execution to fail using the fail_json()
method. fail_json()
aborts Ansible execution and returns a non-0 response code, which pytest
takes as a unit test failure. How do we fix this problem? We know how…a mock. After we patch
fetch_url()
we do the same thing to fetch_url()
.
mocker.patch('ansible.module_utils.network.meraki.meraki.MerakiModule.fail_json', side_effect=mocked_fail_json)
mocked_fail_json()
is as simple as can be.
def mocked_fail_json(*args, **kwargs):
pass
Conclusion
My unit tests aren’t merged so I haven’t been able to test rate limit handling. However, this was a good exercise to teach me how to mock objects.