太棒了!欢迎来到 【跟着AI学Python】Python进阶:测试驱动开发(TDD)!🎉今天我们将学习如何用 自动化测试 为你的银行系统保驾护航,确保代码可靠、可维护、无回归!
💡 TDD 核心思想:先写测试 → 再写实现 → 测试通过 → 重构让 bug 在开发阶段就被消灭!
🎯 今日目标
✅ 掌握 pytest(比 unittest 更简洁)编写单元测试✅ 为银行账户系统编写 全面测试用例✅ 使用 Mock 隔离外部依赖(如文件、网络)✅ 生成 测试覆盖率报告
📘 一、为什么选择 pytest?
| | |
|---|
| | |
| self.assertEqual() | |
| | @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. 运行所有测试
2. 详细输出 + 覆盖率
pytest --cov=bank --cov-report=html --cov-report=term
📊 输出示例:
Name Stmts Miss Cover----------------------------bank.py 143 45 69%
3. 查看 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)
- ✅ 设计改进:可测试的代码 = 解耦、模块化的好代码
🎉 恭喜完成测试驱动开发进阶!你的银行系统现在不仅功能完整,而且坚如磐石!
继续加油,你的代码正在变得越来越专业、可靠!🛡️✨