当前位置:首页>python>【跟着AI学Python】Python进阶:测试驱动开发(TDD)

【跟着AI学Python】Python进阶:测试驱动开发(TDD)

  • 2026-09-09 11:04:51
【跟着AI学Python】Python进阶:测试驱动开发(TDD)
【Qwen3-Max】

太棒了!欢迎来到 【跟着AI学Python】Python进阶:测试驱动开发(TDD)!🎉今天我们将学习如何用 自动化测试 为你的银行系统保驾护航,确保代码可靠、可维护、无回归!

💡 TDD 核心思想:先写测试 → 再写实现 → 测试通过 → 重构让 bug 在开发阶段就被消灭!


🎯 今日目标

✅ 掌握 pytest(比 unittest 更简洁)编写单元测试✅ 为银行账户系统编写 全面测试用例✅ 使用 Mock 隔离外部依赖(如文件、网络)✅ 生成 测试覆盖率报告


📘 一、为什么选择 pytest?

特性
unittest
pytest
语法
需继承 TestCase
普通函数即可
断言
self.assertEqual()
直接 assert
参数化
复杂
@pytest.mark.parametrize
 超简单
插件生态
有限
丰富(覆盖率、异步等)

✅ 安装:

pip install pytest pytest-cov

🔧 二、实践 1:为银行账户编写单元测试

项目结构

bank_system/├── bank.py                 # 账户逻辑├── test_bank.py            # 测试文件(命名以 test_ 开头)└── data/    └── accounts.jsonl      # 账户数据文件
# test_bank.pyimport pytestfrom bank import BankAccount, SavingsAccount, CheckingAccount, InsufficientFundsError, InvalidAmountErrorclass TestBankAccount:    """基类账户测试"""    def test_deposit_valid_amount(self):        """测试有效存款"""        account = SavingsAccount("Alice")        account.deposit(100)        assert account.balance == 100.0    def test_deposit_invalid_amount(self):        """测试无效存款(负数)"""        account = SavingsAccount("Alice")        with pytest.raises(InvalidAmountError):            account.deposit(-50)    def test_account_id_unique(self):        """测试账号唯一性"""        acc1 = SavingsAccount("A")        acc2 = CheckingAccount("B")        assert acc1.account_id != acc2.account_idclass TestSavingsAccount:    """储蓄账户测试"""    def setup_method(self):        """每个测试前初始化"""        self.account = SavingsAccount("Alice")        self.account.deposit(1000)  # 初始余额 1000    def test_withdraw_success(self):        """成功取款(含手续费)"""        self.account.withdraw(100)        # 100 + 1 手续费 = 101 扣除        assert self.account.balance == 899.0    def test_withdraw_insufficient_funds(self):        """余额不足取款"""        with pytest.raises(InsufficientFundsError):            self.account.withdraw(2000)  # 2000 + 1 > 1000    def test_withdraw_invalid_amount(self):        """无效取款金额"""        with pytest.raises(InvalidAmountError):            self.account.withdraw(0)class TestCheckingAccount:    """支票账户测试"""    def test_overdraft_allowed(self):        """允许透支到 -500"""        account = CheckingAccount("Bob")        account.deposit(300)        account.withdraw(700)  # 300 - 700 = -400 > -500        assert account.balance == -400.0    def test_overdraft_exceeded(self):        """透支超限"""        account = CheckingAccount("Bob")        with pytest.raises(InsufficientFundsError):            account.withdraw(600)  # 0 - 600 = -600 < -500

✅ 测试设计原则:

  • 每个测试只验证一个行为
  • 使用 with pytest.raises() 检查异常
  • setup_method 避免重复初始化

🔧 三、实践 2:用 Mock 隔离外部依赖

场景:测试 save_accounts() 时,不真正写入磁盘

# test_bank.py(新增)from unittest.mock import mock_open, patchfrom bank import save_accountsdef test_save_accounts():    """测试保存账户(使用 Mock 避免真实文件操作)"""    # 创建模拟账户    account = SavingsAccount("Test")    account.account_id = 999    account._balance = 500.0    # Mock open() 函数    with patch("builtins.open", mock_open()) as mock_file:        save_accounts([account])        # 验证 open 被调用        mock_file.assert_called_once_with("data/accounts.jsonl", "w", encoding="utf-8")        # 验证写入内容        handle = mock_file()        written_content = "".join(call[0][0] for call in handle.write.call_args_list)        assert '"owner": "Test"' in written_content        assert '"balance": 500.0' in written_content

🔑 Mock 的价值:

  • 测试不依赖文件系统/网络
  • 速度快(毫秒级)
  • 可模拟异常场景(如磁盘满)

模拟加载失败场景

def test_load_accounts_file_not_found():    """测试文件不存在时的行为"""    with patch("bank.os.path.exists", return_value=False):        accounts = list(load_accounts())  # 应返回空        assert len(accounts) == 0

🔧 四、运行测试 & 生成覆盖率报告

1. 运行所有测试

pytest  # 自动发现 test_*.py

2. 详细输出 + 覆盖率

pytest --cov=bank --cov-report=html --cov-report=term

📊 输出示例:

Name      Stmts   Miss  Cover----------------------------bank.py     143     45    69%

3. 查看 HTML 报告

  • 生成 htmlcov/index.html
  • 打开浏览器 → 红色=未覆盖,绿色=已覆盖

🧪 五、TDD 实战:先写测试,再实现功能

需求:添加“转账”功能

# bank.py(在 BankAccount 类中添加)def transfer_to(self, target_account: "BankAccount", amount: float) -> bool:    """    转账到目标账户    :param target_account: 收款账户    :param amount: 转账金额    :return: 是否成功    """    if amount <= 0:        raise InvalidAmountError(amount, "转账金额必须为正数")    if self is target_account:        raise BankError("不能转账给自己")    # 执行取款(会自动扣除手续费)    if not self.withdraw(amount):        return False    # 执行存款    target_account.deposit(amount)    # 记录日志    logger.info(        f"💰 转账成功 | 从账号: {self.account_id} | "        f"到账号: {target_account.account_id} | 金额: {amount:.2f}"    )    return True
步骤 1:先写测试(预期行为)
# test_bank.pydef test_transfer_between_accounts():    """测试账户间转账"""    alice = SavingsAccount("Alice")    bob = SavingsAccount("Bob")    alice.deposit(500)    alice.transfer_to(bob, 200)  # ← 功能尚未实现!    assert alice.balance == 299.0  # 500 - 200 - 1(手续费)    assert bob.balance == 200.0
步骤 2:运行测试 → 失败(红)
AttributeError: 'SavingsAccount' object has no attribute 'transfer_to'
步骤 3:实现功能(让测试通过 → 绿)
# bank.pyclass BankAccount(ABC):    # ...    def transfer_to(self, target_account, amount):        """转账到目标账户"""        if not isinstance(target_account, BankAccount):            raise ValueError("目标必须是 BankAccount 实例")        self.withdraw(amount)        target_account.deposit(amount)
步骤 4:运行测试 → 通过!
步骤 5:重构(如有需要)

✅ TDD 循环完成!


📝 小结:测试驱动开发的价值

没有测试的代码 = 未知行为的代码

通过 TDD,你将获得:

  • ✅ 即时反馈:修改代码后秒知是否破坏功能
  • ✅ 文档作用:测试即活文档(展示如何使用 API)
  • ✅ 设计改进:可测试的代码 = 解耦、模块化的好代码
  • ✅ 自信重构:大胆优化,不怕引入 bug

🎉 恭喜完成测试驱动开发进阶!你的银行系统现在不仅功能完整,而且坚如磐石!

继续加油,你的代码正在变得越来越专业、可靠!🛡️✨

最新文章

随机文章