Prerequisites
- Basic understanding of programming concepts ๐
- Python installation (3.8+) ๐
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write clean, Pythonic code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on Pythonโs unittest.mock module! ๐ In this guide, weโll explore how mocking can revolutionize your testing strategy.
Have you ever tried to test code that connects to a database, calls an API, or sends emails? ๐ง Without mocking, these tests would be slow, unreliable, and potentially expensive! Thatโs where mocking comes to the rescue. ๐ฆธโโ๏ธ
By the end of this tutorial, youโll feel confident using mocks to create fast, reliable tests for even the most complex code! Letโs dive in! ๐โโ๏ธ
๐ Understanding Mocking
๐ค What is Mocking?
Mocking is like using a stunt double in movies ๐ฌ. Instead of having the real actor perform dangerous stunts, you use a double who looks similar but is safer to work with. In testing, mocks are stand-ins for real objects that are expensive, slow, or unpredictable.
In Python terms, mocking lets you replace parts of your system with fake objects that:
- โจ Return predictable values
- ๐ Execute instantly (no network calls!)
- ๐ก๏ธ Isolate your code for true unit testing
๐ก Why Use Mocking?
Hereโs why developers love mocking:
- Speed โก: Tests run in milliseconds, not seconds
- Reliability ๐: No dependency on external services
- Control ๐ฎ: Test edge cases and error scenarios easily
- Cost-effective ๐ฐ: No API rate limits or usage fees
Real-world example: Imagine testing a weather app ๐ฆ๏ธ. Without mocking, youโd need to call a real weather API for every test. With mocking, you can simulate any weather condition instantly!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
# ๐ Hello, Mock!
from unittest.mock import Mock
# ๐จ Creating a simple mock
weather_api = Mock()
# ๐ฏ Configure the mock to return specific data
weather_api.get_temperature.return_value = 25 # ๐ก๏ธ Always sunny!
# ๐ก Use it like a real object
temp = weather_api.get_temperature("London")
print(f"Temperature: {temp}ยฐC") # Temperature: 25ยฐC
# ๐ Check if the method was called
weather_api.get_temperature.assert_called_with("London")
๐ก Explanation: Notice how we created a fake weather API that always returns 25ยฐC! No internet connection needed! ๐
๐ฏ Common Patterns
Here are patterns youโll use daily:
# ๐๏ธ Pattern 1: Using patch decorator
from unittest.mock import patch
@patch('requests.get')
def test_api_call(mock_get):
# ๐จ Configure the mock response
mock_get.return_value.json.return_value = {'status': 'success'}
# ๐ Your test code here
result = my_api_function()
assert result['status'] == 'success'
# ๐ฎ Pattern 2: Mock as context manager
with patch('builtins.open', mock_open(read_data='Hello! ๐')):
content = read_file('test.txt')
assert content == 'Hello! ๐'
# ๐ Pattern 3: Side effects for multiple calls
mock_db = Mock()
mock_db.fetch.side_effect = [
{'id': 1, 'name': 'Alice ๐ฉ'},
{'id': 2, 'name': 'Bob ๐จ'},
None # ๐ No more results
]
๐ก Practical Examples
๐ Example 1: E-commerce Order Service
Letโs build something real:
# ๐๏ธ Our order service that needs mocking
class OrderService:
def __init__(self, payment_gateway, email_service, inventory):
self.payment = payment_gateway
self.email = email_service
self.inventory = inventory
def process_order(self, order_id, items, customer_email, amount):
# ๐ฆ Check inventory
for item in items:
if not self.inventory.check_stock(item['id'], item['quantity']):
return {'status': 'failed', 'reason': 'out_of_stock'}
# ๐ณ Process payment
payment_result = self.payment.charge(amount, customer_email)
if not payment_result['success']:
return {'status': 'failed', 'reason': 'payment_failed'}
# ๐ง Send confirmation email
self.email.send_confirmation(customer_email, order_id)
return {'status': 'success', 'order_id': order_id}
# ๐งช Let's test it with mocks!
from unittest.mock import Mock, patch
def test_successful_order():
# ๐จ Create our mocks
mock_payment = Mock()
mock_email = Mock()
mock_inventory = Mock()
# ๐ฏ Configure successful responses
mock_inventory.check_stock.return_value = True # โ
In stock!
mock_payment.charge.return_value = {'success': True, 'transaction_id': 'TX123'}
# ๐ Create service with mocks
service = OrderService(mock_payment, mock_email, mock_inventory)
# ๐ฎ Process an order
result = service.process_order(
'ORDER001',
[{'id': 'PROD1', 'quantity': 2}],
'[email protected]',
99.99
)
# โ
Verify success
assert result['status'] == 'success'
# ๐ Verify all services were called correctly
mock_inventory.check_stock.assert_called_with('PROD1', 2)
mock_payment.charge.assert_called_with(99.99, '[email protected]')
mock_email.send_confirmation.assert_called_once()
print("๐ Order processed successfully!")
# ๐ซ Test failure scenarios
def test_out_of_stock():
# ๐จ Create mocks
mock_payment = Mock()
mock_email = Mock()
mock_inventory = Mock()
# โ Configure out of stock response
mock_inventory.check_stock.return_value = False
service = OrderService(mock_payment, mock_email, mock_inventory)
result = service.process_order('ORDER002', [{'id': 'PROD1', 'quantity': 100}], '[email protected]', 999.99)
assert result['status'] == 'failed'
assert result['reason'] == 'out_of_stock'
# ๐ก Payment should NOT be charged!
mock_payment.charge.assert_not_called()
mock_email.send_confirmation.assert_not_called()
print("โ
Out of stock handled correctly!")
๐ฏ Try it yourself: Add a test for payment failure scenario!
๐ฎ Example 2: Game Leaderboard Service
Letโs make it fun:
# ๐ Game leaderboard with external dependencies
import json
from datetime import datetime
class GameLeaderboard:
def __init__(self, database, cache, notification_service):
self.db = database
self.cache = cache
self.notifier = notification_service
def add_score(self, player_name, score, game_id):
# ๐ฏ Check cache for current high score
cache_key = f"highscore:{game_id}"
current_high = self.cache.get(cache_key)
# ๐พ Save to database
score_data = {
'player': player_name,
'score': score,
'game_id': game_id,
'timestamp': datetime.now().isoformat(),
'emoji': self._get_rank_emoji(score)
}
self.db.insert('scores', score_data)
# ๐ New high score?
if current_high is None or score > current_high['score']:
self.cache.set(cache_key, score_data, ttl=3600)
self.notifier.announce_high_score(player_name, score, game_id)
return {'new_high_score': True, 'rank': 1}
# ๐ Get player rank
rank = self.db.count('scores', {'game_id': game_id, 'score': {'$gt': score}}) + 1
return {'new_high_score': False, 'rank': rank}
def _get_rank_emoji(self, score):
if score >= 1000: return "๐"
elif score >= 500: return "๐ฅ"
elif score >= 100: return "๐ฅ"
else: return "๐"
# ๐งช Test with mocks and patch
from unittest.mock import Mock, patch, MagicMock
def test_new_high_score():
# ๐จ Create our service mocks
mock_db = Mock()
mock_cache = Mock()
mock_notifier = Mock()
# ๐ฏ Configure cache to return no previous high score
mock_cache.get.return_value = None
# ๐ฎ Create leaderboard
leaderboard = GameLeaderboard(mock_db, mock_cache, mock_notifier)
# ๐ Add a new high score!
with patch('datetime.datetime') as mock_datetime:
mock_datetime.now.return_value.isoformat.return_value = '2024-01-01T12:00:00'
result = leaderboard.add_score('PlayerOne', 1500, 'GAME001')
# โ
Verify new high score
assert result['new_high_score'] == True
assert result['rank'] == 1
# ๐ Verify database insert
mock_db.insert.assert_called_once()
call_args = mock_db.insert.call_args[0]
assert call_args[0] == 'scores'
assert call_args[1]['emoji'] == '๐'
# ๐ Verify notification sent
mock_notifier.announce_high_score.assert_called_with('PlayerOne', 1500, 'GAME001')
print("๐ New high score recorded!")
def test_regular_score_with_ranking():
# ๐จ Setup mocks
mock_db = Mock()
mock_cache = Mock()
mock_notifier = Mock()
# ๐ Configure existing high score
mock_cache.get.return_value = {'score': 2000, 'player': 'ChampionPlayer'}
# ๐ Configure database count for ranking
mock_db.count.return_value = 5 # 5 players scored higher
leaderboard = GameLeaderboard(mock_db, mock_cache, mock_notifier)
result = leaderboard.add_score('NewPlayer', 750, 'GAME001')
# โ
Verify not a high score
assert result['new_high_score'] == False
assert result['rank'] == 6 # 5 + 1
# ๐ซ No high score notification
mock_notifier.announce_high_score.assert_not_called()
print("โ
Regular score ranked correctly!")
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Magic Methods and Spec
When youโre ready to level up, try these advanced patterns:
# ๐ฏ Mock with spec for type safety
from unittest.mock import Mock, create_autospec
class RealDatabase:
def connect(self): pass
def query(self, sql): pass
def close(self): pass
# ๐ช Create a mock that matches the real class
mock_db = create_autospec(RealDatabase)
# โ
This works - method exists
mock_db.query("SELECT * FROM users")
# โ This raises AttributeError - no such method!
# mock_db.invalid_method() # ๐ฅ Caught at test time!
# ๐จ Mock magic methods
class MagicCounter:
def __init__(self):
self.count = 0
def __len__(self):
return self.count
def __getitem__(self, key):
return f"Item {key}"
# ๐ Mock with magic methods
mock_counter = Mock(spec=MagicCounter)
mock_counter.__len__.return_value = 42
mock_counter.__getitem__.return_value = "Mocked item! โจ"
assert len(mock_counter) == 42
assert mock_counter[0] == "Mocked item! โจ"
๐๏ธ Advanced Topic 2: PropertyMock and Async Mocking
For the brave developers:
# ๐ Mock properties
from unittest.mock import PropertyMock, patch
class GameCharacter:
@property
def health(self):
return self._health
@property
def is_alive(self):
return self.health > 0
# ๐ฎ Mock a property
with patch.object(GameCharacter, 'health', new_callable=PropertyMock) as mock_health:
mock_health.return_value = 100
character = GameCharacter()
assert character.health == 100
assert character.is_alive == True
# ๐ฅ Simulate damage
mock_health.return_value = 0
assert character.is_alive == False
# ๐ Async mocking (Python 3.8+)
from unittest.mock import AsyncMock
async def test_async_api():
# ๐ฏ Create async mock
mock_api = AsyncMock()
mock_api.fetch_data.return_value = {'status': 'success', 'data': [1, 2, 3]}
# ๐ Use it like a real async function
result = await mock_api.fetch_data('endpoint')
assert result['status'] == 'success'
# ๐ Verify async calls
mock_api.fetch_data.assert_awaited_once_with('endpoint')
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Patching in the Wrong Place
# โ Wrong way - patching where it's defined
# file: payment.py
import stripe
def charge_customer(amount):
return stripe.Charge.create(amount=amount)
# test file
@patch('stripe.Charge') # โ Won't work!
def test_charge(mock_charge):
charge_customer(100)
# โ
Correct way - patch where it's used!
@patch('payment.stripe.Charge') # โ
Patch in payment module
def test_charge(mock_charge):
mock_charge.create.return_value = {'id': 'ch_123', 'status': 'succeeded'}
result = charge_customer(100)
assert result['status'] == 'succeeded'
๐คฏ Pitfall 2: Forgetting to Reset Mocks
# โ Dangerous - mock state carries over!
mock_api = Mock()
def test_first():
mock_api.call_count = 0
mock_api.method()
assert mock_api.method.call_count == 1
def test_second():
# ๐ฅ This fails! call_count is still 1
assert mock_api.method.call_count == 0
# โ
Safe - use fresh mocks or reset!
def test_with_reset():
mock_api = Mock() # Fresh mock
# OR
mock_api.reset_mock() # Reset existing mock
mock_api.method()
assert mock_api.method.call_count == 1
๐ ๏ธ Best Practices
- ๐ฏ Mock at boundaries: Mock external dependencies, not your own code
- ๐ Use spec: Always use
spec
orautospec
for type safety - ๐ก๏ธ Test behavior, not implementation: Focus on what, not how
- ๐จ Keep it simple: Donโt over-mock; some integration is good
- โจ Name your mocks: Use descriptive names like
mock_payment_gateway
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Weather Alert System
Create a weather alert system with proper mocking:
๐ Requirements:
- โ Fetch weather data from an API
- ๐ท๏ธ Check for severe weather conditions
- ๐ค Send alerts to subscribed users
- ๐ Log all alerts to a database
- ๐จ Include weather emojis in alerts!
๐ Bonus Points:
- Add retry logic for API failures
- Implement rate limiting
- Create different alert levels
๐ก Solution
๐ Click to see solution
# ๐ฏ Our weather alert system with full mocking!
from unittest.mock import Mock, patch, call
from datetime import datetime
class WeatherAlertSystem:
def __init__(self, weather_api, notification_service, database, logger):
self.api = weather_api
self.notifier = notification_service
self.db = database
self.logger = logger
def check_and_alert(self, city):
try:
# ๐ก๏ธ Get weather data
weather_data = self.api.get_current_weather(city)
# ๐ช๏ธ Check for severe conditions
alerts = self._analyze_conditions(weather_data)
if alerts:
# ๐ข Get subscribers
subscribers = self.db.get_subscribers(city)
# ๐ง Send alerts
for alert in alerts:
for subscriber in subscribers:
self.notifier.send_alert(
subscriber['email'],
alert['message'],
alert['severity']
)
# ๐พ Log alert
self.db.log_alert({
'city': city,
'timestamp': datetime.now().isoformat(),
'type': alert['type'],
'severity': alert['severity']
})
self.logger.info(f"Sent {len(alerts)} alerts for {city}")
return {'alerts_sent': len(alerts), 'status': 'success'}
return {'alerts_sent': 0, 'status': 'success'}
except Exception as e:
self.logger.error(f"Error checking weather for {city}: {e}")
return {'alerts_sent': 0, 'status': 'error', 'error': str(e)}
def _analyze_conditions(self, weather_data):
alerts = []
# ๐ก๏ธ Temperature alerts
if weather_data['temp'] > 35:
alerts.append({
'type': 'heat',
'severity': 'high',
'message': f"๐ฅ Extreme heat warning! {weather_data['temp']}ยฐC"
})
elif weather_data['temp'] < -10:
alerts.append({
'type': 'cold',
'severity': 'high',
'message': f"๐ฅถ Extreme cold warning! {weather_data['temp']}ยฐC"
})
# ๐จ Wind alerts
if weather_data['wind_speed'] > 100:
alerts.append({
'type': 'wind',
'severity': 'extreme',
'message': f"๐ช๏ธ Tornado warning! Wind: {weather_data['wind_speed']}km/h"
})
return alerts
# ๐งช Comprehensive test with mocks
def test_extreme_weather_alerts():
# ๐จ Create all our mocks
mock_api = Mock()
mock_notifier = Mock()
mock_db = Mock()
mock_logger = Mock()
# ๐ก๏ธ Configure extreme weather data
mock_api.get_current_weather.return_value = {
'temp': 42,
'wind_speed': 120,
'humidity': 85,
'conditions': 'extreme'
}
# ๐ฅ Configure subscribers
mock_db.get_subscribers.return_value = [
{'email': '[email protected]', 'name': 'Alice'},
{'email': '[email protected]', 'name': 'Bob'}
]
# ๐ฎ Create system and check weather
system = WeatherAlertSystem(mock_api, mock_notifier, mock_db, mock_logger)
with patch('datetime.datetime') as mock_datetime:
mock_datetime.now.return_value.isoformat.return_value = '2024-01-01T15:00:00'
result = system.check_and_alert('Phoenix')
# โ
Verify results
assert result['alerts_sent'] == 2
assert result['status'] == 'success'
# ๐ Verify API was called
mock_api.get_current_weather.assert_called_once_with('Phoenix')
# ๐ง Verify notifications sent (2 alerts ร 2 subscribers = 4 calls)
assert mock_notifier.send_alert.call_count == 4
# Check specific calls
expected_calls = [
call('[email protected]', '๐ฅ Extreme heat warning! 42ยฐC', 'high'),
call('[email protected]', '๐ฅ Extreme heat warning! 42ยฐC', 'high'),
call('[email protected]', '๐ช๏ธ Tornado warning! Wind: 120km/h', 'extreme'),
call('[email protected]', '๐ช๏ธ Tornado warning! Wind: 120km/h', 'extreme')
]
mock_notifier.send_alert.assert_has_calls(expected_calls)
# ๐พ Verify database logging
assert mock_db.log_alert.call_count == 2
print("๐ Weather alert system working perfectly!")
# ๐ซ Test error handling
def test_api_failure_handling():
# ๐จ Setup mocks
mock_api = Mock()
mock_notifier = Mock()
mock_db = Mock()
mock_logger = Mock()
# ๐ฅ Configure API to fail
mock_api.get_current_weather.side_effect = Exception("API timeout")
system = WeatherAlertSystem(mock_api, mock_notifier, mock_db, mock_logger)
result = system.check_and_alert('London')
# โ
Verify error handling
assert result['status'] == 'error'
assert result['alerts_sent'] == 0
assert 'API timeout' in result['error']
# ๐ Verify error was logged
mock_logger.error.assert_called_once()
# ๐ซ No alerts should be sent
mock_notifier.send_alert.assert_not_called()
print("โ
Error handling works correctly!")
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create mocks for any dependency ๐ช
- โ Use patch decorators to replace objects ๐ก๏ธ
- โ Test edge cases without real services ๐ฏ
- โ Verify method calls and arguments ๐
- โ Build testable code with confidence! ๐
Remember: Mocking is a powerful tool, but donโt mock everything! Find the right balance between unit and integration tests. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered Pythonโs unittest.mock module!
Hereโs what to do next:
- ๐ป Practice with the weather alert exercise
- ๐๏ธ Add mocks to your existing test suite
- ๐ Explore pytest-mock for even more features
- ๐ Learn about test doubles: mocks vs stubs vs fakes
Remember: Great tests make great software. Keep mocking, keep testing, and most importantly, have fun! ๐
Happy testing! ๐๐งชโจ