How To Return Multiple Values From Pytest Mock (Practical Guide)
Testing plays a pivotal role in ensuring code reliability and stability.
However, what about when your code relies on external services, APIs, databases or intricate libraries?
How do you easily test those features in a contained environment?
This is where mocking comes into play. It gives you the ability to simulate different scenarios, ensuring your code works as expected.
Now what if you wanted to simulate multiple values being returned by your mock object? Maybe different API endpoints, responses, variables or datasets?
Do you have to create a new mock object for each value?
Returning multiple values from a mock is a specific technique that allows you to simulate diverse outcomes of the mocked dependency, thus thoroughly testing how our code handles different responses.
This guide will walk you through the intricacies of achieving it with Python’s Mock library.
We’ll discuss how to return single and multiple values using different methods and adding a dynamic aspect to it.
Not only this, but we will delve further into side_effect
method, how it is used, and its side effect on your code. (pun intended!)
Let’s get started, then!
What You’ll Learn
By the end of this tutorial, you will
- Understand the importance of return values in mocking and how they ensure accurate code interactions.
- Learn how to set up a basic Mock with a single return value and use the
return_value
attribute - Uncover the limitations of single return value mocking and how to overcome them using the
side_effect
attribute - Explore how to combine
return_value
andside_effect
for handling multiple return values effectively. - Gain insight on some tips and tricks to consider while returning multiple values while mocking.
What is Mocking?
Mocking involves replacing the actual implementation of a function or method with a simulated version.
So, simply put, returning a mock value means specifying what the function should return during a test, regardless of its actual behavior.
It allows you to isolate the code you’re testing and focus on specific scenarios without being dependent on the real behavior of other components.
You can explore the world of Mocking further through this guide.
Why Return Multiple Values From A Mock?
Mocking a function or module and returning a value is good enough. But what if you wanted to simulate something more?
What if you wanted to simulate multiple values being returned by your mock object? Maybe throw some exceptions or simulate different API responses?
Returning multiple values from your mock is the way to go. Why?
Here are a few reasons:
Enhanced Test Reliability:
Returning multiple values is instrumental in testing the robustness of your code under various conditions.
This not only fosters the reliability of your test cases but also establishes a clear contract between different parts of your code, ensuring effective communication.
Handling Errors and Exceptional Cases:
Critical to testing is assessing how your code handles errors or exceptional cases.
Simulating error responses or exceptional conditions, enable you to validate your code’s correct response to adverse situations.
Testing Multiple Stages or States:
Return values become particularly beneficial when a function or method involves multiple stages or states. This capability allows you to simulate different responses at each stage, proving valuable in scenarios like e-commerce websites, response forms, and sales funnels.
Practical Use Cases:
Consider a scenario where a function expects multiple return values from a dependency.
Mocking these returns is a practical approach to covering different test scenarios, ensuring your code adeptly handles various responses.
Imagine developing an e-commerce application, testing a function that calculates the total price of a user’s shopping cart. This function relies on an external service to fetch current item prices. Alternatively, you may need to call an external API to calculate shipping costs based on the user’s location.
Mocking various return values allows you to test the total price function for different dependencies, irrespective of the actual pricing service’s status or response.
This approach ensures your code gracefully handles different prices and shipping costs, providing a reliable and predictable test scenario.
There are other ways to generate test data with Pytest Parameterization and libraries like Hypothesis, but in this article we’ll focus on mocking.
Role of side effect
in Mocking
Before you dive further in the article, let’s get you familiar with the side_effect
function.
A side_effect
mock function is a way to simulate external interactions, exceptions, or dynamic behavior during the execution of a mocked function.
Let’s just say that it allows you to go beyond a simple return value and introduce more complex effects.
It’s like telling the function, “Hey, when you’re called, in addition to returning a value, also do something special.”
Look at this boilerplate code to understand the syntax of side_effect
function:1
2
3
4
5
6
7
8
9import pytest
def test_mock_with_side_effect(mocker):
m = mocker.Mock()
m.side_effect = ['Pytest', 'with', 'Eric']
assert m() == 'Pytest'
assert m() == 'with'
assert m() == 'Eric'
In this example, we use the mocker.Mock
fixture provided by Pytest to create a mock object.
The side_effect
attribute is then set to a list of values, and each time the mock is called, it returns the next value in the list.
The side effect mocks provide a controlled environment for testing various scenarios without relying on the actual external dependencies.
Difference between side_effect
and return_value
return_value
in a mock sets the value that the mocked function will return. Pretty straightforward.
On the other hand, side_effect
allows you to define a function or an exception to be raised when the mock is called. It provides more flexibility, enabling dynamic behavior based on the input or other conditions.
Consider a scenario where you have a function that interacts with an external API to fetch weather data. Let’s mock this interaction using both return_value
and side_effect
.1
2
3
4def test_weather_with_return_value():
with patch('requests.get') as mock_get:
mock_get.return_value.json.return_value = {'temperature': 25}
assert get_weather_data('NewYork') == 25
In this example, the return_value is straightforward. We set the JSON response to {'temperature': 25}
, and the test asserts that the function returns 25.1
2
3
4
5
6
7
8def test_weather_with_side_effect():
with patch('requests.get') as mock_get:
mock_get.return_value.json.side_effect = [{'temperature': 20}, {'temperature': 25}, {'temperature': 30}]
# First call returns 20, second call returns 25, and third call returns 30
assert get_weather_data('NewYork') == 20
assert get_weather_data('NewYork') == 25
assert get_weather_data('NewYork') == 30
In this case, side_effect
is used to simulate different temperatures on each call. The first call returns 20, the second returns 25, and the third returns 30. This allows you to test how your code handles varying responses from the external API.
Project Set Up
Getting Started
Let’s prepare an example project to run some of the code locally:
Simple repo with a couple of test files explaining the key concepts.
Prerequisites
To achieve the above objectives, the following is recommended:
- Basic knowledge of the Python programming language
- Basics of Pytest Unit Testing
To get started, clone the repo here.
Single Return Value Mocking
The Pytest-mock plugin extends Pytest’s capabilities to include mocking (built off unittest.mock
), making it easier to isolate code units and verify their interactions.
If you want to learn more about Pytest mocking, then give this guide a read.
To use the pytest-mock
plugin, you need to ensure it is installed in your Python environment. You can install it using pip:1
pip install pytest-mock
To create a mock object with a single return value, use the mocker fixture provided by pytest mock
directly in the tests. This sets the behavior of the mock object when it is called.1
2
3
4
5
6
7
8
9
10
11
12import pytest
# Test using the mocker fixture for mocking
def test_mock_obj_with_mocker(mocker):
# Create a mock object with a single return value using mocker
mock_obj = mocker.Mock(return_value=42)
# Now, you can use mock_obj just like a function or method
result = mock_obj()
# Assert that the result is equal to the mock return value
assert result == 42
The return_value
attribute of a mock object allows you to specify what the mock should return when it is called.
This attribute simplifies the process of defining the behavior of your mock, especially when you only need a single return value.
Now, why would this be useful? Let’s look at a simple example.
You can mock database queries to ensure the code correctly processes different query results and handles database errors. Without the need to actually connect to the database.
Also, you can handle user authentication responses like a pro to ensure proper handling of valid and invalid credentials.
Moreover, you can easily assert time-related functions to simulate different time scenarios, such as current time, timeouts, or time delays in your coding ventures.
Multiple Return Values with ‘side_effect’
Consider a scenario where you want to test a function under various conditions, each requiring a different return value. The simplest example can be to check whether an input number is positive, negative or zero.
With a single return value, this can become a cumbersome process. This is where the limitations of single return value mocking become apparent.
Enter the hero of our story – the side_effect
attribute. This powerful feature in mocking frameworks provides a solution to the constraints of single-return value mocking.
Instead of being confined to returning a single value, side_effect
allows you to define a function or iterable that produces a sequence of return values.
side_effect
also allows you to define exceptions to be raised when the mock is called.
We covered how to raise exceptions from Mocked functions in detail in this guide.
Return Different Values of Mock based on Arguments
Now, let’s look at a sample code for a scenario when you want to simulate various conditions or inputs and observe how your code responds.
Imagine a banking system, where the interest rate on an amount depends on the type of bank account the customer has - a saving or current account.
src/interest_calculator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def define_interest_rate(account_type):
"""
Defines interest rate based on account type
:param account_type: Account type
:return: Interest rate
"""
if account_type == "saving":
return 0.02
elif account_type == "current":
return 0.01
else:
return 0.005
def calculate_interest(account_type, balance):
"""
Calculate interest based on account type and balance
:param account_type: Account type
:param balance: Balance
:return: Interest
"""
rate = define_interest_rate(account_type)
return balance * rate
Our source code is a simple calculate_interest
function that calculates interest based on the account_type
and balance
arguments.
We have another function define_interest_rate
that defines interest rates based on the type of account.
Now let’s look at how to test the code.
tests/test_pytest_mock_side_effect.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from src.interest_calculator import calculate_interest, define_interest_rate
def test_return_different_interest_rates_based_on_account_type(mocker):
mocker.patch(
"src.interest_calculator.define_interest_rate",
side_effect=lambda account_type: {
"saving": 0.02,
"current": 0.01,
}.get(account_type, 0.005),
)
result_saving = calculate_interest("saving", 1000)
result_current = calculate_interest("current", 1000)
result_default = calculate_interest("default", 1000)
assert result_saving == 20.0
assert result_current == 10.0
assert result_default == 5.0
Let’s break down the test to understand how it works.
First we patch the define_interest_rate
function. We use the side_effect
parameter to define a map of account_type: interest_rate
. The lambda function uses a default value of 0.5%
if the account_type
is not saving
or current
.
We then assert the mocked values against expectation.
This example leverages the side_effect
parameter to produce an iterable mocked response.1
pytest -v -s
As you can see, the test case passes and we have handled the exception as well by setting a default value for our interest rate calculator.
Fallback behavior when ‘side_effect’ is exhausted
Understanding how the side_effect
behaves when it is exhausted is crucial for writing robust tests with mock objects, ensuring that your test cases handle potential exceptions or cyclic behaviours appropriately.
Exhaustion in the context of side_effect
refers to the scenario where all the elements in the iterable (e.g., a list or tuple) used for side_effect
have been consumed or not.
If the side_effect
is an iterable (e.g. a list or tuple), the mocked object will keep returning values from the iterable depending on whether the iterable is exhausted or not.
If the iterable is not exhausted, the mocked object will return values from the beginning of the iterable in a cyclic manner.
If the iterable is exhausted (all elements are used), further calls to the mocked object will raise a StopIteration
exception.
If the side_effect
is a callable (function), it will be called with the same arguments as the mock. If the function raises StopIteration
, it signals that the side_effect
is exhausted.
The solution to avoid exhaustion is to implement fallback behaviour. For example, you can raise a custom exception or return a default value.
Exhaution during mocking is a very detailed topic and let’s not overwhelm you with too much information.
Just to give you a hint of how to avoid exhaustion, let’s see a small code using iterable.
tests/test_pytest_side_effect_exhaustion.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import pytest
def test_side_effect_exhaustion(mocker):
# Create a MagicMock with side_effect as an iterable
mock_iterable = mocker.MagicMock()
mock_iterable.side_effect = [1, 2, 3]
# Consume all elements from the iterable
assert mock_iterable() == 1
assert mock_iterable() == 2
assert mock_iterable() == 3
# Further calls will raise StopIteration
with pytest.raises(StopIteration):
mock_iterable()
Running the test,1
pytest -v -s
Let’s discuss how the test case passed.
The test_side_effect_exhaustion
checks the behavior when the side_effect
is an iterable. As you can see mock_iterable
is created with side_effect set to [1, 2, 3]
. It calls the mock three times, consuming all elements from the iterable.
The fourth call is expected to raise StopIteration
since the iterable is exhausted and we have handled the error accordingly.
This is how you can handle exhaustion in your code and utilize side_effect
to its full potential.
Best Practices and Tips
Let’s unwrap some tips and tricks you need to follow while using mocking to return multiple values.
- Mocks should be clear, concise, and easy to understand. Avoid unnecessary complexity in mock setups.
- Use the
return_value
attribute to set the return value of a mock object andside_effect
to define a function or iterable that produces a sequence of return values or exceptions to be raised when the mock is called. - Too much mocking can be very hard to track and lead to unpredictable behaviour. Some good practices include using a fixture for mocking and controlling its life with the scope.
- One way to achieve a clean testing environment is to use Pytest Fixtures to manage Setup and Teardown.
- Using fixtures for mocking allows better control over the lifecycle of mocks. By controlling the scope, you ensure that mocks are appropriately isolated and don’t interfere with other parts of your test suite.
- Also, it’s important to handle exceptions for testing error handling, validating exception propagation, or ensuring that exceptions do not break your system.
Let’s consider a scenario where a Python application interacts with a database to retrieve user information. In this case, we want to test how the application behaves when the database connection is successful and when it fails.
But it’s best to avoid connecting to the real database and mock the database connection using Fixtures.
Without mocks, we run the risk that the changes made in one test might affect the behaviour of subsequent tests, and the worst-case scenario is that the changes are permanently reflected in our database.
It will be a real dilemma to undo the changes if you don’t even know when and where the changes were implemented!
Conclusion and Learnings
OK, that’s a wrap.
You have learned how to simulate complex scenarios, control behaviour, and manage return values to enhance the robustness and reliability of your test suites.
Mocking allows you to test specific components of your code in isolation, ensuring that you focus on the behaviour of the unit under examination without interference from external dependencies.
Then, we looked at how using multiple return values with side_effect
provides a more expressive way to simulate diverse scenarios within a single test case, making your tests concise and readable.
Mocking is a vast topic with numerous possibilities. Encourage yourself to explore advanced mocking techniques, such as side effect customization, argument matching, and more.
If you have ideas for improvement or would like me to cover anything specific, please send me a message via Twitter, GitHub or Email.
Till the next time… Cheers!
Additional Learning
How to Handle Multiple Returns with multiple arguments
Official Docs - unittest.mock — mock object library
Introduction to Pytest Mocking - What It Is and Why You Need It
How To Easily Resolve The “Fixture ‘Mocker’ Not Found” Error in Pytest
How To Test Raised Exceptions with Pytest MagicMock? (Advanced Guide)
3 Ways To Easily Test Your Python Async Functions With Pytest Asyncio
What is Setup and Teardown in Pytest? (Importance of a Clean Test Environment)
What Are Pytest Fixture Scopes? (How To Choose The Best Scope For Your Test)
The Ultimate Guide To Using Pytest Monkeypatch with 2 Code Examples