Session-3-5-Test

This page shows the source code for Session-3-5-Test.py in browser-friendly HTML format. It was generated automatically from the original Python file.

Source File Session-3-5-Test.py
Folder Chapter-3-Advanced-Sessions
"""
Test suite for Advanced Session 5
Demonstrates pytest features and best practices

Run with: pytest test_session5.py -v
Coverage: pytest test_session5.py --cov=advanced_session5_examples
"""

import pytest
from advanced_session5_examples import (
    add, divide, is_prime, reverse_string, calculate_average,
    BankAccount, DataProcessor, Config,
    format_name, parse_value, validate_email
)

# ============================================
# PART 1: Basic Function Tests
# ============================================

class TestBasicFunctions:
    """Test basic mathematical and string functions."""
    
    def test_add_positive_numbers(self):
        """Test adding two positive numbers."""
        assert add(2, 3) == 5
        assert add(10, 20) == 30
    
    def test_add_negative_numbers(self):
        """Test adding negative numbers."""
        assert add(-5, -3) == -8
        assert add(-10, 5) == -5
    
    def test_add_zero(self):
        """Test adding with zero."""
        assert add(0, 0) == 0
        assert add(5, 0) == 5
    
    def test_divide_normal(self):
        """Test normal division."""
        assert divide(10, 2) == 5.0
        assert divide(15, 3) == 5.0
    
    def test_divide_by_zero(self):
        """Test division by zero raises error."""
        with pytest.raises(ValueError, match="Cannot divide by zero"):
            divide(10, 0)
    
    def test_divide_negative(self):
        """Test division with negative numbers."""
        assert divide(-10, 2) == -5.0
        assert divide(10, -2) == -5.0


# ============================================
# PART 2: Parametrized Tests
# ============================================

class TestParametrized:
    """Demonstrate parametrized testing."""
    
    @pytest.mark.parametrize("a, b, expected", [
        (2, 3, 5),
        (0, 0, 0),
        (-1, 1, 0),
        (100, 200, 300),
        (-5, -3, -8),
    ])
    def test_add_multiple_cases(self, a, b, expected):
        """Test add function with multiple parameter sets."""
        assert add(a, b) == expected
    
    @pytest.mark.parametrize("number, expected", [
        (2, True),
        (3, True),
        (5, True),
        (7, True),
        (11, True),
        (4, False),
        (6, False),
        (9, False),
        (15, False),
    ])
    def test_is_prime_multiple_cases(self, number, expected):
        """Test prime checking with multiple numbers."""
        assert is_prime(number) == expected
    
    @pytest.mark.parametrize("string, expected", [
        ("hello", "olleh"),
        ("", ""),
        ("a", "a"),
        ("Python", "nohtyP"),
        ("12345", "54321"),
    ])
    def test_reverse_string_cases(self, string, expected):
        """Test string reversal with various inputs."""
        assert reverse_string(string) == expected


# ============================================
# PART 3: Fixtures
# ============================================

@pytest.fixture
def sample_numbers():
    """Provide sample number list for testing."""
    return [10, 20, 30, 40, 50]


@pytest.fixture
def bank_account():
    """Provide a bank account instance."""
    return BankAccount("Test User", 1000)


@pytest.fixture
def data_processor():
    """Provide a data processor instance."""
    processor = DataProcessor()
    processor.add_data(10)
    processor.add_data(20)
    processor.add_data(30)
    return processor


class TestWithFixtures:
    """Tests using fixtures."""
    
    def test_calculate_average(self, sample_numbers):
        """Test average calculation with fixture."""
        result = calculate_average(sample_numbers)
        assert result == 30.0
    
    def test_bank_account_deposit(self, bank_account):
        """Test deposit with fixture."""
        initial = bank_account.get_balance()
        bank_account.deposit(500)
        assert bank_account.get_balance() == initial + 500
    
    def test_bank_account_withdraw(self, bank_account):
        """Test withdrawal with fixture."""
        initial = bank_account.get_balance()
        bank_account.withdraw(300)
        assert bank_account.get_balance() == initial - 300
    
    def test_data_processor_statistics(self, data_processor):
        """Test statistics calculation with fixture."""
        stats = data_processor.get_statistics()
        assert stats['mean'] == 20.0
        assert stats['min'] == 10
        assert stats['max'] == 30
        assert stats['count'] == 3


# ============================================
# PART 4: Testing Exceptions
# ============================================

class TestExceptions:
    """Test that functions raise appropriate exceptions."""
    
    def test_is_prime_invalid_input(self):
        """Test is_prime raises error for invalid input."""
        with pytest.raises(ValueError):
            is_prime(1)
        with pytest.raises(ValueError):
            is_prime(0)
        with pytest.raises(ValueError):
            is_prime(-5)
    
    def test_reverse_string_type_error(self):
        """Test reverse_string raises TypeError for non-string."""
        with pytest.raises(TypeError):
            reverse_string(123)
        with pytest.raises(TypeError):
            reverse_string([1, 2, 3])
    
    def test_calculate_average_empty_list(self):
        """Test average raises error for empty list."""
        with pytest.raises(ValueError, match="empty list"):
            calculate_average([])
    
    def test_bank_account_negative_initial(self):
        """Test bank account rejects negative initial balance."""
        with pytest.raises(ValueError):
            BankAccount("Test", -100)
    
    def test_bank_account_invalid_deposit(self):
        """Test deposit rejects invalid amounts."""
        account = BankAccount("Test", 1000)
        with pytest.raises(ValueError):
            account.deposit(0)
        with pytest.raises(ValueError):
            account.deposit(-50)
    
    def test_bank_account_insufficient_funds(self):
        """Test withdraw raises error for insufficient funds."""
        account = BankAccount("Test", 100)
        with pytest.raises(ValueError, match="Insufficient funds"):
            account.withdraw(200)


# ============================================
# PART 5: Testing Class Behavior
# ============================================

class TestBankAccount:
    """Comprehensive tests for BankAccount class."""
    
    def test_initialization(self):
        """Test account initialization."""
        account = BankAccount("Alice", 500)
        assert account.owner == "Alice"
        assert account.balance == 500
    
    def test_deposit_increases_balance(self):
        """Test deposit increases balance correctly."""
        account = BankAccount("Bob", 1000)
        new_balance = account.deposit(500)
        assert new_balance == 1500
        assert account.get_balance() == 1500
    
    def test_withdraw_decreases_balance(self):
        """Test withdraw decreases balance correctly."""
        account = BankAccount("Charlie", 1000)
        new_balance = account.withdraw(300)
        assert new_balance == 700
        assert account.get_balance() == 700
    
    def test_multiple_transactions(self):
        """Test multiple deposits and withdrawals."""
        account = BankAccount("Diana", 1000)
        account.deposit(500)
        account.withdraw(200)
        account.deposit(300)
        assert account.get_balance() == 1600
    
    def test_str_representation(self):
        """Test string representation."""
        account = BankAccount("Eve", 1000)
        assert "Eve" in str(account)
        assert "1000" in str(account)


class TestDataProcessor:
    """Comprehensive tests for DataProcessor class."""
    
    def test_add_data(self):
        """Test adding data points."""
        processor = DataProcessor()
        processor.add_data(10)
        processor.add_data(20)
        assert len(processor.data) == 2
    
    def test_clear_data(self):
        """Test clearing data."""
        processor = DataProcessor()
        processor.add_data(10)
        processor.add_data(20)
        processor.clear()
        assert len(processor.data) == 0
    
    def test_statistics_empty_raises_error(self):
        """Test statistics on empty data raises error."""
        processor = DataProcessor()
        with pytest.raises(ValueError):
            processor.get_statistics()
    
    def test_statistics_calculation(self):
        """Test statistics are calculated correctly."""
        processor = DataProcessor()
        for value in [10, 20, 30, 40, 50]:
            processor.add_data(value)
        
        stats = processor.get_statistics()
        assert stats['mean'] == 30.0
        assert stats['min'] == 10
        assert stats['max'] == 50
        assert stats['count'] == 5
    
    def test_filter_above_threshold(self):
        """Test filtering data."""
        processor = DataProcessor()
        for value in [10, 20, 30, 40, 50]:
            processor.add_data(value)
        
        filtered = processor.filter_above_threshold(25)
        assert filtered == [30, 40, 50]


# ============================================
# PART 6: Testing Utility Functions
# ============================================

class TestUtilityFunctions:
    """Test utility functions with type hints."""
    
    def test_format_name_without_middle(self):
        """Test name formatting without middle name."""
        result = format_name("John", "Doe")
        assert result == "John Doe"
    
    def test_format_name_with_middle(self):
        """Test name formatting with middle name."""
        result = format_name("John", "Doe", "Q")
        assert result == "John Q Doe"
    
    @pytest.mark.parametrize("value, expected", [
        (42, 42.0),
        ("3.14", 3.14),
        (2.5, 2.5),
        ("100", 100.0),
    ])
    def test_parse_value_valid(self, value, expected):
        """Test parsing valid values."""
        assert parse_value(value) == expected
    
    def test_parse_value_invalid(self):
        """Test parsing invalid values raises error."""
        with pytest.raises(ValueError):
            parse_value("not a number")
    
    @pytest.mark.parametrize("email, expected", [
        ("test@example.com", True),
        ("user@domain.co.uk", True),
        ("invalid", False),
        ("@example.com", False),
        ("test@", False),
    ])
    def test_validate_email(self, email, expected):
        """Test email validation."""
        assert validate_email(email) == expected


# ============================================
# PART 7: Testing Configuration
# ============================================

class TestConfig:
    """Test configuration class."""
    
    def test_default_values(self):
        """Test default configuration values."""
        config = Config()
        assert config.debug is False
        assert config.log_level == "INFO"
        assert config.max_retries == 3
        assert config.timeout == 30.0
    
    def test_load_from_dict(self):
        """Test loading configuration from dictionary."""
        config = Config()
        config.load_from_dict({
            'debug': True,
            'max_retries': 5
        })
        assert config.debug is True
        assert config.max_retries == 5
    
    def test_validate_valid_config(self):
        """Test validation accepts valid configuration."""
        config = Config()
        assert config.validate() is True
    
    def test_validate_invalid_max_retries(self):
        """Test validation rejects negative max_retries."""
        config = Config()
        config.max_retries = -1
        assert config.validate() is False
    
    def test_validate_invalid_timeout(self):
        """Test validation rejects non-positive timeout."""
        config = Config()
        config.timeout = 0
        assert config.validate() is False
    
    def test_validate_invalid_log_level(self):
        """Test validation rejects invalid log level."""
        config = Config()
        config.log_level = "INVALID"
        assert config.validate() is False
    
    def test_to_dict(self):
        """Test converting config to dictionary."""
        config = Config()
        config_dict = config.to_dict()
        assert isinstance(config_dict, dict)
        assert 'debug' in config_dict
        assert 'log_level' in config_dict


# ============================================
# PART 8: Marks and Skipping
# ============================================

class TestMarks:
    """Demonstrate pytest marks."""
    
    @pytest.mark.slow
    def test_slow_operation(self):
        """Mark a slow test."""
        # This would be a slow operation
        import time
        # time.sleep(2)  # Uncomment to make it actually slow
        assert True
    
    @pytest.mark.skip(reason="Not implemented yet")
    def test_future_feature(self):
        """Skip a test for future feature."""
        assert False  # This won't run
    
    @pytest.mark.skipif(True, reason="Conditional skip")
    def test_conditional(self):
        """Conditionally skip a test."""
        assert False  # This won't run
    
    @pytest.mark.xfail(reason="Known bug")
    def test_known_bug(self):
        """Mark a test as expected to fail."""
        assert 1 == 2  # Expected to fail


# ============================================
# Summary
# ============================================

"""
Test Suite Summary:

This test suite demonstrates:
1. Basic function testing
2. Parametrized tests
3. Fixtures for setup/teardown
4. Exception testing
5. Class behavior testing
6. Utility function testing
7. Configuration testing
8. Test marks and skipping

Run commands:
- pytest test_session5.py                    # Run all tests
- pytest test_session5.py -v                 # Verbose output
- pytest test_session5.py::TestBasicFunctions  # Run specific class
- pytest test_session5.py -k "add"           # Run tests matching 'add'
- pytest test_session5.py -m slow            # Run tests marked as slow
- pytest test_session5.py --cov              # With coverage report
- pytest test_session5.py -x                 # Stop on first failure
- pytest test_session5.py --tb=short         # Short traceback format

Coverage:
- pytest --cov=advanced_session5_examples --cov-report=html

This will generate htmlcov/index.html with detailed coverage report.
"""