Skip to main content

Python mocking: advanced side_effect and spec usage

· 8 min read
Serhii Hrekov
software engineer, creator, artist, programmer, projects founder

side_effect and spec are powerful but often underutilized features of Python's unittest.mock library. Mastering them is key to writing tests that are both robust and expressive. This article will dive deep into these advanced concepts, showing you how to simulate complex scenarios and prevent common mocking pitfalls.

Using side_effect to Simulate Successive Calls

The side_effect attribute allows you to configure a mock to do something more than just return a single value. It can be a function, an exception, or, most powerfully, an iterable (like a list or a tuple) that provides different return values for successive calls to the mock. This is essential for testing functions that make multiple, stateful calls to a dependency [1].

Example 1: Simulating Successive API Calls

Imagine you're testing a function that polls an API until it receives a successful response. The API might return a "pending" status multiple times before finally returning "completed."

order_tracker.py

import requests

def track_order_status(order_id):
"""Polls an API for order status until it's 'completed'."""
status = 'pending'
while status != 'completed':
response = requests.get(f'https://api.example.com/order/{order_id}')
status = response.json().get('status')
return status

Here's how you can use side_effect to mock this behavior.

test_order_tracker.py

import unittest
from unittest.mock import patch, Mock
import requests
from order_tracker import track_order_status

class TestOrderTracker(unittest.TestCase):
@patch('order_tracker.requests.get')
def test_tracking_multiple_calls_with_side_effect(self, mock_get):
# Configure the mock to return a different response on each call
mock_get.side_effect = [
Mock(json=lambda: {'status': 'pending'}),
Mock(json=lambda: {'status': 'processing'}),
Mock(json=lambda: {'status': 'completed'})
]

status = track_order_status(123)

# We can verify the mock was called the correct number of times
self.assertEqual(mock_get.call_count, 3)
self.assertEqual(status, 'completed')

By setting side_effect to a list of mocks, we tell mock_get to return the first mock on the first call, the second on the second call, and so on. This allows us to simulate the exact behavior of a real, asynchronous API.

Example 2: Simulating Exceptions

side_effect can also be used to raise exceptions, which is crucial for testing error handling logic.

user_data.py

import requests

def get_user_profile(user_id):
"""Fetches user data and handles network errors."""
try:
response = requests.get(f'https://api.example.com/users/{user_id}')
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Network error: {e}")
return None

We can use side_effect to raise a RequestException.

test_user_data.py

import unittest
from unittest.mock import patch
import requests
from user_data import get_user_profile

class TestUserData(unittest.TestCase):
@patch('user_data.requests.get')
def test_get_user_profile_network_error(self, mock_get):
# The mock will now raise this exception when called
mock_get.side_effect = requests.exceptions.RequestException('Simulated Network Error')

user_profile = get_user_profile(1)

# We assert that the function correctly handled the exception
self.assertIsNone(user_profile)

Using side_effect for exceptions is a cleaner and more direct way to test error handling than manually raising exceptions in your test code.


The Power of spec and autospec

While side_effect is about behavior, spec and autospec are about enforcing an interface. By default, mocks are highly permissive. You can call any method or access any attribute on them, and they won't complain. This is a common pitfall because it can hide bugs, as a test will pass even if your code tries to call a method that doesn't exist on the real object [2].

  • spec=True: This enforces a specification on your mock. It ensures that the mock can only accept the methods and attributes that the real object possesses. If your code tries to call a non-existent method, the mock will raise an AttributeError.
  • autospec=True: This is the most powerful option. It automatically creates a mock with the exact same signature (methods and attributes) as the object it's replacing. It also inspects a function's arguments, ensuring that your test code calls the mock with the correct number and type of arguments [2, 3].

Example 3: Enforcing Mock Discipline with autospec

Consider a function that uses a Notifier class.

notification_service.py

class Notifier:
def send_notification(self, message):
print(f"Sending: {message}")

def notify_admin(notifier_client, message):
notifier_client.send_notification(message)

Now, let's write a test that has a typo in the method name.

test_notification_service.py

import unittest
from unittest.mock import patch
from notification_service import notify_admin, Notifier

class TestNotificationService(unittest.TestCase):
@patch('notification_service.Notifier', autospec=True)
def test_typo_caught_by_autospec(self, mock_notifier_class):
mock_instance = mock_notifier_class.return_value

with self.assertRaises(AttributeError):
# This will raise an error because `send_notificaton` doesn't exist on the real Notifier
mock_instance.send_notificaton('System is down')

Without autospec, the send_notificaton method call would not have raised an error, and the test would have provided a false positive. autospec forces your test to be a faithful representation of your code's interactions, providing a much higher degree of confidence [2].

The Bottom Line: Combining Advanced Features

  • Use side_effect when you need to simulate dynamic behavior over multiple calls, such as successive API responses or a chain of exceptions.
  • Use autospec (or spec for simpler cases) on every mock you create with @patch or create_autospec. It is the single best way to prevent your tests from passing when your code is broken. By using it consistently, you enforce "mock discipline" and ensure your tests are brittle in a good way-they break when the interface they rely on changes, which is exactly what you want.

Sources

  1. unittest.mock side_effect
  2. Effective Python Mocking: A Practical Guide
  3. The value of spec and autospec in mocking
  4. Mastering the unittest.mock library
  5. Why you should always use autospec when mocking in Python