Python Testing: Mocking Functions based on Input Arguments
Posted by Aly Sivji in Quick Hits
In Python, functions are objects. This means we can return them from other functions.
In this Quick Hit, we will use this property of functions to mock out an external API with fake data that can be used to test our internal application logic.
Note: I previously used Python functions to simulate the behavior of a case statement.
Imagine we have an external API we can use to download activity tracking data.
This API, a simple wrapper around requests, has two endpoints: /user/
and /activity/
. Both take the parameter {'user': user_id}
.
We want to write a program that will produce a motivational message that tells the user how many miles they ran. This is fairly straightforward:
# calc_stats.py
import fitness_api
def motivation_message(person_id):
user = fitness_api.get('/user/', params={'user': person_id})
name = user[0].get('name')
activities = fitness_api.get('/activity/', params={'user': person_id})
total_distance = 0
for activity in activities:
total_distance += activity.get('distance')
return f'{name} has run {total_distance} miles'
How can we test this code? It has an external dependency on fitness_api
.
We'll follow standing testing procedure and mock the external API with test data. We can now compare the result of the function with what we expect our function to return.
Wait a minute, fitness_api.get()
returns different values based on input parameters. This makes things a bit more complicated. How can we write a mock that returns different values?
We'll answer this question by exploring functions in a bit more depth:
# Let's create a function that takes every argument
def foo(*args, **kwargs):
return (args, kwargs)
foo('a', 'b')
foo('a', b=2, c='test')
# Create a mock to return this function. Let's see what happens
from unittest.mock import MagicMock
my_mock = MagicMock(name='main', return_value=foo)
my_mock
func_from_mock = my_mock()
func_from_mock
func_from_mock(5,5)
func_from_mock(5, 8, param=5)
This is great! We can read positional and keyword arguments inside of our function.
Lets create a function that returns test data given input arguments. We'll wrap this function in our mock which we then use to replace the external API. This technique is referred to as monkeypatching.
Aren't dynamic languages great?!
# test_calc_stats.py
import calc_stats
user_data = [
{ 'id': 1, 'name': 'Aly', 'email': 'alysivji@gmail.com'},
]
activity_data = [
{ 'id': 65, 'description': 'morning jog', 'distance': 3.1 },
{ 'id': 66, 'description': 'lunch break', 'distance': 1.2 },
{ 'id': 67, 'description': 'weekend long run', 'distance': 6.2 },
{ 'id': 68, 'description': 'night run', 'distance': 2.5 },
]
def load_data(endpoint, *args, **kwargs):
if 'user' in endpoint:
return user_data
elif 'activity' in endpoint:
return activity_data
def test_motivation_message(mocker):
# Arrange
mock_api = mocker.MagicMock(name='api')
mock_api.get.side_effect = load_data
mocker.patch('calc_stats.fitness_api', new=mock_api)
# Act
result = calc_stats.motivation_message('alysivji@gmail.com')
# Assert
assert result == f'Aly has run {3.1 + 1.2 + 6.2 + 2.5} miles'
Note: I'm using pytest with the pytest-mock extension.
Running our tests:
$ pytest
================================= test session starts ==================================
platform darwin -- Python 3.6.2, pytest-3.3.1, py-1.5.2, pluggy-0.6.0
rootdir: /Users/alysivji/Documents/siv-dev/projects/blog-notebooks/quick-hits, inifile:
plugins: mock-1.6.3, cov-2.5.1
collected 1 item
test_calc_stats.py . [100%]
=============================== 1 passed in 0.01 seconds ===============================
🙌 🙌 🙌
We can use this pattern to add tests to programs which use requests to pull data from an external API.
Comments