Python Testing: Mocking Functions based on Input Arguments
source link: https://alysivji.github.io/mocking-functions-inputs-args.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Siv Scripts
Solving Problems Using Code
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')
(('a', 'b'), {})
foo('a', b=2, c='test')
(('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
<MagicMock name='main' id='4408396264'>
func_from_mock = my_mock()
func_from_mock
<function __main__.foo>
func_from_mock(5,5)
((5, 5), {})
func_from_mock(5, 8, param=5)
((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': '[email protected]'}, ] 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('[email protected]') # 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.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK