Assignment 7-A

Testing & Debugging

Build a robust Banking System with comprehensive error handling, logging, and test coverage. Master exception handling, custom exceptions, the logging module, unittest, pytest, and test-driven development (TDD) practices.

5-7 hours
Challenging
175 Points
Submit Assignment
What You'll Practice
  • Handle exceptions gracefully
  • Create custom exception classes
  • Implement logging throughout
  • Write comprehensive unit tests
  • Apply TDD methodology
Contents
01

Assignment Overview

In this assignment, you will build a Banking System that demonstrates professional-level error handling, debugging, and testing practices. This project requires you to apply ALL concepts from Module 7: exception handling, custom exceptions, logging, unittest, pytest, and test-driven development.

Testing Requirement: You must achieve at least 90% test coverage using pytest-cov. All functions must have corresponding unit tests. Tests must pass before submission.
Skills Applied: This assignment tests your understanding of Exception Handling (7.1), Debugging & Logging (7.2), and Testing (7.3) from Module 7.
Exception Handling (7.1)

Try/except/else/finally, multiple exceptions, raising, custom exceptions

Debugging & Logging (7.2)

Print debugging, pdb debugger, logging module with levels

Testing (7.3)

unittest, pytest, fixtures, mocking, TDD, coverage

Ready to submit? Already completed the assignment? Submit your work now!
Submit Now
02

The Scenario

SecureBank Python System

You have been hired as a Quality Assurance Engineer at SecureBank, a digital bank that needs a rock-solid transaction system. The CTO has given you this task:

"We need a banking system that never fails silently. Every error must be caught, logged, and handled gracefully. The system must have comprehensive test coverage - we can't afford bugs in production. Use TDD to build the core features, implement custom exceptions for banking errors, and set up proper logging for audit trails."

Your Task

Create a Python banking system with robust error handling, comprehensive logging, and thorough test coverage. Your code must demonstrate proficiency in all error handling and testing concepts taught in Module 7.

03

Requirements

Your project must implement ALL of the following requirements. Each requirement is mandatory and will be tested individually.

1
Custom Exception Hierarchy (7.1)

Create a hierarchy of custom banking exceptions:

  • BankingError - Base exception for all banking errors
  • InsufficientFundsError - When withdrawal exceeds balance
  • InvalidAmountError - When amount is negative or zero
  • AccountNotFoundError - When account doesn't exist
  • TransactionLimitError - When daily limit exceeded
  • AuthenticationError - When PIN/password is wrong
class BankingError(Exception):
    """Base exception for all banking-related errors."""
    
    def __init__(self, message: str, error_code: str = None):
        self.message = message
        self.error_code = error_code
        super().__init__(self.message)
    
    def __str__(self):
        if self.error_code:
            return f"[{self.error_code}] {self.message}"
        return self.message


class InsufficientFundsError(BankingError):
    """Raised when withdrawal amount exceeds available balance."""
    
    def __init__(self, balance: float, amount: float):
        self.balance = balance
        self.amount = amount
        message = f"Insufficient funds: balance ₹{balance:.2f}, requested ₹{amount:.2f}"
        super().__init__(message, "ERR_INSUFFICIENT_FUNDS")


class InvalidAmountError(BankingError):
    """Raised when transaction amount is invalid (negative or zero)."""
    
    def __init__(self, amount: float):
        self.amount = amount
        message = f"Invalid amount: ₹{amount:.2f}. Amount must be positive."
        super().__init__(message, "ERR_INVALID_AMOUNT")


# Implement remaining custom exceptions...
2
Account Class with Exception Handling (7.1)

Create an Account class that uses try/except/else/finally:

  • All methods must have proper exception handling
  • Use else block for success operations
  • Use finally for cleanup/logging
  • Raise appropriate custom exceptions
  • Never let exceptions propagate unhandled
class Account:
    """Bank account with robust error handling."""
    
    DAILY_WITHDRAWAL_LIMIT = 50000.0
    
    def __init__(self, account_number: str, holder_name: str, 
                 pin: str, initial_balance: float = 0.0):
        self._account_number = account_number
        self._holder_name = holder_name
        self._pin = pin
        self._balance = initial_balance
        self._daily_withdrawn = 0.0
        self._transaction_history = []
    
    def withdraw(self, amount: float, pin: str) -> float:
        """
        Withdraw money from account.
        
        Raises:
            AuthenticationError: If PIN is incorrect
            InvalidAmountError: If amount <= 0
            InsufficientFundsError: If amount > balance
            TransactionLimitError: If daily limit exceeded
        """
        try:
            # Validate PIN
            if pin != self._pin:
                raise AuthenticationError("Invalid PIN")
            
            # Validate amount
            if amount <= 0:
                raise InvalidAmountError(amount)
            
            # Check balance
            if amount > self._balance:
                raise InsufficientFundsError(self._balance, amount)
            
            # Check daily limit
            if self._daily_withdrawn + amount > self.DAILY_WITHDRAWAL_LIMIT:
                raise TransactionLimitError(
                    self.DAILY_WITHDRAWAL_LIMIT, 
                    self._daily_withdrawn
                )
        
        except BankingError:
            # Re-raise banking errors after logging
            raise
        
        else:
            # Success - perform withdrawal
            self._balance -= amount
            self._daily_withdrawn += amount
            self._record_transaction("WITHDRAWAL", amount)
            return self._balance
        
        finally:
            # Always log the attempt
            logger.info(f"Withdrawal attempt on {self._account_number}")
3
Multiple Exception Handling (7.1)

Implement a transfer() method that handles multiple exceptions:

  • Handle multiple exception types in one except block
  • Use exception chaining with raise ... from
  • Implement proper rollback on failure
  • Log all exceptions with traceback
def transfer(self, to_account: 'Account', amount: float, pin: str) -> bool:
    """
    Transfer money to another account.
    Handles multiple exceptions and implements rollback.
    """
    try:
        # Attempt withdrawal from this account
        self.withdraw(amount, pin)
        
        try:
            # Attempt deposit to target account
            to_account.deposit(amount)
        except (InvalidAmountError, AccountNotFoundError) as e:
            # Rollback: restore balance to this account
            self._balance += amount
            self._daily_withdrawn -= amount
            raise TransferError(
                f"Transfer failed, funds restored: {e}"
            ) from e
    
    except (InsufficientFundsError, TransactionLimitError, 
            AuthenticationError) as e:
        logger.error(f"Transfer failed: {e}")
        raise
    
    except Exception as e:
        # Catch any unexpected errors
        logger.exception("Unexpected error during transfer")
        raise BankingError(f"Transfer failed: {str(e)}") from e
    
    else:
        logger.info(f"Transfer successful: ₹{amount}")
        return True
4
Logging Configuration (7.2)

Set up comprehensive logging with multiple handlers:

  • Configure root logger with appropriate level
  • File handler for all logs (DEBUG level)
  • Console handler for warnings and above
  • Separate file for error logs only
  • Use proper log formatting with timestamps
import logging
import logging.handlers
from datetime import datetime

def setup_logging():
    """Configure logging for the banking system."""
    
    # Create logger
    logger = logging.getLogger('banking')
    logger.setLevel(logging.DEBUG)
    
    # Create formatters
    detailed_formatter = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - '
        '%(filename)s:%(lineno)d - %(message)s'
    )
    simple_formatter = logging.Formatter(
        '%(levelname)s - %(message)s'
    )
    
    # File handler - all logs
    file_handler = logging.FileHandler('banking.log')
    file_handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(detailed_formatter)
    
    # Error file handler - errors only
    error_handler = logging.FileHandler('banking_errors.log')
    error_handler.setLevel(logging.ERROR)
    error_handler.setFormatter(detailed_formatter)
    
    # Console handler - warnings and above
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.WARNING)
    console_handler.setFormatter(simple_formatter)
    
    # Add handlers to logger
    logger.addHandler(file_handler)
    logger.addHandler(error_handler)
    logger.addHandler(console_handler)
    
    return logger

logger = setup_logging()
5
Logging Throughout the Application (7.2)

Add logging statements at appropriate levels:

  • DEBUG: Detailed diagnostic information
  • INFO: Successful operations, transactions
  • WARNING: Low balance alerts, unusual activity
  • ERROR: Failed operations, caught exceptions
  • CRITICAL: Security breaches, system failures
class Bank:
    """Bank system with comprehensive logging."""
    
    def __init__(self, name: str):
        self._name = name
        self._accounts = {}
        logger.info(f"Bank '{name}' initialized")
    
    def create_account(self, holder_name: str, initial_deposit: float,
                      pin: str) -> Account:
        """Create a new bank account."""
        logger.debug(f"Creating account for {holder_name}")
        
        try:
            if initial_deposit < 1000:
                logger.warning(
                    f"Low initial deposit: ₹{initial_deposit}"
                )
            
            account_number = self._generate_account_number()
            account = Account(account_number, holder_name, pin, 
                            initial_deposit)
            self._accounts[account_number] = account
            
            logger.info(
                f"Account created: {account_number} for {holder_name}"
            )
            return account
            
        except Exception as e:
            logger.error(f"Failed to create account: {e}")
            raise
    
    def authenticate(self, account_number: str, pin: str) -> bool:
        """Authenticate user with account number and PIN."""
        logger.debug(f"Authentication attempt for {account_number}")
        
        if account_number not in self._accounts:
            logger.warning(f"Auth failed: account {account_number} not found")
            return False
        
        account = self._accounts[account_number]
        if account._pin != pin:
            logger.critical(
                f"SECURITY: Failed PIN attempt for {account_number}"
            )
            return False
        
        logger.info(f"Authentication successful: {account_number}")
        return True
6
Unit Tests with unittest (7.3)

Write comprehensive tests using the unittest framework:

  • Test class for each main class (Account, Bank)
  • setUp() and tearDown() methods for test fixtures
  • Test both success and failure cases
  • Test exception raising with assertRaises
  • Use subTest for parameterized tests
import unittest
from banking import Account, Bank
from exceptions import (
    InsufficientFundsError, InvalidAmountError,
    AuthenticationError, TransactionLimitError
)


class TestAccount(unittest.TestCase):
    """Unit tests for Account class using unittest."""
    
    def setUp(self):
        """Set up test fixtures."""
        self.account = Account(
            account_number="ACC001",
            holder_name="John Doe",
            pin="1234",
            initial_balance=10000.0
        )
    
    def tearDown(self):
        """Clean up after tests."""
        self.account = None
    
    def test_deposit_valid_amount(self):
        """Test depositing a valid amount."""
        new_balance = self.account.deposit(5000.0)
        self.assertEqual(new_balance, 15000.0)
    
    def test_deposit_invalid_amount_raises_error(self):
        """Test that depositing invalid amount raises exception."""
        with self.assertRaises(InvalidAmountError) as context:
            self.account.deposit(-100.0)
        self.assertIn("Invalid amount", str(context.exception))
    
    def test_withdraw_with_wrong_pin(self):
        """Test withdrawal with incorrect PIN."""
        with self.assertRaises(AuthenticationError):
            self.account.withdraw(1000.0, "wrong")
    
    def test_withdraw_insufficient_funds(self):
        """Test withdrawal exceeding balance."""
        with self.assertRaises(InsufficientFundsError) as context:
            self.account.withdraw(50000.0, "1234")
        self.assertEqual(context.exception.balance, 10000.0)
        self.assertEqual(context.exception.amount, 50000.0)
    
    def test_multiple_amounts(self):
        """Test deposits with multiple amounts using subTest."""
        amounts = [100, 500, 1000, 5000]
        for amount in amounts:
            with self.subTest(amount=amount):
                initial = self.account.balance
                self.account.deposit(amount)
                self.assertEqual(
                    self.account.balance, 
                    initial + amount
                )


if __name__ == '__main__':
    unittest.main()
7
Tests with pytest (7.3)

Write tests using pytest with its features:

  • Use @pytest.fixture for test setup
  • Use @pytest.mark.parametrize for multiple test cases
  • Test exceptions with pytest.raises
  • Use markers for test categorization
  • Write conftest.py for shared fixtures
import pytest
from banking import Account, Bank
from exceptions import *


# conftest.py - shared fixtures
@pytest.fixture
def sample_account():
    """Fixture providing a sample account."""
    return Account(
        account_number="ACC001",
        holder_name="John Doe",
        pin="1234",
        initial_balance=10000.0
    )


@pytest.fixture
def bank_with_accounts():
    """Fixture providing a bank with multiple accounts."""
    bank = Bank("Test Bank")
    bank.create_account("Alice", 5000.0, "1111")
    bank.create_account("Bob", 10000.0, "2222")
    return bank


# test_account_pytest.py
class TestAccountPytest:
    """Pytest tests for Account class."""
    
    def test_deposit_increases_balance(self, sample_account):
        """Test that deposit increases balance correctly."""
        sample_account.deposit(5000.0)
        assert sample_account.balance == 15000.0
    
    @pytest.mark.parametrize("amount,expected", [
        (100, 10100),
        (500, 10500),
        (1000, 11000),
        (0.01, 10000.01),
    ])
    def test_deposit_various_amounts(self, sample_account, amount, expected):
        """Test deposits with various amounts."""
        sample_account.deposit(amount)
        assert sample_account.balance == pytest.approx(expected, rel=1e-2)
    
    @pytest.mark.parametrize("invalid_amount", [-100, -1, 0, -0.01])
    def test_deposit_invalid_amounts(self, sample_account, invalid_amount):
        """Test that invalid amounts raise InvalidAmountError."""
        with pytest.raises(InvalidAmountError):
            sample_account.deposit(invalid_amount)
    
    @pytest.mark.slow
    def test_daily_limit_enforcement(self, sample_account):
        """Test that daily withdrawal limit is enforced."""
        # Withdraw up to limit
        sample_account.withdraw(50000.0, "1234")
        
        # Next withdrawal should fail
        with pytest.raises(TransactionLimitError):
            sample_account.withdraw(1.0, "1234")
8
Mocking and Patching (7.3)

Use mocking to isolate units under test:

  • Mock external dependencies (logging, file I/O)
  • Use unittest.mock.patch decorator
  • Mock datetime for time-dependent tests
  • Verify mock calls with assert_called_with
from unittest.mock import Mock, patch, MagicMock
from datetime import datetime


class TestBankMocking:
    """Tests using mocking and patching."""
    
    @patch('banking.logger')
    def test_logging_on_deposit(self, mock_logger, sample_account):
        """Test that deposit logs correctly."""
        sample_account.deposit(1000.0)
        mock_logger.info.assert_called()
    
    @patch('banking.datetime')
    def test_transaction_timestamp(self, mock_datetime, sample_account):
        """Test transaction records correct timestamp."""
        mock_datetime.now.return_value = datetime(2026, 1, 22, 10, 30)
        
        sample_account.deposit(1000.0)
        
        last_txn = sample_account.transaction_history[-1]
        assert last_txn['timestamp'] == datetime(2026, 1, 22, 10, 30)
    
    def test_transfer_with_mock_account(self, sample_account):
        """Test transfer using a mock target account."""
        mock_target = Mock(spec=Account)
        mock_target.deposit.return_value = 5000.0
        
        sample_account.transfer(mock_target, 1000.0, "1234")
        
        mock_target.deposit.assert_called_once_with(1000.0)
    
    @patch('banking.Bank._generate_account_number')
    def test_account_number_generation(self, mock_gen, bank_with_accounts):
        """Test that account number generator is called."""
        mock_gen.return_value = "ACC999"
        
        account = bank_with_accounts.create_account("Test", 1000, "0000")
        
        assert account.account_number == "ACC999"
        mock_gen.assert_called_once()
9
Test-Driven Development (TDD) (7.3)

Implement a new feature using TDD methodology:

  • Feature: Interest calculation for savings accounts
  • Write failing tests FIRST
  • Write minimum code to pass tests
  • Refactor while keeping tests green
  • Document the TDD process in README
# Step 1: Write failing tests first (RED)
class TestInterestCalculation:
    """TDD tests for interest calculation feature."""
    
    def test_calculate_monthly_interest(self):
        """Test monthly interest calculation."""
        account = SavingsAccount("SAV001", "Jane", "1234", 10000.0)
        # 5% annual rate = 0.417% monthly
        interest = account.calculate_monthly_interest()
        assert interest == pytest.approx(41.67, rel=0.01)
    
    def test_apply_interest_increases_balance(self):
        """Test that applying interest increases balance."""
        account = SavingsAccount("SAV001", "Jane", "1234", 10000.0)
        account.apply_monthly_interest()
        assert account.balance == pytest.approx(10041.67, rel=0.01)
    
    def test_interest_rate_cannot_be_negative(self):
        """Test that negative interest rate raises error."""
        with pytest.raises(InvalidAmountError):
            SavingsAccount("SAV001", "Jane", "1234", 10000.0, 
                          interest_rate=-0.05)


# Step 2: Write minimum code to pass (GREEN)
class SavingsAccount(Account):
    """Savings account with interest calculation."""
    
    DEFAULT_INTEREST_RATE = 0.05  # 5% annual
    
    def __init__(self, account_number, holder_name, pin, 
                 initial_balance=0.0, interest_rate=None):
        super().__init__(account_number, holder_name, pin, initial_balance)
        if interest_rate is not None and interest_rate < 0:
            raise InvalidAmountError(interest_rate)
        self._interest_rate = interest_rate or self.DEFAULT_INTEREST_RATE
    
    def calculate_monthly_interest(self) -> float:
        """Calculate monthly interest amount."""
        monthly_rate = self._interest_rate / 12
        return round(self._balance * monthly_rate, 2)
    
    def apply_monthly_interest(self) -> float:
        """Apply monthly interest to balance."""
        interest = self.calculate_monthly_interest()
        self._balance += interest
        return self._balance


# Step 3: Refactor (keeping tests green)
10
Test Coverage Report (7.3)

Generate and maintain test coverage:

  • Use pytest-cov for coverage reports
  • Achieve minimum 90% coverage
  • Include coverage badge in README
  • Generate HTML coverage report
  • Identify and test uncovered branches
# Run tests with coverage
pytest --cov=banking --cov=exceptions --cov-report=html --cov-report=term

# Expected output:
# Name                 Stmts   Miss  Cover
# ----------------------------------------
# banking.py             150     12    92%
# exceptions.py           45      2    96%
# ----------------------------------------
# TOTAL                  195     14    93%

# Coverage report saved to htmlcov/
# pytest.ini configuration
[pytest]
addopts = --cov=banking --cov=exceptions --cov-fail-under=90
testpaths = tests
markers =
    slow: marks tests as slow
    integration: marks integration tests
11
Integration Tests (7.3)

Write integration tests for complete workflows:

  • Test complete user journey (create account → deposit → withdraw → transfer)
  • Test error recovery scenarios
  • Test logging output
  • Use pytest markers to separate from unit tests
@pytest.mark.integration
class TestBankingIntegration:
    """Integration tests for complete banking workflows."""
    
    def test_complete_banking_workflow(self):
        """Test complete user journey."""
        # Setup
        bank = Bank("Integration Test Bank")
        
        # Create accounts
        alice = bank.create_account("Alice", 10000.0, "1234")
        bob = bank.create_account("Bob", 5000.0, "5678")
        
        # Alice deposits more money
        alice.deposit(5000.0)
        assert alice.balance == 15000.0
        
        # Alice transfers to Bob
        alice.transfer(bob, 3000.0, "1234")
        assert alice.balance == 12000.0
        assert bob.balance == 8000.0
        
        # Bob withdraws
        bob.withdraw(2000.0, "5678")
        assert bob.balance == 6000.0
        
        # Verify transaction histories
        assert len(alice.transaction_history) == 3  # deposit, transfer out
        assert len(bob.transaction_history) == 2    # transfer in, withdrawal
    
    def test_error_recovery_workflow(self):
        """Test system recovers properly from errors."""
        bank = Bank("Error Test Bank")
        account = bank.create_account("Test User", 1000.0, "1234")
        
        # Failed withdrawal should not affect balance
        with pytest.raises(InsufficientFundsError):
            account.withdraw(5000.0, "1234")
        assert account.balance == 1000.0
        
        # Account should still be usable
        account.deposit(500.0)
        assert account.balance == 1500.0
12
Main Program with Error Demonstration

Create a main.py that demonstrates all features:

  • Show proper exception handling in action
  • Display logging output
  • Run test suite programmatically
  • Generate coverage report
def main():
    """Demonstrate the banking system with error handling."""
    print("=" * 60)
    print("SECUREBANK TESTING & DEBUGGING DEMONSTRATION")
    print("=" * 60)
    
    # Initialize bank
    bank = Bank("SecureBank")
    
    # Create accounts
    print("\n1. Creating accounts...")
    alice = bank.create_account("Alice Johnson", 10000.0, "1234")
    bob = bank.create_account("Bob Smith", 5000.0, "5678")
    
    # Demonstrate successful operations
    print("\n2. Successful operations:")
    print(f"   Alice deposits ₹5000: Balance = ₹{alice.deposit(5000.0):.2f}")
    print(f"   Alice withdraws ₹2000: Balance = ₹{alice.withdraw(2000.0, '1234'):.2f}")
    
    # Demonstrate exception handling
    print("\n3. Exception handling demonstrations:")
    
    # Invalid amount
    try:
        alice.deposit(-100)
    except InvalidAmountError as e:
        print(f"   ✓ Caught InvalidAmountError: {e}")
    
    # Insufficient funds
    try:
        bob.withdraw(50000.0, "5678")
    except InsufficientFundsError as e:
        print(f"   ✓ Caught InsufficientFundsError: {e}")
    
    # Wrong PIN
    try:
        alice.withdraw(100.0, "wrong")
    except AuthenticationError as e:
        print(f"   ✓ Caught AuthenticationError: {e}")
    
    print("\n4. Check log files:")
    print("   - banking.log (all logs)")
    print("   - banking_errors.log (errors only)")
    
    print("\n" + "=" * 60)
    print("Run 'pytest --cov' to execute test suite")
    print("=" * 60)

if __name__ == "__main__":
    main()
04

Submission

Create a public GitHub repository with the exact name shown below:

Required Repository Name
python-banking-testing
github.com/<your-username>/python-banking-testing
Required Files
python-banking-testing/
├── banking.py                # Main banking classes (Account, Bank, SavingsAccount)
├── exceptions.py             # Custom exception hierarchy
├── main.py                   # Demonstration script
├── pytest.ini                # Pytest configuration
├── tests/
│   ├── __init__.py
│   ├── conftest.py           # Shared pytest fixtures
│   ├── test_account.py       # Account tests (unittest)
│   ├── test_bank.py          # Bank tests (pytest)
│   ├── test_exceptions.py    # Exception tests
│   ├── test_integration.py   # Integration tests
│   └── test_tdd.py           # TDD feature tests
├── banking.log               # Generated log file
├── banking_errors.log        # Error log file
├── htmlcov/                  # Coverage report (HTML)
├── output.txt                # Output from main.py
└── README.md                 # Documentation
README.md Must Include:
  • Your full name and submission date
  • Exception hierarchy diagram
  • Explanation of logging strategy
  • TDD process documentation (red-green-refactor)
  • Test coverage badge (90%+ required)
  • Instructions to run tests
Do Include
  • All 12 requirements implemented
  • Custom exceptions with error codes
  • Comprehensive logging at all levels
  • Both unittest and pytest tests
  • Mocking examples
  • 90%+ test coverage
Do Not Include
  • Unhandled exceptions
  • Bare except clauses
  • Print statements instead of logging
  • Tests that don't pass
  • Coverage below 90%
  • Missing docstrings
Important: Run pytest --cov --cov-fail-under=90 before submitting to ensure all tests pass and coverage is at least 90%!
Submit Your Assignment

Enter your GitHub username - we'll verify your repository automatically

05

Grading Rubric

Your assignment will be graded on the following criteria:

Criteria Points Description
Custom Exceptions (7.1) 25 Complete exception hierarchy with error codes, proper inheritance
Exception Handling (7.1) 30 Try/except/else/finally, multiple exceptions, exception chaining
Logging (7.2) 30 Multiple handlers, proper levels, formatted output, audit trail
Unit Tests (7.3) 35 unittest and pytest tests, fixtures, parametrize, mocking
TDD & Coverage (7.3) 30 TDD process documented, 90%+ coverage, integration tests
Code Quality 25 Docstrings, type hints, clean code, README documentation
Total 175

Ready to Submit?

Make sure all tests pass and coverage is at least 90%.

Submit Your Assignment
06

What You Will Practice

Exception Handling (7.1)

Try/except/else/finally blocks, multiple exception handling, raising and re-raising, custom exception classes

Debugging & Logging (7.2)

Python logging module, log levels, handlers, formatters, debugging strategies

Unit Testing (7.3)

unittest framework, pytest, fixtures, parametrized tests, assertions, test organization

TDD & Mocking (7.3)

Test-driven development, red-green-refactor, mocking, patching, test coverage

07

Pro Tips

Exception Tips
  • Never use bare except: - always specify exception type
  • Use raise ... from e for exception chaining
  • Custom exceptions should inherit from appropriate base
  • Include helpful error messages and codes
Logging Tips
  • Use appropriate log levels (DEBUG → CRITICAL)
  • Include context in log messages (user, account)
  • Log exceptions with logger.exception()
  • Rotate log files in production
Testing Tips
  • Test both success AND failure cases
  • Use fixtures for common setup
  • Parametrize tests for multiple inputs
  • Keep tests independent and isolated
Common Mistakes
  • Catching exceptions too broadly
  • Not testing exception paths
  • Forgetting to mock external dependencies
  • Writing tests after code (not TDD)
08

Pre-Submission Checklist

Code Requirements
Testing Requirements