+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 201 of 365

๐Ÿ“˜ Test Fixtures: setUp and tearDown

Master test fixtures: setup and teardown in Python with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
30 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 fixtures! ๐ŸŽ‰ In this guide, weโ€™ll explore how setUp and tearDown methods can transform your testing experience.

Youโ€™ll discover how test fixtures make your tests cleaner, more maintainable, and DRY (Donโ€™t Repeat Yourself)! Whether youโ€™re testing web applications ๐ŸŒ, data processing pipelines ๐Ÿ”„, or game logic ๐ŸŽฎ, understanding test fixtures is essential for writing robust test suites.

By the end of this tutorial, youโ€™ll feel confident using setUp and tearDown in your own projects! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Test Fixtures

๐Ÿค” What are Test Fixtures?

Test fixtures are like preparing your workspace before starting a project ๐ŸŽจ. Think of it as setting up your kitchen before cooking - you gather ingredients, clean surfaces, and prepare tools. After cooking, you clean up!

In Python testing terms, fixtures are the setup and cleanup code that runs before and after your tests. This means you can:

  • โœจ Prepare test data consistently
  • ๐Ÿš€ Initialize test objects once
  • ๐Ÿ›ก๏ธ Clean up resources automatically

๐Ÿ’ก Why Use Test Fixtures?

Hereโ€™s why developers love test fixtures:

  1. DRY Code ๐Ÿ”’: Donโ€™t repeat setup code in every test
  2. Consistent State ๐Ÿ’ป: Each test starts with the same conditions
  3. Clean Isolation ๐Ÿ“–: Tests donโ€™t affect each other
  4. Resource Management ๐Ÿ”ง: Automatic cleanup of files, connections, etc.

Real-world example: Imagine testing an e-commerce cart ๐Ÿ›’. With fixtures, you can create a fresh cart with sample products before each test!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Simple Example with unittest

Letโ€™s start with a friendly example:

# ๐Ÿ‘‹ Hello, Test Fixtures!
import unittest

class TestShoppingCart(unittest.TestCase):
    
    def setUp(self):
        # ๐ŸŽจ This runs before EACH test method
        print("๐Ÿ”ง Setting up test...")
        self.cart = []  # ๐Ÿ›’ Fresh cart for each test
        self.products = [
            {"name": "Python Book", "price": 29.99, "emoji": "๐Ÿ“˜"},
            {"name": "Coffee", "price": 4.99, "emoji": "โ˜•"},
            {"name": "Laptop", "price": 999.99, "emoji": "๐Ÿ’ป"}
        ]
    
    def tearDown(self):
        # ๐Ÿงน This runs after EACH test method
        print("๐Ÿงน Cleaning up test...")
        self.cart.clear()
        # Could close files, database connections, etc.
    
    def test_add_item(self):
        # ๐ŸŽฏ Test adding items
        self.cart.append(self.products[0])
        self.assertEqual(len(self.cart), 1)
        print(f"โœ… Added {self.products[0]['emoji']} to cart!")
    
    def test_multiple_items(self):
        # ๐ŸŽฏ Test multiple items
        self.cart.extend(self.products[:2])
        self.assertEqual(len(self.cart), 2)
        print(f"โœ… Added multiple items to cart!")

๐Ÿ’ก Explanation: Notice how setUp creates a fresh cart for each test! The tearDown ensures cleanup happens automatically.

๐ŸŽฏ Common Patterns

Here are patterns youโ€™ll use daily:

# ๐Ÿ—๏ธ Pattern 1: Database test fixtures
class TestUserDatabase(unittest.TestCase):
    def setUp(self):
        # ๐Ÿ—„๏ธ Create test database
        self.db = TestDatabase()
        self.db.connect()
        self.test_user = {
            "name": "Alice",
            "email": "[email protected]",
            "emoji": "๐Ÿ‘ฉโ€๐Ÿ’ป"
        }
    
    def tearDown(self):
        # ๐Ÿงน Clean up database
        self.db.clear_all_data()
        self.db.disconnect()

# ๐ŸŽจ Pattern 2: File handling fixtures
class TestFileProcessor(unittest.TestCase):
    def setUp(self):
        # ๐Ÿ“ Create test files
        self.test_file = "test_data.txt"
        with open(self.test_file, 'w') as f:
            f.write("Test data ๐ŸŽฏ")
    
    def tearDown(self):
        # ๐Ÿ—‘๏ธ Remove test files
        import os
        if os.path.exists(self.test_file):
            os.remove(self.test_file)

# ๐Ÿ”„ Pattern 3: Mock fixtures
class TestAPIClient(unittest.TestCase):
    def setUp(self):
        # ๐ŸŽญ Set up mocks
        self.mock_response = {"status": "success", "emoji": "โœ…"}
        self.client = APIClient()
        self.client.mock_mode = True

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: E-Commerce Cart Testing

Letโ€™s build something real:

# ๐Ÿ›๏ธ Complete shopping cart test suite
import unittest
from datetime import datetime

class ShoppingCart:
    def __init__(self):
        self.items = []
        self.discount_code = None
        
    def add_item(self, product):
        # โž• Add product to cart
        self.items.append(product)
        return f"Added {product['emoji']} {product['name']}!"
    
    def apply_discount(self, code):
        # ๐Ÿท๏ธ Apply discount code
        self.discount_code = code
        return "Discount applied! ๐ŸŽ‰"
    
    def calculate_total(self):
        # ๐Ÿ’ฐ Calculate total with discount
        total = sum(item['price'] for item in self.items)
        if self.discount_code == "PYTHON20":
            total *= 0.8  # 20% off
        return round(total, 2)
    
    def checkout(self):
        # ๐Ÿ›’ Process checkout
        if not self.items:
            return "Cart is empty! ๐Ÿ˜…"
        return f"Order confirmed! Total: ${self.calculate_total()} ๐ŸŽ‰"

class TestShoppingCart(unittest.TestCase):
    def setUp(self):
        # ๐ŸŽจ Fresh cart and products for each test
        print(f"\n๐Ÿ”ง Setting up test at {datetime.now().strftime('%H:%M:%S')}")
        self.cart = ShoppingCart()
        self.products = {
            'book': {"name": "Python Mastery", "price": 49.99, "emoji": "๐Ÿ“˜"},
            'course': {"name": "Testing Course", "price": 99.99, "emoji": "๐ŸŽ“"},
            'coffee': {"name": "Developer Fuel", "price": 4.99, "emoji": "โ˜•"}
        }
        
    def tearDown(self):
        # ๐Ÿงน Clean up and log
        print(f"๐Ÿงน Test completed. Cart had {len(self.cart.items)} items")
        self.cart = None
        
    def test_empty_cart(self):
        # ๐Ÿ›’ Test empty cart behavior
        result = self.cart.checkout()
        self.assertEqual(result, "Cart is empty! ๐Ÿ˜…")
        
    def test_add_single_item(self):
        # โž• Test adding one item
        result = self.cart.add_item(self.products['book'])
        self.assertIn("๐Ÿ“˜", result)
        self.assertEqual(len(self.cart.items), 1)
        
    def test_discount_code(self):
        # ๐Ÿท๏ธ Test discount functionality
        self.cart.add_item(self.products['course'])
        self.cart.apply_discount("PYTHON20")
        total = self.cart.calculate_total()
        self.assertEqual(total, 79.99)  # 20% off 99.99
        
    def test_full_shopping_flow(self):
        # ๐ŸŽฎ Test complete user journey
        # Add items
        self.cart.add_item(self.products['book'])
        self.cart.add_item(self.products['coffee'])
        
        # Apply discount
        self.cart.apply_discount("PYTHON20")
        
        # Checkout
        result = self.cart.checkout()
        self.assertIn("$43.98", result)  # (49.99 + 4.99) * 0.8
        self.assertIn("๐ŸŽ‰", result)

๐ŸŽฏ Try it yourself: Add a test for removing items and cart persistence!

๐ŸŽฎ Example 2: Game State Testing

Letโ€™s make it fun:

# ๐Ÿ† Game state testing with fixtures
import unittest
import json
from unittest.mock import patch, mock_open

class GameState:
    def __init__(self):
        self.player = {"name": "", "score": 0, "level": 1, "lives": 3}
        self.achievements = []
        self.game_over = False
        
    def start_game(self, player_name):
        # ๐ŸŽฎ Initialize new game
        self.player["name"] = player_name
        self.achievements.append("๐ŸŒŸ First Steps")
        return f"Welcome {player_name}! Let's play! ๐ŸŽฎ"
    
    def score_points(self, points):
        # ๐ŸŽฏ Add points and check level up
        if self.game_over:
            return "Game Over! Start new game ๐Ÿ˜…"
            
        self.player["score"] += points
        
        # Level up every 100 points
        new_level = (self.player["score"] // 100) + 1
        if new_level > self.player["level"]:
            self.player["level"] = new_level
            self.achievements.append(f"๐Ÿ† Level {new_level} Master")
            return f"Level Up! Now level {new_level}! ๐ŸŽ‰"
        
        return f"Score: {self.player['score']} โœจ"
    
    def lose_life(self):
        # ๐Ÿ’” Lose a life
        self.player["lives"] -= 1
        if self.player["lives"] <= 0:
            self.game_over = True
            return "Game Over! ๐Ÿ’€"
        return f"Lives remaining: {'โค๏ธ' * self.player['lives']}"
    
    def save_game(self, filename):
        # ๐Ÿ’พ Save game state
        save_data = {
            "player": self.player,
            "achievements": self.achievements,
            "game_over": self.game_over
        }
        with open(filename, 'w') as f:
            json.dump(save_data, f)
        return "Game saved! ๐Ÿ’พ"

class TestGameState(unittest.TestCase):
    def setUp(self):
        # ๐ŸŽจ Fresh game state for each test
        print("\n๐ŸŽฎ Starting new test game...")
        self.game = GameState()
        self.test_player = "TestHero"
        self.save_file = "test_save.json"
        
    def tearDown(self):
        # ๐Ÿงน Clean up save files
        import os
        if os.path.exists(self.save_file):
            os.remove(self.save_file)
            print("๐Ÿ—‘๏ธ Cleaned up save file")
            
    def test_new_game_setup(self):
        # ๐ŸŒŸ Test game initialization
        result = self.game.start_game(self.test_player)
        self.assertEqual(self.game.player["name"], self.test_player)
        self.assertEqual(self.game.player["lives"], 3)
        self.assertIn("๐ŸŒŸ First Steps", self.game.achievements)
        
    def test_scoring_and_levels(self):
        # ๐Ÿ“ˆ Test scoring system
        self.game.start_game(self.test_player)
        
        # Score 50 points - no level up
        result = self.game.score_points(50)
        self.assertEqual(self.game.player["level"], 1)
        
        # Score 50 more - level up!
        result = self.game.score_points(50)
        self.assertIn("Level Up", result)
        self.assertEqual(self.game.player["level"], 2)
        self.assertIn("๐Ÿ† Level 2 Master", self.game.achievements)
        
    def test_life_system(self):
        # ๐Ÿ’” Test losing lives
        self.game.start_game(self.test_player)
        
        # Lose 2 lives
        self.game.lose_life()
        result = self.game.lose_life()
        self.assertEqual(result, "Lives remaining: โค๏ธ")
        
        # Lose final life
        result = self.game.lose_life()
        self.assertEqual(result, "Game Over! ๐Ÿ’€")
        self.assertTrue(self.game.game_over)
        
    def test_save_and_load(self):
        # ๐Ÿ’พ Test save game functionality
        self.game.start_game(self.test_player)
        self.game.score_points(150)
        
        # Save game
        result = self.game.save_game(self.save_file)
        self.assertEqual(result, "Game saved! ๐Ÿ’พ")
        
        # Verify save file
        with open(self.save_file, 'r') as f:
            saved_data = json.load(f)
        
        self.assertEqual(saved_data["player"]["score"], 150)
        self.assertEqual(saved_data["player"]["level"], 2)

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Class-level Fixtures: setUpClass and tearDownClass

When youโ€™re ready to level up, try these advanced patterns:

# ๐ŸŽฏ Advanced fixture patterns
import unittest
import sqlite3
import tempfile
import shutil

class TestDatabaseOperations(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        # ๐ŸŒŸ Runs ONCE before all tests in the class
        print("\n๐Ÿ—๏ธ Setting up test database environment...")
        cls.temp_dir = tempfile.mkdtemp()
        cls.db_path = f"{cls.temp_dir}/test.db"
        
        # Create test database schema
        conn = sqlite3.connect(cls.db_path)
        conn.execute('''
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                name TEXT,
                email TEXT,
                emoji TEXT
            )
        ''')
        conn.close()
        
    @classmethod
    def tearDownClass(cls):
        # ๐Ÿงน Runs ONCE after all tests complete
        print("\n๐Ÿ—‘๏ธ Cleaning up test environment...")
        shutil.rmtree(cls.temp_dir)
        
    def setUp(self):
        # ๐Ÿ”„ Fresh connection for each test
        self.conn = sqlite3.connect(self.db_path)
        self.cursor = self.conn.cursor()
        
    def tearDown(self):
        # ๐Ÿงน Clean data after each test
        self.cursor.execute("DELETE FROM users")
        self.conn.commit()
        self.conn.close()
        
    def test_user_creation(self):
        # ๐Ÿ‘ค Test creating users
        self.cursor.execute(
            "INSERT INTO users (name, email, emoji) VALUES (?, ?, ?)",
            ("Alice", "[email protected]", "๐Ÿ‘ฉโ€๐Ÿ’ป")
        )
        self.conn.commit()
        
        self.cursor.execute("SELECT COUNT(*) FROM users")
        count = self.cursor.fetchone()[0]
        self.assertEqual(count, 1)

๐Ÿ—๏ธ pytest Fixtures - The Modern Way

For the brave developers using pytest:

# ๐Ÿš€ pytest fixtures - more powerful!
import pytest
import tempfile
from pathlib import Path

@pytest.fixture
def game_state():
    # ๐ŸŽฎ Create game state fixture
    print("\n๐Ÿ”ง Creating game state...")
    state = {
        "player": {"name": "TestHero", "score": 0, "emoji": "๐Ÿฆธ"},
        "inventory": ["๐Ÿ—ก๏ธ Sword", "๐Ÿ›ก๏ธ Shield", "๐Ÿงช Potion"],
        "level": 1
    }
    yield state  # ๐ŸŽฏ This is where the test runs
    print("๐Ÿงน Cleaning up game state...")
    
@pytest.fixture
def temp_save_file():
    # ๐Ÿ“ Temporary file fixture
    with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json') as f:
        temp_path = f.name
    
    yield temp_path
    
    # Cleanup
    Path(temp_path).unlink(missing_ok=True)
    
@pytest.fixture(scope="session")
def test_database():
    # ๐Ÿ—„๏ธ Database fixture that lasts entire session
    print("\n๐Ÿ—๏ธ Creating test database...")
    db = TestDatabase()
    db.initialize()
    
    yield db
    
    print("\n๐Ÿ—‘๏ธ Destroying test database...")
    db.destroy()

# ๐ŸŽฏ Using pytest fixtures
def test_save_game(game_state, temp_save_file):
    # Game state and temp file are automatically provided!
    game_state["score"] = 100
    
    # Save to temp file
    import json
    with open(temp_save_file, 'w') as f:
        json.dump(game_state, f)
    
    # Verify save
    with open(temp_save_file, 'r') as f:
        loaded = json.load(f)
    
    assert loaded["score"] == 100
    print(f"โœ… Game saved successfully to {temp_save_file}")

def test_player_progress(game_state):
    # ๐Ÿ“ˆ Test with fixture
    initial_score = game_state["player"]["score"]
    game_state["player"]["score"] += 50
    
    assert game_state["player"]["score"] == initial_score + 50
    print(f"โœจ Player scored 50 points!")

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Shared Mutable State

# โŒ Wrong way - sharing mutable objects!
class BadTestExample(unittest.TestCase):
    shared_list = []  # ๐Ÿ˜ฐ This is shared between ALL tests!
    
    def test_one(self):
        self.shared_list.append(1)
        self.assertEqual(len(self.shared_list), 1)  # Passes first time
        
    def test_two(self):
        self.shared_list.append(2)
        self.assertEqual(len(self.shared_list), 1)  # ๐Ÿ’ฅ Fails! List has 2 items

# โœ… Correct way - fresh state in setUp!
class GoodTestExample(unittest.TestCase):
    def setUp(self):
        self.test_list = []  # ๐Ÿ›ก๏ธ Fresh list for each test
        
    def test_one(self):
        self.test_list.append(1)
        self.assertEqual(len(self.test_list), 1)  # โœ… Always passes
        
    def test_two(self):
        self.test_list.append(2)
        self.assertEqual(len(self.test_list), 1)  # โœ… Always passes

๐Ÿคฏ Pitfall 2: Forgetting Cleanup

# โŒ Dangerous - no cleanup!
class BadFileTest(unittest.TestCase):
    def setUp(self):
        self.test_file = "test_data.txt"
        with open(self.test_file, 'w') as f:
            f.write("test data")
    
    # ๐Ÿ’ฅ No tearDown - files accumulate!

# โœ… Safe - always clean up!
class GoodFileTest(unittest.TestCase):
    def setUp(self):
        self.test_file = "test_data.txt"
        with open(self.test_file, 'w') as f:
            f.write("test data")
    
    def tearDown(self):
        # ๐Ÿงน Always clean up resources
        import os
        try:
            os.remove(self.test_file)
        except FileNotFoundError:
            pass  # File already cleaned up

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Keep Fixtures Focused: Each fixture should have one clear purpose
  2. ๐Ÿ“ Fast Setup: Keep setUp methods quick to maintain test speed
  3. ๐Ÿ›ก๏ธ Isolated Tests: Each test should be independent
  4. ๐ŸŽจ Clear Names: Name fixtures to describe what they provide
  5. โœจ Fail-Safe Cleanup: tearDown should handle partial failures

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Library Management System Test Suite

Create a comprehensive test suite with fixtures:

๐Ÿ“‹ Requirements:

  • โœ… Book checkout/return system with due dates
  • ๐Ÿท๏ธ Member management with borrowing limits
  • ๐Ÿ‘ค Late fee calculation
  • ๐Ÿ“… Reservation system
  • ๐ŸŽจ Each book and member needs an emoji!

๐Ÿš€ Bonus Points:

  • Add database fixture for persistence
  • Implement search functionality tests
  • Create performance test fixtures

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
# ๐ŸŽฏ Library Management System Test Suite!
import unittest
from datetime import datetime, timedelta
from collections import defaultdict

class Book:
    def __init__(self, isbn, title, author, emoji="๐Ÿ“–"):
        self.isbn = isbn
        self.title = title
        self.author = author
        self.emoji = emoji
        self.available = True
        self.due_date = None
        self.borrowed_by = None
        
class Member:
    def __init__(self, member_id, name, emoji="๐Ÿ‘ค"):
        self.member_id = member_id
        self.name = name
        self.emoji = emoji
        self.books_borrowed = []
        self.late_fees = 0.0
        self.borrowing_limit = 3
        
class Library:
    def __init__(self):
        self.books = {}
        self.members = {}
        self.reservations = defaultdict(list)
        self.daily_fine = 0.50
        
    def add_book(self, book):
        self.books[book.isbn] = book
        return f"Added {book.emoji} {book.title}!"
        
    def register_member(self, member):
        self.members[member.member_id] = member
        return f"Welcome {member.emoji} {member.name}!"
        
    def checkout_book(self, member_id, isbn):
        member = self.members.get(member_id)
        book = self.books.get(isbn)
        
        if not member or not book:
            return "Invalid member or book! โŒ"
            
        if not book.available:
            return f"Book not available! Reserved by {len(self.reservations[isbn])} people ๐Ÿ“š"
            
        if len(member.books_borrowed) >= member.borrowing_limit:
            return f"Borrowing limit reached! Max: {member.borrowing_limit} ๐Ÿ“š"
            
        # Process checkout
        book.available = False
        book.borrowed_by = member_id
        book.due_date = datetime.now() + timedelta(days=14)
        member.books_borrowed.append(isbn)
        
        return f"โœ… {member.name} borrowed {book.emoji} {book.title}! Due: {book.due_date.strftime('%Y-%m-%d')}"
        
    def return_book(self, member_id, isbn):
        member = self.members.get(member_id)
        book = self.books.get(isbn)
        
        if not member or not book:
            return "Invalid member or book! โŒ"
            
        if book.borrowed_by != member_id:
            return "You didn't borrow this book! ๐Ÿค”"
            
        # Calculate late fees
        if datetime.now() > book.due_date:
            days_late = (datetime.now() - book.due_date).days
            fee = days_late * self.daily_fine
            member.late_fees += fee
            message = f"โš ๏ธ Late return! Fee: ${fee:.2f}"
        else:
            message = "โœ… Returned on time!"
            
        # Process return
        book.available = True
        book.borrowed_by = None
        book.due_date = None
        member.books_borrowed.remove(isbn)
        
        # Check reservations
        if self.reservations[isbn]:
            next_member = self.reservations[isbn].pop(0)
            message += f" ๐Ÿ“ข {self.members[next_member].name} is next in line!"
            
        return message

class TestLibraryManagement(unittest.TestCase):
    def setUp(self):
        # ๐Ÿ—๏ธ Create fresh library system
        print("\n๐Ÿ“š Setting up library test...")
        self.library = Library()
        
        # Add test books
        self.books = [
            Book("978-1", "Python Mastery", "Guido", "๐Ÿ"),
            Book("978-2", "Test Driven Dev", "Kent Beck", "๐Ÿงช"),
            Book("978-3", "Clean Code", "Uncle Bob", "โœจ")
        ]
        
        for book in self.books:
            self.library.add_book(book)
            
        # Add test members
        self.members = [
            Member("M001", "Alice", "๐Ÿ‘ฉโ€๐Ÿ’ป"),
            Member("M002", "Bob", "๐Ÿ‘จโ€๐Ÿ’ผ"),
            Member("M003", "Charlie", "๐Ÿ‘จโ€๐ŸŽ“")
        ]
        
        for member in self.members:
            self.library.register_member(member)
            
    def tearDown(self):
        # ๐Ÿงน Clean up
        print(f"๐Ÿ“Š Test complete. Books: {len(self.library.books)}, Members: {len(self.library.members)}")
        self.library = None
        
    def test_book_checkout(self):
        # ๐Ÿ“– Test normal checkout
        result = self.library.checkout_book("M001", "978-1")
        self.assertIn("โœ…", result)
        self.assertIn("Alice", result)
        self.assertFalse(self.library.books["978-1"].available)
        
    def test_borrowing_limit(self):
        # ๐Ÿ“š Test borrowing limit enforcement
        # Checkout 3 books (limit)
        for i in range(3):
            self.library.checkout_book("M001", f"978-{i+1}")
            
        # Try to checkout 4th book
        self.library.add_book(Book("978-4", "Extra Book", "Author", "๐Ÿ“•"))
        result = self.library.checkout_book("M001", "978-4")
        self.assertIn("limit reached", result)
        
    def test_late_return_fees(self):
        # ๐Ÿ’ฐ Test late fee calculation
        # Checkout book
        self.library.checkout_book("M001", "978-1")
        
        # Manually set due date to past
        book = self.library.books["978-1"]
        book.due_date = datetime.now() - timedelta(days=3)
        
        # Return late
        result = self.library.return_book("M001", "978-1")
        self.assertIn("Late return", result)
        self.assertEqual(self.library.members["M001"].late_fees, 1.50)  # 3 days * $0.50
        
    def test_reservation_system(self):
        # ๐Ÿ“‹ Test reservation queue
        # First member checks out
        self.library.checkout_book("M001", "978-1")
        
        # Second member tries to checkout (should fail)
        result = self.library.checkout_book("M002", "978-1")
        self.assertIn("not available", result)
        
        # Add to reservation
        self.library.reservations["978-1"].append("M002")
        self.library.reservations["978-1"].append("M003")
        
        # First member returns
        result = self.library.return_book("M001", "978-1")
        self.assertIn("Bob is next", result)
        
    def test_invalid_operations(self):
        # โŒ Test error handling
        # Invalid member
        result = self.library.checkout_book("M999", "978-1")
        self.assertIn("Invalid", result)
        
        # Invalid book
        result = self.library.checkout_book("M001", "978-999")
        self.assertIn("Invalid", result)
        
        # Return book not borrowed
        result = self.library.return_book("M002", "978-1")
        self.assertIn("didn't borrow", result)

if __name__ == "__main__":
    unittest.main()

๐ŸŽ“ Key Takeaways

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

  • โœ… Create test fixtures with confidence ๐Ÿ’ช
  • โœ… Avoid common testing mistakes that trip up beginners ๐Ÿ›ก๏ธ
  • โœ… Apply best practices in real test suites ๐ŸŽฏ
  • โœ… Debug test issues like a pro ๐Ÿ›
  • โœ… Build maintainable test suites with Python! ๐Ÿš€

Remember: Test fixtures are your friends, not your enemies! Theyโ€™re here to help you write better tests. ๐Ÿค

๐Ÿค Next Steps

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

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the exercises above
  2. ๐Ÿ—๏ธ Add fixtures to your existing test suites
  3. ๐Ÿ“š Move on to our next tutorial: Test Doubles and Mocking
  4. ๐ŸŒŸ Share your testing journey with others!

Remember: Every testing expert was once a beginner. Keep testing, keep learning, and most importantly, have fun! ๐Ÿš€


Happy testing! ๐ŸŽ‰๐Ÿš€โœจ