Python疯狂练习60天——第二十天

今日练习主题:测试与调试

今天我们将深入学习Python的测试和调试技术,包括单元测试、调试工具、性能分析和代码质量检查。

练习1:单元测试与测试框架

import unittest
from unittest.mock import Mock, patch, MagicMock
import pytest
import doctest

# 待测试的示例代码
class Calculator:
    """一个简单的计算器类"""
    
    def add(self, a, b):
        """加法运算"""
        if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
            raise TypeError("参数必须是数字")
        return a + b
    
    def subtract(self, a, b):
        """减法运算"""
        return a - b
    
    def multiply(self, a, b):
        """乘法运算"""
        return a * b
    
    def divide(self, a, b):
        """除法运算"""
        if b == 0:
            raise ValueError("除数不能为零")
        return a / b
    
    def factorial(self, n):
        """计算阶乘"""
        if n < 0:
            raise ValueError("阶乘不能为负数")
        if n == 0:
            return 1
        return n * self.factorial(n - 1)

def email_validator(email):
    """简单的邮箱验证函数"""
    if not email or '@' not in email:
        return False
    local_part, domain = email.split('@', 1)
    return bool(local_part) and bool(domain) and '.' in domain

# 使用doctest的示例
def fibonacci(n):
    """
    计算斐波那契数列的第n项
    
    Examples:
    >>> fibonacci(0)
    0
    >>> fibonacci(1)
    1
    >>> fibonacci(5)
    5
    >>> fibonacci(10)
    55
    """
    if n < 0:
        raise ValueError("n不能为负数")
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

class TestCalculator(unittest.TestCase):
    """Calculator类的单元测试"""
    
    def setUp(self):
        """测试前置设置"""
        self.calc = Calculator()
        print("创建Calculator实例")
    
    def tearDown(self):
        """测试后置清理"""
        self.calc = None
        print("清理Calculator实例")
    
    def test_add(self):
        """测试加法"""
        self.assertEqual(self.calc.add(2, 3), 5)
        self.assertEqual(self.calc.add(-1, 1), 0)
        self.assertEqual(self.calc.add(0, 0), 0)
        self.assertAlmostEqual(self.calc.add(0.1, 0.2), 0.3, places=7)
    
    def test_add_type_error(self):
        """测试加法类型错误"""
        with self.assertRaises(TypeError):
            self.calc.add("2", 3)
        with self.assertRaises(TypeError):
            self.calc.add(2, "3")
    
    def test_subtract(self):
        """测试减法"""
        self.assertEqual(self.calc.subtract(5, 3), 2)
        self.assertEqual(self.calc.subtract(3, 5), -2)
    
    def test_multiply(self):
        """测试乘法"""
        self.assertEqual(self.calc.multiply(3, 4), 12)
        self.assertEqual(self.calc.multiply(-2, 3), -6)
        self.assertEqual(self.calc.multiply(0, 5), 0)
    
    def test_divide(self):
        """测试除法"""
        self.assertEqual(self.calc.divide(6, 3), 2)
        self.assertEqual(self.calc.divide(5, 2), 2.5)
    
    def test_divide_by_zero(self):
        """测试除零错误"""
        with self.assertRaises(ValueError):
            self.calc.divide(5, 0)
    
    def test_factorial(self):
        """测试阶乘"""
        self.assertEqual(self.calc.factorial(0), 1)
        self.assertEqual(self.calc.factorial(1), 1)
        self.assertEqual(self.calc.factorial(5), 120)
    
    def test_factorial_negative(self):
        """测试负数阶乘错误"""
        with self.assertRaises(ValueError):
            self.calc.factorial(-1)
    
    @unittest.skip("暂时跳过这个测试")
    def test_skip_example(self):
        """跳过测试的示例"""
        self.fail("这个测试应该被跳过")
    
    @unittest.skipIf(not hasattr(Calculator, 'advanced_function'), 
                    "需要高级功能支持")
    def test_conditional_skip(self):
        """条件跳过测试"""
        pass

class TestEmailValidator(unittest.TestCase):
    """邮箱验证函数的测试"""
    
    def test_valid_emails(self):
        """测试有效邮箱"""
        valid_emails = [
            "test@example.com",
            "user.name@domain.co.uk",
            "user+tag@domain.org"
        ]
        for email in valid_emails:
            with self.subTest(email=email):
                self.assertTrue(email_validator(email))
    
    def test_invalid_emails(self):
        """测试无效邮箱"""
        invalid_emails = [
            "",
            "invalid",
            "invalid@",
            "@domain.com",
            "user@domain"
        ]
        for email in invalid_emails:
            with self.subTest(email=email):
                self.assertFalse(email_validator(email))

class TestWithMocks(unittest.TestCase):
    """使用Mock对象的测试"""
    
    def test_mock_example(self):
        """Mock对象示例"""
        # 创建Mock对象
        mock_obj = Mock()
        mock_obj.some_method.return_value = "mocked result"
        
        # 使用Mock对象
        result = mock_obj.some_method()
        self.assertEqual(result, "mocked result")
        
        # 验证方法被调用
        mock_obj.some_method.assert_called_once()
    
    def test_patch_example(self):
        """使用patch的示例"""
        with patch('builtins.print') as mock_print:
            print("Hello, World!")
            mock_print.assert_called_with("Hello, World!")
    
    def test_mock_side_effect(self):
        """Mock的side_effect使用"""
        mock_obj = Mock()
        mock_obj.get_value.side_effect = [1, 2, 3]
        
        self.assertEqual(mock_obj.get_value(), 1)
        self.assertEqual(mock_obj.get_value(), 2)
        self.assertEqual(mock_obj.get_value(), 3)

# 使用pytest风格的测试(需要安装pytest)
def test_fibonacci_pytest_style():
    """使用pytest风格的斐波那契测试"""
    assert fibonacci(0) == 0
    assert fibonacci(1) == 1
    assert fibonacci(5) == 5
    assert fibonacci(10) == 55

def test_fibonacci_negative():
    """测试斐波那契负数输入"""
    with pytest.raises(ValueError):
        fibonacci(-1)

def test_calculator_pytest_style():
    """pytest风格的计算器测试"""
    calc = Calculator()
    assert calc.add(2, 3) == 5
    assert calc.multiply(4, 5) == 20

def run_tests():
    """运行所有测试"""
    print("=== 单元测试与测试框架 ===")
    
    # 1. 运行unittest测试
    print("1. 运行unittest测试")
    unittest_loader = unittest.TestLoader()
    
    # 运行单个测试类
    print("
运行Calculator测试:")
    calculator_suite = unittest_loader.loadTestsFromTestCase(TestCalculator)
    calculator_runner = unittest.TextTestRunner(verbosity=2)
    calculator_result = calculator_runner.run(calculator_suite)
    
    print("
运行EmailValidator测试:")
    email_suite = unittest_loader.loadTestsFromTestCase(TestEmailValidator)
    email_runner = unittest.TextTestRunner(verbosity=2)
    email_result = email_runner.run(email_suite)
    
    # 2. 运行doctest
    print("
2. 运行doctest")
    doctest_result = doctest.testmod()
    print(f"Doctest结果: 运行{doctest_result.attempted}个测试,失败{doctest_result.failed}个")
    
    # 3. 创建测试套件
    print("
3. 创建测试套件")
    all_tests = unittest.TestSuite()
    all_tests.addTest(calculator_suite)
    all_tests.addTest(email_suite)
    
    print(f"测试套件包含 {all_tests.countTestCases()} 个测试用例")
    
    # 4. 测试发现
    print("
4. 测试发现示例")
    # 在实际项目中可以使用:python -m unittest discover
    
    return {
        'calculator_tests': calculator_result,
        'email_tests': email_result,
        'doctest': doctest_result
    }

# 运行测试
test_results = run_tests()

练习2:调试技术与性能分析

import pdb
import logging
import cProfile
import pstats
import timeit
from memory_profiler import profile
import traceback
import sys

def debugging_techniques():
    """调试技术与性能分析"""
    print("
=== 调试技术与性能分析 ===")
    
    # 设置日志记录
    logging.basicConfig(
        level=logging.DEBUG,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        handlers=[
            logging.FileHandler('debug.log'),
            logging.StreamHandler()
        ]
    )
    logger = logging.getLogger('DebugDemo')
    
    class DebugExample:
        def __init__(self):
            self.data = []
            self.counter = 0
            logger.info("DebugExample实例已创建")
        
        def problematic_function(self, n):
            """一个有问题的函数,用于调试演示"""
            logger.debug(f"开始执行problematic_function({n})")
            
            result = 0
            for i in range(n):
                # 故意制造一个潜在问题
                if i == 5:
                    logger.warning("遇到特殊值 i=5")
                
                result += i * 2
                
                # 模拟复杂计算
                time.sleep(0.01)
                
                self.counter += 1
            
            logger.debug(f"problematic_function完成,结果: {result}")
            return result
        
        def divide_numbers(self, a, b):
            """除法函数,可能抛出异常"""
            logger.info(f"执行除法: {a} / {b}")
            
            if b == 0:
                logger.error("除零错误!")
                raise ValueError("除数不能为零")
            
            result = a / b
            logger.debug(f"除法结果: {result}")
            return result
        
        def process_data(self, data_list):
            """处理数据的函数"""
            logger.info(f"开始处理数据,数量: {len(data_list)}")
            
            processed = []
            for i, item in enumerate(data_list):
                try:
                    # 模拟数据处理
                    if isinstance(item, str):
                        processed.append(item.upper())
                    elif isinstance(item, (int, float)):
                        processed.append(item * 2)
                    else:
                        logger.warning(f"无法处理的数据类型: {type(item)}")
                        processed.append(None)
                        
                except Exception as e:
                    logger.error(f"处理数据时出错: {e}", exc_info=True)
                    processed.append(None)
            
            logger.info("数据处理完成")
            return processed
    
    def demo_basic_debugging():
        """基础调试演示"""
        print("1. 基础调试技术")
        print("-" * 40)
        
        example = DebugExample()
        
        # 使用print调试(不推荐在生产环境使用)
        print("=== Print调试 ===")
        result = example.problematic_function(3)
        print(f"结果: {result}")
        
        # 使用日志调试
        print("
=== 日志调试 ===")
        example.problematic_function(5)
        
        # 异常处理
        print("
=== 异常处理 ===")
        try:
            example.divide_numbers(10, 0)
        except ValueError as e:
            print(f"捕获到异常: {e}")
            # 打印完整的异常信息
            traceback.print_exc()
        
        # 正常情况
        try:
            result = example.divide_numbers(10, 2)
            print(f"正常除法结果: {result}")
        except ValueError as e:
            print(f"异常: {e}")
    
    def demo_pdb_debugging():
        """PDB调试演示"""
        print("
2. PDB调试器")
        print("-" * 40)
        
        def buggy_function(numbers):
            """一个有bug的函数"""
            total = 0
            for i in range(len(numbers)):
                # 这里有一个off-by-one错误
                total += numbers[i]  # 应该是numbers[i]
                print(f"处理第 {i} 个数字: {numbers[i]}")
            
            average = total / len(numbers)  # 如果numbers为空会除零
            return total, average
        
        # 手动触发PDB(撤销注释来使用)
        # print("撤销注释下面的代码来体验PDB调试")
        # numbers = [1, 2, 3, 4, 5]
        # 设置断点
        # pdb.set_trace()
        # result = buggy_function(numbers)
        # print(f"结果: {result}")
        
        print("PDB常用命令:")
        print("  n(ext) - 执行下一行")
        print("  s(tep) - 进入函数")
        print("  c(ontinue) - 继续执行")
        print("  l(ist) - 显示代码")
        print("  p(rint) - 打印变量")
        print("  q(uit) - 退出调试")
    
    def demo_profiling():
        """性能分析演示"""
        print("
3. 性能分析")
        print("-" * 40)
        
        def slow_function(n):
            """一个运行较慢的函数"""
            result = 0
            for i in range(n):
                for j in range(1000):
                    result += i * j
            return result
        
        def fast_function(n):
            """一个优化后的函数"""
            return sum(i * j for i in range(n) for j in range(1000))
        
        # 使用timeit测试性能
        print("=== 性能比较 ===")
        
        n = 100
        slow_time = timeit.timeit(lambda: slow_function(n), number=10)
        fast_time = timeit.timeit(lambda: fast_function(n), number=10)
        
        print(f"慢函数执行时间: {slow_time:.4f}秒")
        print(f"快函数执行时间: {fast_time:.4f}秒")
        print(f"性能提升: {slow_time/fast_time:.2f}倍")
        
        # 使用cProfile进行详细分析
        print("
=== cProfile分析 ===")
        
        profiler = cProfile.Profile()
        profiler.enable()
        
        # 执行要分析的代码
        slow_function(50)
        fast_function(50)
        
        profiler.disable()
        
        # 输出分析结果
        stats = pstats.Stats(profiler)
        print("函数调用统计:")
        stats.sort_stats('cumulative').print_stats(10)
    
    @profile
    def memory_intensive_function():
        """内存密集型函数(需要memory-profiler包)"""
        print("
4. 内存分析")
        print("-" * 40)
        
        # 创建大量数据
        big_list = [i for i in range(100000)]
        big_dict = {i: str(i) for i in range(50000)}
        
        # 处理数据
        processed = [x * 2 for x in big_list]
        result = {k: v.upper() for k, v in big_dict.items()}
        
        return processed, result
    
    def demo_advanced_debugging():
        """高级调试技术"""
        print("
5. 高级调试技术")
        print("-" * 40)
        
        # 断言调试
        def calculate_discount(price, discount_rate):
            """计算折扣价格"""
            assert price > 0, "价格必须大于0"
            assert 0 <= discount_rate <= 1, "折扣率必须在0-1之间"
            
            discounted_price = price * (1 - discount_rate)
            assert discounted_price >= 0, "折扣后价格不能为负"
            
            return discounted_price
        
        print("=== 断言调试 ===")
        try:
            result = calculate_discount(100, 0.2)
            print(f"折扣价格: {result}")
            
            # 这会触发断言错误
            # result = calculate_discount(-100, 0.2)
        except AssertionError as e:
            print(f"断言错误: {e}")
        
        # 使用sys.excepthook处理未捕获的异常
        def global_exception_handler(exc_type, exc_value, exc_traceback):
            """全局异常处理"""
            if issubclass(exc_type, KeyboardInterrupt):
                # 忽略Ctrl+C
                sys.__excepthook__(exc_type, exc_value, exc_traceback)
                return
            
            logger.critical("未捕获的异常:", 
                          exc_info=(exc_type, exc_value, exc_traceback))
        
        sys.excepthook = global_exception_handler
        
        print("=== 自定义异常钩子已设置 ===")
    
    def demo_debugging_tools():
        """调试工具演示"""
        print("
6. 调试工具")
        print("-" * 40)
        
        # 使用inspect模块
        import inspect
        
        def example_function(x, y=10):
            """示例函数"""
            return x + y
        
        print("=== 代码检查 ===")
        print(f"函数名: {example_function.__name__}")
        print(f"文档字符串: {example_function.__doc__}")
        print(f"源代码: {inspect.getsource(example_function)}")
        
        # 获取函数签名
        sig = inspect.signature(example_function)
        print(f"参数: {list(sig.parameters.keys())}")
        
        # 使用__debug__常量
        print("
=== 调试常量 ===")
        if __debug__:
            print("调试模式已启用")
        else:
            print("调试模式未启用(使用-O优化选项时)")
    
    # 运行所有调试演示
    demo_basic_debugging()
    demo_pdb_debugging()
    demo_profiling()
    
    # 内存分析需要单独运行:python -m memory_profiler script.py
    print("
内存分析示例需要运行: python -m memory_profiler script.py")
    
    demo_advanced_debugging()
    demo_debugging_tools()
    
    logger.info("调试演示完成")

def create_test_project():
    """创建完整的测试项目示例"""
    print("
=== 完整测试项目示例 ===")
    
    # 项目结构
    project_structure = """
    my_project/
    ├── src/
    │   ├── __init__.py
    │   ├── calculator.py
    │   ├── validator.py
    │   └── utils.py
    ├── tests/
    │   ├── __init__.py
    │   ├── test_calculator.py
    │   ├── test_validator.py
    │   ├── test_utils.py
    │   └── conftest.py
    ├── requirements.txt
    ├── setup.py
    ├── pytest.ini
    └── .github/
        └── workflows/
            └── tests.yml
    """
    
    print("项目结构:")
    print(project_structure)
    
    # 创建示例配置文件
    config_examples = {
        'pytest.ini': """
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short --strict-markers
markers =
    slow: marks tests as slow (deselect with '-m "not slow"')
    integration: integration tests
""",
        'conftest.py': """
import pytest
from src.calculator import Calculator
from src.validator import EmailValidator

@pytest.fixture
def calculator():
    '''提供Calculator实例'''
    return Calculator()

@pytest.fixture
def email_validator():
    '''提供EmailValidator实例'''
    return EmailValidator()

@pytest.fixture(autouse=True)
def setup_teardown():
    '''每个测试自动运行的设置和清理'''
    print("测试开始")
    yield
    print("测试结束")
""",
        'github_workflow': """
name: Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [3.8, 3.9, 3.10]
    steps:
    - uses: actions/checkout@v2
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v2
      with:
        python-version: ${{ matrix.python-version }}
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
    - name: Run tests
      run: |
        python -m pytest tests/ -v --cov=src --cov-report=xml
    - name: Upload coverage
      uses: codecov/codecov-action@v2
"""
    }
    
    print("配置文件示例:")
    for filename, content in config_examples.items():
        print(f"
{filename}:")
        print(content)
    
    # 测试策略提议
    testing_strategy = """
    测试策略提议:
    
    1. 单元测试 (Unit Tests)
       - 测试单个函数/方法
       - 快速执行,隔离依赖
       - 使用mock对象
    
    2. 集成测试 (Integration Tests)
       - 测试模块间交互
       - 验证数据流和接口
    
    3. 功能测试 (Functional Tests)
       - 测试完整功能
       - 从用户角度验证
    
    4. 性能测试 (Performance Tests)
       - 测试系统性能
       - 压力测试和负载测试
    
    5. 端到端测试 (E2E Tests)
       - 测试完整工作流
       - 模拟真实用户场景
    """
    
    print(testing_strategy)
    
    # 最佳实践
    best_practices = """
    测试最佳实践:
    
    ✓ 测试名称要具有描述性
    ✓ 每个测试只测试一个功能
    ✓ 使用setup和teardown管理测试环境
    ✓ 测试要独立,不依赖执行顺序
    ✓ 使用适当的断言方法
    ✓ 测试边界情况和异常情况
    ✓ 保持测试代码的整洁
    ✓ 定期运行测试套件
    ✓ 使用持续集成
    ✓ 测量测试覆盖率
    """
    
    print(best_practices)

def run_comprehensive_testing():
    """运行综合测试演示"""
    print("=== 综合测试演示 ===")
    
    # 运行单元测试
    test_results = run_tests()
    
    # 运行调试演示
    debugging_techniques()
    
    # 创建测试项目示例
    create_test_project()
    
    # 测试覆盖率报告(模拟)
    print("
=== 测试覆盖率报告 ===")
    coverage_report = {
        '模块': ['calculator.py', 'validator.py', 'utils.py', '全部'],
        '语句覆盖率': [95, 88, 92, 92],
        '分支覆盖率': [90, 85, 88, 88],
        '函数覆盖率': [100, 95, 96, 97]
    }
    
    print("测试覆盖率统计:")
    for i, module in enumerate(coverage_report['模块']):
        stmt = coverage_report['语句覆盖率'][i]
        branch = coverage_report['分支覆盖率'][i]
        func = coverage_report['函数覆盖率'][i]
        print(f"{module:10} | 语句: {stmt:3}% | 分支: {branch:3}% | 函数: {func:3}%")
    
    # 测试质量评估
    print("
=== 测试质量评估 ===")
    quality_metrics = {
        '总测试用例数': 45,
        '通过测试数': 43,
        '失败测试数': 2,
        '跳过测试数': 1,
        '测试执行时间': '2.3秒',
        '平均测试时间': '51毫秒',
        '测试代码行数': 560,
        '生产代码行数': 320,
        '测试/代码比例': '1.75:1'
    }
    
    for metric, value in quality_metrics.items():
        print(f"{metric}: {value}")
    
    # 提议和改善
    print("
=== 改善提议 ===")
    improvements = [
        "增加边界情况测试",
        "提高异常情况测试覆盖率",
        "添加性能测试",
        "实现持续集成流水线",
        "增加集成测试用例"
    ]
    
    for i, improvement in enumerate(improvements, 1):
        print(f"{i}. {improvement}")
    
    return {
        'test_results': test_results,
        'coverage': coverage_report,
        'quality_metrics': quality_metrics,
        'improvements': improvements
    }

# 运行综合测试演示
final_results = run_comprehensive_testing()

print("
" + "="*60)
print("第二十天学习完成!")
print("="*60)
print("今天学习了:")
print("✓ 单元测试框架(unittest, pytest)")
print("✓ 测试用例设计和组织")
print("✓ Mock对象和测试替身")
print("✓ 调试工具和技术")
print("✓ 性能分析和优化")
print("✓ 测试最佳实践")
print("✓ 持续集成和测试自动化")

学习总结:

测试方面:

  • 掌握了unittest和pytest测试框架
  • 学会了编写有效的测试用例
  • 理解了Mock对象和测试隔离
  • 学会了测试组织和执行策略

调试方面:

  • 掌握了多种调试工具和技术
  • 学会了性能分析和优化方法
  • 理解了日志记录和异常处理
  • 学会了使用专业调试器

最佳实践:

  • 测试驱动开发(TDD)理念
  • 持续集成和自动化测试
  • 代码质量保证方法
  • 测试覆盖率分析

这些技能对于编写高质量、可维护的Python代码至关重大。明天我们将开始学习更高级的主题!

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
米途栗然的头像 - 鹿快
评论 抢沙发

请登录后发表评论

    暂无评论内容