Siv Scripts

Solving Problems Using Code

Sun 24 December 2017

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:

In [1]:
# Let's create a function that takes every argument
def foo(*args, **kwargs):
    return (args, kwargs)
In [2]:
foo('a', 'b')
Out[2]:
(('a', 'b'), {})
In [3]:
foo('a', b=2, c='test')
Out[3]:
(('a',), {'b': 2, 'c': 'test'})
In [4]:
# Create a mock to return this function. Let's see what happens
from unittest.mock import MagicMock
In [5]:
my_mock = MagicMock(name='main', return_value=foo)
In [6]:
my_mock
Out[6]:
<MagicMock name='main' id='4408396264'>
In [7]:
func_from_mock = my_mock()
In [8]:
func_from_mock
Out[8]:
<function __main__.foo>
In [9]:
func_from_mock(5,5)
Out[9]:
((5, 5), {})
In [10]:
func_from_mock(5, 8, param=5)
Out[10]:
((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