+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 283 of 365

๐Ÿ“˜ Test Coverage: Coverage.py

Master test coverage: coverage.py 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 coverage with Coverage.py! ๐ŸŽ‰ In this guide, weโ€™ll explore how to measure and improve your test coverage, ensuring your code is thoroughly tested and reliable.

Youโ€™ll discover how Coverage.py can transform your testing strategy, giving you confidence that your code works as expected. Whether youโ€™re building web applications ๐ŸŒ, data processing pipelines ๐Ÿ“Š, or Python libraries ๐Ÿ“š, understanding test coverage is essential for writing robust, maintainable code.

By the end of this tutorial, youโ€™ll feel confident using Coverage.py to measure, analyze, and improve your test coverage! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Test Coverage

๐Ÿค” What is Test Coverage?

Test coverage is like a safety net for your code ๐ŸŽช. Think of it as a map showing which parts of your code have been tested and which havenโ€™t. Itโ€™s like checking if youโ€™ve painted every wall in a house - you want to make sure no spots are missed!

In Python terms, test coverage measures the percentage of your code that gets executed when you run your tests. This means you can:

  • โœจ Identify untested code paths
  • ๐Ÿš€ Improve code reliability
  • ๐Ÿ›ก๏ธ Catch bugs before production

๐Ÿ’ก Why Use Coverage.py?

Hereโ€™s why developers love Coverage.py:

  1. Comprehensive Metrics ๐Ÿ“Š: See exactly which lines are tested
  2. Multiple Report Formats ๐Ÿ“‹: HTML, XML, JSON, and terminal output
  3. Branch Coverage ๐ŸŒณ: Track conditional logic testing
  4. Easy Integration ๐Ÿ”ง: Works with pytest, unittest, and more

Real-world example: Imagine building an e-commerce site ๐Ÿ›’. With Coverage.py, you can ensure all payment processing paths are tested, preventing costly bugs in production!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Installation and Setup

Letโ€™s start by installing Coverage.py:

# ๐Ÿ‘‹ Install Coverage.py
pip install coverage

# ๐ŸŽจ Or install with pytest plugin
pip install pytest-cov

๐ŸŽฏ Basic Usage

Hereโ€™s how to use Coverage.py with a simple example:

# ๐Ÿ“ calculator.py
class Calculator:
    """๐Ÿงฎ A simple calculator class"""
    
    def add(self, a, b):
        """โž• Add two numbers"""
        return a + b
    
    def subtract(self, a, b):
        """โž– Subtract b from a"""
        return a - b
    
    def multiply(self, a, b):
        """โœ–๏ธ Multiply two numbers"""
        return a * b
    
    def divide(self, a, b):
        """โž— Divide a by b"""
        if b == 0:
            raise ValueError("Cannot divide by zero! ๐Ÿšซ")
        return a / b

# ๐Ÿ“ test_calculator.py
import pytest
from calculator import Calculator

class TestCalculator:
    """๐Ÿงช Test our calculator"""
    
    def setup_method(self):
        """๐Ÿ—๏ธ Set up test calculator"""
        self.calc = Calculator()
    
    def test_add(self):
        """โœ… Test addition"""
        assert self.calc.add(2, 3) == 5
        assert self.calc.add(-1, 1) == 0
    
    def test_subtract(self):
        """โœ… Test subtraction"""
        assert self.calc.subtract(5, 3) == 2
        assert self.calc.subtract(0, 5) == -5

Now run with coverage:

# ๐Ÿš€ Run tests with coverage
coverage run -m pytest test_calculator.py

# ๐Ÿ“Š Generate coverage report
coverage report

# ๐ŸŽจ Generate HTML report
coverage html

๐Ÿ’ก Explanation: Notice how weโ€™re only testing add and subtract methods. Coverage.py will show us that multiply and divide are untested!

๐Ÿ’ก Practical Examples

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

Letโ€™s build a real-world example with comprehensive testing:

# ๐Ÿ“ order_processor.py
from datetime import datetime
from typing import List, Dict

class Order:
    """๐Ÿ›๏ธ Represents a customer order"""
    
    def __init__(self, order_id: str, customer_id: str):
        self.order_id = order_id
        self.customer_id = customer_id
        self.items: List[Dict] = []
        self.status = "pending"
        self.created_at = datetime.now()
        self.discount = 0.0
    
    def add_item(self, product_id: str, quantity: int, price: float):
        """โž• Add item to order"""
        if quantity <= 0:
            raise ValueError("Quantity must be positive! ๐Ÿ“ฆ")
        if price < 0:
            raise ValueError("Price cannot be negative! ๐Ÿ’ฐ")
        
        self.items.append({
            "product_id": product_id,
            "quantity": quantity,
            "price": price
        })
    
    def apply_discount(self, percentage: float):
        """๐ŸŽ Apply discount to order"""
        if not 0 <= percentage <= 100:
            raise ValueError("Discount must be between 0-100%! ๐Ÿท๏ธ")
        self.discount = percentage
    
    def calculate_total(self) -> float:
        """๐Ÿ’ฐ Calculate order total with discount"""
        subtotal = sum(item["quantity"] * item["price"] 
                      for item in self.items)
        discount_amount = subtotal * (self.discount / 100)
        return subtotal - discount_amount
    
    def process_payment(self, payment_amount: float) -> bool:
        """๐Ÿ’ณ Process payment for order"""
        total = self.calculate_total()
        
        if payment_amount < total:
            return False
        
        self.status = "paid"
        return True

# ๐Ÿ“ test_order_processor.py
import pytest
from order_processor import Order

class TestOrderProcessor:
    """๐Ÿงช Comprehensive order processing tests"""
    
    def test_order_creation(self):
        """โœ… Test order initialization"""
        order = Order("ORD123", "CUST456")
        assert order.order_id == "ORD123"
        assert order.customer_id == "CUST456"
        assert order.status == "pending"
        assert len(order.items) == 0
    
    def test_add_valid_item(self):
        """โœ… Test adding items"""
        order = Order("ORD123", "CUST456")
        order.add_item("PROD001", 2, 29.99)
        
        assert len(order.items) == 1
        assert order.items[0]["quantity"] == 2
    
    def test_add_invalid_item(self):
        """โŒ Test invalid item additions"""
        order = Order("ORD123", "CUST456")
        
        # Test negative quantity
        with pytest.raises(ValueError, match="Quantity must be positive"):
            order.add_item("PROD001", -1, 29.99)
        
        # Test negative price
        with pytest.raises(ValueError, match="Price cannot be negative"):
            order.add_item("PROD001", 1, -10.00)
    
    def test_apply_discount(self):
        """๐ŸŽ Test discount application"""
        order = Order("ORD123", "CUST456")
        order.add_item("PROD001", 2, 50.00)
        order.apply_discount(10)
        
        assert order.discount == 10
        assert order.calculate_total() == 90.00
    
    def test_invalid_discount(self):
        """โŒ Test invalid discounts"""
        order = Order("ORD123", "CUST456")
        
        with pytest.raises(ValueError, match="Discount must be between"):
            order.apply_discount(150)
    
    def test_process_payment_success(self):
        """๐Ÿ’ณ Test successful payment"""
        order = Order("ORD123", "CUST456")
        order.add_item("PROD001", 1, 100.00)
        
        result = order.process_payment(100.00)
        assert result is True
        assert order.status == "paid"
    
    def test_process_payment_insufficient(self):
        """โŒ Test insufficient payment"""
        order = Order("ORD123", "CUST456")
        order.add_item("PROD001", 1, 100.00)
        
        result = order.process_payment(50.00)
        assert result is False
        assert order.status == "pending"

Run coverage and see the results:

# ๐Ÿš€ Run with branch coverage
coverage run --branch -m pytest test_order_processor.py

# ๐Ÿ“Š Detailed report
coverage report -m

# ๐ŸŽจ HTML report with highlighting
coverage html

๐ŸŽฏ Try it yourself: Add tests for edge cases like empty orders or multiple discounts!

๐ŸŽฎ Example 2: Game Character Testing

Letโ€™s create a fun game character system with comprehensive coverage:

# ๐Ÿ“ game_character.py
import random
from typing import List, Optional

class Character:
    """๐ŸŽฎ RPG game character"""
    
    def __init__(self, name: str, character_class: str):
        self.name = name
        self.character_class = character_class
        self.level = 1
        self.health = 100
        self.max_health = 100
        self.experience = 0
        self.skills: List[str] = []
        self.inventory: List[str] = []
        
        # ๐ŸŽฏ Set class-specific attributes
        self._initialize_class()
    
    def _initialize_class(self):
        """๐Ÿ—๏ธ Initialize class-specific attributes"""
        if self.character_class == "warrior":
            self.attack = 15
            self.defense = 10
            self.skills = ["โš”๏ธ Slash", "๐Ÿ›ก๏ธ Block"]
        elif self.character_class == "mage":
            self.attack = 20
            self.defense = 5
            self.skills = ["๐Ÿ”ฅ Fireball", "โ„๏ธ Ice Shield"]
        elif self.character_class == "rogue":
            self.attack = 12
            self.defense = 8
            self.skills = ["๐Ÿ—ก๏ธ Sneak Attack", "๐Ÿ’จ Dodge"]
        else:
            raise ValueError(f"Unknown class: {self.character_class}")
    
    def take_damage(self, damage: int):
        """๐Ÿ’” Take damage"""
        actual_damage = max(0, damage - self.defense)
        self.health = max(0, self.health - actual_damage)
        return actual_damage
    
    def heal(self, amount: int):
        """๐Ÿ’š Heal character"""
        self.health = min(self.max_health, self.health + amount)
    
    def gain_experience(self, exp: int):
        """โญ Gain experience and level up"""
        self.experience += exp
        
        # Level up every 100 exp
        while self.experience >= self.level * 100:
            self.level_up()
    
    def level_up(self):
        """๐Ÿ“ˆ Level up character"""
        self.level += 1
        self.max_health += 10
        self.health = self.max_health
        self.attack += 2
        self.defense += 1
        
        # ๐ŸŽ Learn new skill every 3 levels
        if self.level % 3 == 0:
            self.learn_skill()
    
    def learn_skill(self):
        """โœจ Learn a new skill"""
        new_skills = {
            "warrior": ["๐Ÿ’ช Power Strike", "๐Ÿ”จ Earthquake"],
            "mage": ["โšก Lightning", "๐ŸŒช๏ธ Tornado"],
            "rogue": ["๐ŸŽฏ Precision Strike", "๐ŸŒซ๏ธ Smoke Bomb"]
        }
        
        available = [s for s in new_skills.get(self.character_class, []) 
                    if s not in self.skills]
        if available:
            self.skills.append(random.choice(available))
    
    def is_alive(self) -> bool:
        """โค๏ธ Check if character is alive"""
        return self.health > 0

# ๐Ÿ“ test_game_character.py  
import pytest
from unittest.mock import patch
from game_character import Character

class TestGameCharacter:
    """๐Ÿงช Test our game character system"""
    
    @pytest.mark.parametrize("char_class,expected_attack,expected_defense", [
        ("warrior", 15, 10),
        ("mage", 20, 5),
        ("rogue", 12, 8)
    ])
    def test_character_creation(self, char_class, expected_attack, expected_defense):
        """โœ… Test character initialization"""
        char = Character("Hero", char_class)
        assert char.name == "Hero"
        assert char.level == 1
        assert char.attack == expected_attack
        assert char.defense == expected_defense
    
    def test_invalid_class(self):
        """โŒ Test invalid character class"""
        with pytest.raises(ValueError, match="Unknown class"):
            Character("Hero", "ninja")
    
    def test_take_damage(self):
        """๐Ÿ’” Test damage calculation"""
        warrior = Character("Tank", "warrior")
        
        # Damage reduced by defense
        damage_taken = warrior.take_damage(20)
        assert damage_taken == 10  # 20 - 10 defense
        assert warrior.health == 90
        
        # Minimum damage is 0
        damage_taken = warrior.take_damage(5)
        assert damage_taken == 0
        assert warrior.health == 90
    
    def test_heal(self):
        """๐Ÿ’š Test healing"""
        mage = Character("Wizard", "mage")
        mage.health = 50
        
        # Normal healing
        mage.heal(30)
        assert mage.health == 80
        
        # Can't exceed max health
        mage.heal(50)
        assert mage.health == 100
    
    def test_level_up(self):
        """๐Ÿ“ˆ Test leveling system"""
        rogue = Character("Shadow", "rogue")
        initial_attack = rogue.attack
        
        # Gain experience and level up
        rogue.gain_experience(100)
        
        assert rogue.level == 2
        assert rogue.max_health == 110
        assert rogue.health == 110  # Fully healed on level up
        assert rogue.attack == initial_attack + 2
    
    @patch('random.choice')
    def test_learn_skill(self, mock_choice):
        """โœจ Test skill learning"""
        mock_choice.return_value = "๐Ÿ’ช Power Strike"
        
        warrior = Character("Knight", "warrior")
        # Level up to 3 to learn skill
        warrior.gain_experience(200)  # Level 3
        
        assert "๐Ÿ’ช Power Strike" in warrior.skills
    
    def test_is_alive(self):
        """โค๏ธ Test alive status"""
        char = Character("Hero", "warrior")
        
        assert char.is_alive() is True
        
        char.health = 0
        assert char.is_alive() is False

# ๐Ÿ“ .coveragerc - Configuration file
[run]
source = .
omit = 
    */tests/*
    */test_*
    setup.py
    
[report]
precision = 2
show_missing = True
skip_covered = False

[html]
directory = htmlcov

[xml]
output = coverage.xml

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Branch Coverage

Branch coverage tracks whether both paths of conditional statements are tested:

# ๐Ÿ“ advanced_coverage.py
def calculate_grade(score: int) -> str:
    """๐ŸŽ“ Calculate letter grade with complex logic"""
    if score < 0 or score > 100:
        raise ValueError("Score must be 0-100! ๐Ÿ“Š")
    
    # ๐ŸŽฏ Multiple branches to test
    if score >= 90:
        if score >= 97:
            return "A+"
        elif score >= 93:
            return "A"
        else:
            return "A-"
    elif score >= 80:
        if score >= 87:
            return "B+"
        elif score >= 83:
            return "B"
        else:
            return "B-"
    elif score >= 70:
        return "C"
    elif score >= 60:
        return "D"
    else:
        return "F"

# Test with branch coverage
# coverage run --branch -m pytest

๐Ÿ—๏ธ Coverage Configuration

Create a comprehensive .coveragerc file:

# ๐Ÿ“ .coveragerc
[run]
branch = True
source = .
omit = 
    */site-packages/*
    */tests/*
    setup.py
    */migrations/*
    */venv/*
    
[report]
# Precision for coverage percentages
precision = 2

# Show lines not covered
show_missing = True

# Don't show files with 100% coverage
skip_covered = False

# Exclude lines from coverage
exclude_lines =
    # Don't complain about missing debug-only code:
    def __repr__
    if self\.debug
    
    # Don't complain if tests don't hit defensive assertion code:
    raise AssertionError
    raise NotImplementedError
    
    # Don't complain if non-runnable code isn't run:
    if 0:
    if __name__ == .__main__.:
    
    # Don't complain about abstract methods
    @abstract

[html]
directory = htmlcov
title = My Project Coverage

[xml]
output = coverage.xml

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Testing Only Happy Paths

# โŒ Wrong - only testing success cases
def test_divide_only_success():
    calc = Calculator()
    assert calc.divide(10, 2) == 5

# โœ… Correct - test edge cases too!
def test_divide_comprehensive():
    calc = Calculator()
    
    # Happy path
    assert calc.divide(10, 2) == 5
    
    # Edge cases
    assert calc.divide(0, 5) == 0
    assert calc.divide(-10, 2) == -5
    
    # Error case
    with pytest.raises(ValueError):
        calc.divide(10, 0)

๐Ÿคฏ Pitfall 2: Ignoring Branch Coverage

# โŒ Incomplete - missing branch coverage
def test_process_incomplete():
    result = process_data([1, 2, 3])
    assert result is not None

# โœ… Complete - all branches covered
def test_process_comprehensive():
    # Test with data
    result = process_data([1, 2, 3])
    assert result == 6
    
    # Test empty list branch
    result = process_data([])
    assert result is None
    
    # Test None input branch
    result = process_data(None)
    assert result is None

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Aim for 80%+ Coverage: But remember, 100% isnโ€™t always necessary
  2. ๐Ÿ“Š Use Branch Coverage: Enable --branch to catch conditional logic
  3. ๐Ÿšซ Exclude Appropriately: Donโ€™t test auto-generated or third-party code
  4. ๐Ÿ”„ Continuous Integration: Run coverage in CI/CD pipelines
  5. ๐Ÿ“ˆ Track Trends: Monitor coverage over time

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Banking System with Full Coverage

Create a banking system with comprehensive test coverage:

๐Ÿ“‹ Requirements:

  • โœ… Account creation with initial balance
  • ๐Ÿ’ฐ Deposit and withdrawal operations
  • ๐Ÿ”„ Transfer between accounts
  • ๐Ÿ“Š Transaction history
  • ๐ŸŽฏ Interest calculation
  • ๐Ÿ›ก๏ธ Overdraft protection

๐Ÿš€ Bonus Points:

  • Achieve 95%+ test coverage
  • Include branch coverage
  • Test all error conditions
  • Add performance benchmarks

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
# ๐Ÿ“ banking_system.py
from datetime import datetime
from typing import List, Optional
from dataclasses import dataclass

@dataclass
class Transaction:
    """๐Ÿ’ณ Represents a bank transaction"""
    timestamp: datetime
    type: str  # deposit, withdrawal, transfer
    amount: float
    balance_after: float
    description: str

class BankAccount:
    """๐Ÿฆ Bank account with full features"""
    
    def __init__(self, account_number: str, owner: str, 
                 initial_balance: float = 0.0):
        if initial_balance < 0:
            raise ValueError("Initial balance cannot be negative! ๐Ÿ’ธ")
        
        self.account_number = account_number
        self.owner = owner
        self.balance = initial_balance
        self.transactions: List[Transaction] = []
        self.overdraft_limit = 0.0
        self.interest_rate = 0.02  # 2% annual
        
        if initial_balance > 0:
            self._add_transaction("deposit", initial_balance, 
                                "Initial deposit ๐ŸŽ‰")
    
    def deposit(self, amount: float) -> float:
        """๐Ÿ’ฐ Deposit money"""
        if amount <= 0:
            raise ValueError("Deposit amount must be positive! ๐Ÿ’ต")
        
        self.balance += amount
        self._add_transaction("deposit", amount, "Deposit ๐Ÿ’ฐ")
        return self.balance
    
    def withdraw(self, amount: float) -> float:
        """๐Ÿ’ธ Withdraw money"""
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive! ๐Ÿ’ต")
        
        max_withdrawal = self.balance + self.overdraft_limit
        if amount > max_withdrawal:
            raise ValueError(f"Insufficient funds! Max: ${max_withdrawal:.2f} ๐Ÿšซ")
        
        self.balance -= amount
        self._add_transaction("withdrawal", amount, "Withdrawal ๐Ÿ’ธ")
        return self.balance
    
    def transfer(self, recipient: 'BankAccount', amount: float):
        """๐Ÿ”„ Transfer to another account"""
        if recipient == self:
            raise ValueError("Cannot transfer to same account! ๐Ÿ”„")
        
        # Withdraw from sender
        self.withdraw(amount)
        
        # Deposit to recipient
        recipient.deposit(amount)
        
        # Update transaction descriptions
        self.transactions[-1].description = f"Transfer to {recipient.account_number} ๐Ÿ“ค"
        recipient.transactions[-1].description = f"Transfer from {self.account_number} ๐Ÿ“ฅ"
    
    def calculate_interest(self) -> float:
        """๐Ÿ“ˆ Calculate monthly interest"""
        if self.balance <= 0:
            return 0.0
        
        monthly_rate = self.interest_rate / 12
        interest = self.balance * monthly_rate
        
        self.balance += interest
        self._add_transaction("interest", interest, "Monthly interest ๐Ÿ“ˆ")
        return interest
    
    def get_statement(self, limit: Optional[int] = None) -> List[Transaction]:
        """๐Ÿ“‹ Get transaction history"""
        if limit:
            return self.transactions[-limit:]
        return self.transactions.copy()
    
    def set_overdraft_limit(self, limit: float):
        """๐Ÿ›ก๏ธ Set overdraft protection limit"""
        if limit < 0:
            raise ValueError("Overdraft limit cannot be negative! ๐Ÿšซ")
        self.overdraft_limit = limit
    
    def _add_transaction(self, trans_type: str, amount: float, 
                        description: str):
        """๐Ÿ“ Record transaction"""
        transaction = Transaction(
            timestamp=datetime.now(),
            type=trans_type,
            amount=amount,
            balance_after=self.balance,
            description=description
        )
        self.transactions.append(transaction)

# ๐Ÿ“ test_banking_system.py
import pytest
from banking_system import BankAccount, Transaction

class TestBankingSystem:
    """๐Ÿงช Comprehensive banking system tests"""
    
    def test_account_creation(self):
        """โœ… Test account initialization"""
        # With initial balance
        account = BankAccount("ACC001", "Alice", 1000.0)
        assert account.balance == 1000.0
        assert len(account.transactions) == 1
        
        # Without initial balance
        account2 = BankAccount("ACC002", "Bob")
        assert account2.balance == 0.0
        assert len(account2.transactions) == 0
    
    def test_invalid_initial_balance(self):
        """โŒ Test negative initial balance"""
        with pytest.raises(ValueError, match="Initial balance cannot be negative"):
            BankAccount("ACC001", "Alice", -100.0)
    
    def test_deposit(self):
        """๐Ÿ’ฐ Test deposits"""
        account = BankAccount("ACC001", "Alice")
        
        # Valid deposit
        new_balance = account.deposit(500.0)
        assert new_balance == 500.0
        assert len(account.transactions) == 1
        
        # Multiple deposits
        account.deposit(250.0)
        assert account.balance == 750.0
    
    def test_invalid_deposit(self):
        """โŒ Test invalid deposits"""
        account = BankAccount("ACC001", "Alice")
        
        with pytest.raises(ValueError, match="Deposit amount must be positive"):
            account.deposit(0)
        
        with pytest.raises(ValueError, match="Deposit amount must be positive"):
            account.deposit(-50)
    
    def test_withdrawal(self):
        """๐Ÿ’ธ Test withdrawals"""
        account = BankAccount("ACC001", "Alice", 1000.0)
        
        # Valid withdrawal
        new_balance = account.withdraw(300.0)
        assert new_balance == 700.0
        
        # Withdraw remaining
        account.withdraw(700.0)
        assert account.balance == 0.0
    
    def test_overdraft_protection(self):
        """๐Ÿ›ก๏ธ Test overdraft"""
        account = BankAccount("ACC001", "Alice", 100.0)
        account.set_overdraft_limit(50.0)
        
        # Within overdraft limit
        account.withdraw(140.0)
        assert account.balance == -40.0
        
        # Exceeding overdraft
        with pytest.raises(ValueError, match="Insufficient funds"):
            account.withdraw(20.0)
    
    def test_transfer(self):
        """๐Ÿ”„ Test transfers"""
        sender = BankAccount("ACC001", "Alice", 1000.0)
        recipient = BankAccount("ACC002", "Bob", 500.0)
        
        sender.transfer(recipient, 300.0)
        
        assert sender.balance == 700.0
        assert recipient.balance == 800.0
        
        # Check transaction descriptions
        assert "Transfer to ACC002" in sender.transactions[-1].description
        assert "Transfer from ACC001" in recipient.transactions[-1].description
    
    def test_self_transfer(self):
        """โŒ Test self-transfer"""
        account = BankAccount("ACC001", "Alice", 1000.0)
        
        with pytest.raises(ValueError, match="Cannot transfer to same account"):
            account.transfer(account, 100.0)
    
    def test_interest_calculation(self):
        """๐Ÿ“ˆ Test interest"""
        account = BankAccount("ACC001", "Alice", 1000.0)
        
        interest = account.calculate_interest()
        expected_interest = 1000.0 * 0.02 / 12
        
        assert abs(interest - expected_interest) < 0.01
        assert account.balance > 1000.0
        
        # No interest on zero balance
        empty_account = BankAccount("ACC002", "Bob")
        assert empty_account.calculate_interest() == 0.0
    
    def test_statement(self):
        """๐Ÿ“‹ Test statement generation"""
        account = BankAccount("ACC001", "Alice", 1000.0)
        account.deposit(500.0)
        account.withdraw(200.0)
        
        # Full statement
        full_statement = account.get_statement()
        assert len(full_statement) == 3
        
        # Limited statement
        limited_statement = account.get_statement(limit=2)
        assert len(limited_statement) == 2
        assert limited_statement[0].type == "deposit"
        assert limited_statement[1].type == "withdrawal"

# Run with coverage
# coverage run --branch -m pytest test_banking_system.py -v
# coverage report -m
# coverage html

๐ŸŽ“ Key Takeaways

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

  • โœ… Install and configure Coverage.py with confidence ๐Ÿ’ช
  • โœ… Measure test coverage for your Python projects ๐Ÿ“Š
  • โœ… Identify untested code and improve coverage ๐ŸŽฏ
  • โœ… Use branch coverage to catch all code paths ๐ŸŒณ
  • โœ… Generate beautiful HTML reports to share with your team ๐ŸŽจ

Remember: High test coverage doesnโ€™t guarantee bug-free code, but itโ€™s a powerful tool for building reliable software! ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered test coverage with Coverage.py!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the banking system exercise above
  2. ๐Ÿ—๏ธ Add coverage measurement to your existing projects
  3. ๐Ÿ“š Move on to our next tutorial on debugging techniques
  4. ๐ŸŒŸ Set up coverage tracking in your CI/CD pipeline!

Remember: Good test coverage is like a safety net for your code. Keep testing, keep measuring, and most importantly, have fun building reliable software! ๐Ÿš€


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