+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 211 of 365

๐Ÿ“˜ Test Doubles: Stubs, Mocks, and Fakes

Master test doubles: stubs, mocks, and fakes in Python with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

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 test doubles! ๐ŸŽ‰ Have you ever tried to test code that depends on external services, databases, or APIs? Itโ€™s like trying to test a carโ€™s performance on a track thatโ€™s still being built! ๐Ÿ

Test doubles are your solution - theyโ€™re stand-ins for real objects that help you test your code in isolation. Think of them as stunt doubles in movies ๐ŸŽฌ - they look like the real actor but are specifically designed for testing dangerous scenes!

By the end of this tutorial, youโ€™ll be creating test doubles like a pro, making your tests faster, more reliable, and easier to write. Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Test Doubles

๐Ÿค” What are Test Doubles?

Test doubles are like understudies in theater ๐ŸŽญ - they step in when the real actor (your actual dependencies) canโ€™t perform. They help you:

  • โœจ Test in isolation without external dependencies
  • ๐Ÿš€ Run tests quickly without slow operations
  • ๐Ÿ›ก๏ธ Create predictable test scenarios
  • ๐ŸŽฏ Focus on testing one thing at a time

๐Ÿ’ก Types of Test Doubles

There are several types of test doubles, each with its own superpower:

  1. Stubs ๐ŸŽช: Provide canned answers to calls
  2. Mocks ๐ŸŽญ: Verify interactions happened correctly
  3. Fakes ๐ŸŽจ: Have working implementations but simplified
  4. Spies ๐Ÿ•ต๏ธ: Record information about calls
  5. Dummies ๐Ÿค–: Just fill parameter lists

Today, weโ€™ll focus on the big three: Stubs, Mocks, and Fakes!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Simple Stub Example

Letโ€™s start with a friendly stub example:

# ๐Ÿ‘‹ Hello, Test Doubles!
from unittest.mock import Mock

# ๐ŸŽจ Creating a simple stub
def create_weather_stub():
    weather_service = Mock()
    # ๐ŸŒค๏ธ Stub always returns sunny weather
    weather_service.get_temperature.return_value = 72
    weather_service.get_conditions.return_value = "Sunny โ˜€๏ธ"
    return weather_service

# ๐ŸŽฎ Using our stub
weather = create_weather_stub()
print(f"Temperature: {weather.get_temperature()}ยฐF")  # Always 72!
print(f"Conditions: {weather.get_conditions()}")     # Always Sunny!

๐Ÿ’ก Explanation: Stubs provide predetermined responses. Perfect when you need consistent test data!

๐ŸŽฏ Mock with Assertions

Mocks go beyond stubs - they verify interactions:

from unittest.mock import Mock

# ๐ŸŽญ Create a mock email service
email_service = Mock()

# ๐Ÿš€ Use the mock in our code
def send_welcome_email(user_email, email_service):
    subject = "Welcome! ๐ŸŽ‰"
    body = "Thanks for joining us!"
    email_service.send(user_email, subject, body)

# ๐Ÿงช Test with assertions
send_welcome_email("[email protected]", email_service)

# โœ… Verify the email was sent correctly
email_service.send.assert_called_once_with(
    "[email protected]",
    "Welcome! ๐ŸŽ‰",
    "Thanks for joining us!"
)

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: E-commerce Order Processing

Letโ€™s build a realistic order processing system:

from unittest.mock import Mock, MagicMock
from datetime import datetime
import pytest

# ๐Ÿ›๏ธ Our order processing system
class OrderProcessor:
    def __init__(self, payment_gateway, inventory_service, email_service):
        self.payment_gateway = payment_gateway
        self.inventory_service = inventory_service
        self.email_service = email_service
    
    def process_order(self, order):
        # ๐Ÿ“ฆ Check inventory
        if not self.inventory_service.check_availability(order.items):
            return {"status": "failed", "reason": "Out of stock ๐Ÿ˜ข"}
        
        # ๐Ÿ’ณ Process payment
        payment_result = self.payment_gateway.charge(
            order.customer_id,
            order.total_amount
        )
        
        if not payment_result.success:
            return {"status": "failed", "reason": "Payment declined ๐Ÿ’ณโŒ"}
        
        # ๐Ÿ“ง Send confirmation email
        self.email_service.send_confirmation(
            order.customer_email,
            order.order_id
        )
        
        return {
            "status": "success",
            "message": "Order processed! ๐ŸŽ‰",
            "order_id": order.order_id
        }

# ๐Ÿงช Testing with test doubles
def test_successful_order():
    # ๐ŸŽจ Create our test doubles
    payment_gateway = Mock()
    inventory_service = Mock()
    email_service = Mock()
    
    # ๐ŸŽฏ Set up stub responses
    inventory_service.check_availability.return_value = True
    payment_result = Mock()
    payment_result.success = True
    payment_gateway.charge.return_value = payment_result
    
    # ๐Ÿ›’ Create processor and test order
    processor = OrderProcessor(payment_gateway, inventory_service, email_service)
    
    order = Mock()
    order.items = ["Widget", "Gadget"]
    order.customer_id = "CUST123"
    order.total_amount = 99.99
    order.customer_email = "[email protected]"
    order.order_id = "ORD456"
    
    # ๐Ÿš€ Process the order
    result = processor.process_order(order)
    
    # โœ… Verify success
    assert result["status"] == "success"
    assert "๐ŸŽ‰" in result["message"]
    
    # ๐Ÿ” Verify all services were called correctly
    inventory_service.check_availability.assert_called_once_with(order.items)
    payment_gateway.charge.assert_called_once_with("CUST123", 99.99)
    email_service.send_confirmation.assert_called_once_with(
        "[email protected]",
        "ORD456"
    )

๐ŸŽฏ Try it yourself: Add a test for when payment fails!

๐ŸŽฎ Example 2: Game Score API with Fakes

Letโ€™s create a fake implementation for testing:

# ๐Ÿ† Real API client (would hit actual API)
class GameScoreAPI:
    def __init__(self, api_key):
        self.api_key = api_key
        self.base_url = "https://api.gamescores.com"
    
    def get_high_scores(self, game_id):
        # ๐ŸŒ Would make HTTP request
        pass
    
    def submit_score(self, player_id, score):
        # ๐Ÿ“ค Would POST to API
        pass

# ๐ŸŽจ Fake implementation for testing
class FakeGameScoreAPI:
    def __init__(self):
        self.scores = {}  # ๐Ÿ“Š In-memory storage
        
    def get_high_scores(self, game_id):
        # ๐ŸŽฏ Return fake high scores
        if game_id not in self.scores:
            self.scores[game_id] = []
        
        return sorted(self.scores[game_id], 
                     key=lambda x: x['score'], 
                     reverse=True)[:10]
    
    def submit_score(self, game_id, player_id, score):
        # ๐Ÿ’พ Store in memory instead of API
        if game_id not in self.scores:
            self.scores[game_id] = []
        
        self.scores[game_id].append({
            'player': player_id,
            'score': score,
            'timestamp': datetime.now(),
            'rank': self._calculate_rank(game_id, score)
        })
        
        return {'success': True, 'rank': self._calculate_rank(game_id, score)}
    
    def _calculate_rank(self, game_id, score):
        # ๐Ÿ… Calculate player's rank
        if game_id not in self.scores:
            return 1
        
        higher_scores = sum(1 for s in self.scores[game_id] 
                          if s['score'] > score)
        return higher_scores + 1

# ๐ŸŽฎ Game using our API
class ArcadeGame:
    def __init__(self, score_api):
        self.score_api = score_api
        self.game_id = "space-invaders"
    
    def end_game(self, player_id, final_score):
        # ๐ŸŽŠ Submit score and show result
        result = self.score_api.submit_score(
            self.game_id,
            player_id,
            final_score
        )
        
        if result['rank'] <= 3:
            return f"๐Ÿ† NEW HIGH SCORE! Rank #{result['rank']}!"
        elif result['rank'] <= 10:
            return f"๐ŸŒŸ Top 10! Rank #{result['rank']}!"
        else:
            return f"Score submitted! Rank #{result['rank']} ๐ŸŽฎ"

# ๐Ÿงช Test with fake
def test_arcade_game_scoring():
    # ๐ŸŽจ Use fake instead of real API
    fake_api = FakeGameScoreAPI()
    game = ArcadeGame(fake_api)
    
    # ๐ŸŽฎ Simulate multiple players
    assert "๐Ÿ†" in game.end_game("Player1", 1000)  # First player
    assert "๐Ÿ†" in game.end_game("Player2", 2000)  # New high score!
    assert "๐ŸŒŸ" in game.end_game("Player3", 500)   # Lower score
    
    # ๐Ÿ” Verify high scores
    high_scores = fake_api.get_high_scores("space-invaders")
    assert len(high_scores) == 3
    assert high_scores[0]['score'] == 2000

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Advanced Mocking with Side Effects

When you need dynamic behavior:

from unittest.mock import Mock, call

# ๐ŸŽฏ Mock with side effects
database_mock = Mock()

# ๐ŸŽฒ Different results each time
database_mock.get_user.side_effect = [
    {"name": "Alice", "status": "active"},
    {"name": "Bob", "status": "inactive"},
    None  # Third call returns None
]

# ๐Ÿงช Test the progression
print(database_mock.get_user())  # Alice
print(database_mock.get_user())  # Bob
print(database_mock.get_user())  # None

# ๐Ÿš€ Mock that raises exceptions
api_mock = Mock()
api_mock.connect.side_effect = ConnectionError("Network timeout! ๐ŸŒโŒ")

try:
    api_mock.connect()
except ConnectionError as e:
    print(f"Caught: {e}")

๐Ÿ—๏ธ Spy Pattern Implementation

Create a spy that records everything:

class SpyEmailService:
    def __init__(self, real_service=None):
        self.real_service = real_service
        self.sent_emails = []  # ๐Ÿ“ Record all emails
        
    def send(self, to, subject, body):
        # ๐Ÿ“ธ Record the call
        self.sent_emails.append({
            'to': to,
            'subject': subject,
            'body': body,
            'timestamp': datetime.now()
        })
        
        # ๐ŸŽฏ Optionally call real service
        if self.real_service:
            return self.real_service.send(to, subject, body)
        
        return True  # ๐ŸŽญ Fake success
    
    def get_sent_count(self):
        return len(self.sent_emails)
    
    def was_email_sent_to(self, email):
        return any(e['to'] == email for e in self.sent_emails)
    
    def get_emails_with_subject(self, subject):
        return [e for e in self.sent_emails if subject in e['subject']]

# ๐Ÿงช Using our spy
spy = SpyEmailService()
spy.send("[email protected]", "Welcome! ๐ŸŽ‰", "Thanks for joining!")
spy.send("[email protected]", "Alert! ๐Ÿšจ", "System notification")

print(f"Emails sent: {spy.get_sent_count()}")
print(f"Email to user? {spy.was_email_sent_to('[email protected]')}")

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Over-Mocking

# โŒ Wrong - mocking too much!
def test_calculator_add():
    calc = Mock()
    calc.add.return_value = 5
    assert calc.add(2, 3) == 5  # ๐Ÿ’ฅ Not testing anything real!

# โœ… Correct - mock external dependencies only!
def test_calculator_with_logger():
    logger = Mock()  # Mock the logger
    calc = Calculator(logger)  # Real calculator
    
    result = calc.add(2, 3)
    assert result == 5  # Test real logic
    logger.info.assert_called_once()  # Verify logging

๐Ÿคฏ Pitfall 2: Brittle Mock Assertions

# โŒ Too specific - breaks easily!
mock.method.assert_called_with(
    "exact string",
    {"every": "single", "key": "must", "match": "perfectly"}
)

# โœ… Better - focus on what matters!
mock.method.assert_called_once()
args, kwargs = mock.method.call_args

assert "string" in args[0]  # Key part of string
assert kwargs.get("key") == "must"  # Important values only

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Mock at boundaries: Only mock external dependencies
  2. ๐Ÿ“ Clear test names: test_order_fails_when_payment_declined
  3. ๐Ÿ›ก๏ธ One mock per concept: Donโ€™t create mega-mocks
  4. ๐ŸŽจ Use appropriate double: Stub for queries, Mock for commands
  5. โœจ Keep it simple: Prefer simple fakes over complex mocks

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Weather Alert System

Create a weather alert system with test doubles:

๐Ÿ“‹ Requirements:

  • โœ… Weather API client (to be mocked)
  • ๐Ÿท๏ธ Alert service that sends notifications
  • ๐Ÿ‘ค User preferences for alert thresholds
  • ๐Ÿ“… Schedule checker for when to send alerts
  • ๐ŸŽจ Different alert types (rain โ˜”, snow โ„๏ธ, heat ๐ŸŒก๏ธ)

๐Ÿš€ Bonus Points:

  • Use all three types of test doubles
  • Test error scenarios
  • Create a fake that simulates weather patterns

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
from unittest.mock import Mock, MagicMock
from datetime import datetime, time
import pytest

# ๐ŸŒฆ๏ธ Weather Alert System
class WeatherAlertSystem:
    def __init__(self, weather_api, notification_service, user_prefs):
        self.weather_api = weather_api
        self.notification_service = notification_service
        self.user_prefs = user_prefs
    
    def check_and_alert(self, user_id):
        # ๐Ÿ“Š Get user preferences
        prefs = self.user_prefs.get_preferences(user_id)
        if not prefs or not prefs.get('alerts_enabled'):
            return None
        
        # ๐ŸŒก๏ธ Get current weather
        weather = self.weather_api.get_current_weather(prefs['location'])
        
        alerts_sent = []
        
        # โ˜” Check rain alert
        if weather['rain_chance'] > prefs.get('rain_threshold', 70):
            alert = f"โ˜” Rain alert! {weather['rain_chance']}% chance"
            self.notification_service.send_alert(user_id, alert)
            alerts_sent.append(alert)
        
        # ๐ŸŒก๏ธ Check temperature alert
        if weather['temperature'] > prefs.get('heat_threshold', 90):
            alert = f"๐ŸŒก๏ธ Heat alert! {weather['temperature']}ยฐF"
            self.notification_service.send_alert(user_id, alert)
            alerts_sent.append(alert)
        
        # โ„๏ธ Check snow alert
        if weather.get('snow_chance', 0) > prefs.get('snow_threshold', 50):
            alert = f"โ„๏ธ Snow alert! {weather['snow_chance']}% chance"
            self.notification_service.send_alert(user_id, alert)
            alerts_sent.append(alert)
        
        return alerts_sent

# ๐ŸŽจ Fake Weather API for testing
class FakeWeatherAPI:
    def __init__(self):
        self.weather_data = {}
        self.call_count = 0
    
    def set_weather(self, location, weather):
        """Helper method to set test data"""
        self.weather_data[location] = weather
    
    def get_current_weather(self, location):
        self.call_count += 1
        
        # ๐ŸŽฏ Return fake data or default
        return self.weather_data.get(location, {
            'temperature': 72,
            'rain_chance': 10,
            'snow_chance': 0,
            'conditions': 'Sunny โ˜€๏ธ'
        })

# ๐Ÿงช Test Suite
class TestWeatherAlertSystem:
    def test_rain_alert_triggered(self):
        # ๐ŸŽญ Setup mocks
        weather_api = Mock()
        notification_service = Mock()
        user_prefs = Mock()
        
        # ๐ŸŽฏ Configure stubs
        user_prefs.get_preferences.return_value = {
            'alerts_enabled': True,
            'location': 'Seattle',
            'rain_threshold': 60
        }
        
        weather_api.get_current_weather.return_value = {
            'temperature': 65,
            'rain_chance': 85,  # Above threshold!
            'conditions': 'Cloudy โ˜๏ธ'
        }
        
        # ๐Ÿš€ Test the system
        alert_system = WeatherAlertSystem(
            weather_api, 
            notification_service, 
            user_prefs
        )
        
        alerts = alert_system.check_and_alert('user123')
        
        # โœ… Verify behavior
        assert len(alerts) == 1
        assert "โ˜”" in alerts[0]
        notification_service.send_alert.assert_called_once()
        
    def test_multiple_alerts_with_fake(self):
        # ๐ŸŽจ Use fake for more complex scenario
        fake_weather = FakeWeatherAPI()
        notification_service = Mock()
        user_prefs = Mock()
        
        # ๐ŸŒฆ๏ธ Set extreme weather
        fake_weather.set_weather('Phoenix', {
            'temperature': 115,  # Hot! ๐Ÿ”ฅ
            'rain_chance': 5,
            'conditions': 'Scorching โ˜€๏ธ'
        })
        
        user_prefs.get_preferences.return_value = {
            'alerts_enabled': True,
            'location': 'Phoenix',
            'heat_threshold': 100
        }
        
        alert_system = WeatherAlertSystem(
            fake_weather,
            notification_service,
            user_prefs
        )
        
        alerts = alert_system.check_and_alert('user456')
        
        # โœ… Should get heat alert
        assert len(alerts) == 1
        assert "๐ŸŒก๏ธ" in alerts[0]
        assert "115ยฐF" in alerts[0]
        
    def test_spy_pattern_for_api_calls(self):
        # ๐Ÿ•ต๏ธ Create a spy to track API usage
        class WeatherAPISpy:
            def __init__(self, real_api=None):
                self.real_api = real_api
                self.calls = []
            
            def get_current_weather(self, location):
                self.calls.append({
                    'method': 'get_current_weather',
                    'location': location,
                    'timestamp': datetime.now()
                })
                
                if self.real_api:
                    return self.real_api.get_current_weather(location)
                
                # ๐ŸŽฏ Default response
                return {'temperature': 70, 'rain_chance': 20}
        
        spy = WeatherAPISpy()
        notification_service = Mock()
        user_prefs = Mock()
        
        user_prefs.get_preferences.return_value = {
            'alerts_enabled': True,
            'location': 'Boston'
        }
        
        alert_system = WeatherAlertSystem(spy, notification_service, user_prefs)
        alert_system.check_and_alert('user789')
        
        # ๐Ÿ” Verify API was called correctly
        assert len(spy.calls) == 1
        assert spy.calls[0]['location'] == 'Boston'

๐ŸŽ“ Key Takeaways

Youโ€™ve learned so much! Hereโ€™s what you can now do:

  • โœ… Create stubs for predictable test data ๐ŸŽช
  • โœ… Use mocks to verify interactions ๐ŸŽญ
  • โœ… Build fakes for complex scenarios ๐ŸŽจ
  • โœ… Avoid common testing pitfalls ๐Ÿ›ก๏ธ
  • โœ… Write faster, more reliable tests ๐Ÿš€

Remember: Test doubles are your friends - they make testing easier, not harder! Theyโ€™re here to help you write better, more maintainable tests. ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered test doubles!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the weather alert exercise
  2. ๐Ÿ—๏ธ Refactor existing tests to use test doubles
  3. ๐Ÿ“š Explore advanced mocking with pytest-mock
  4. ๐ŸŒŸ Learn about property-based testing next!

Remember: Every testing expert started by writing their first mock. Keep practicing, keep learning, and most importantly, keep your tests fast and reliable! ๐Ÿš€


Happy testing! ๐ŸŽ‰๐Ÿงชโœจ