上一章讲到“关于单元测试的常见错误观念和做法”,这一章我们通过实例讲讲“第一个单元测试”到底应该怎么做。
1. 需求
我们要测试一个银行账户类Account的“取款”工作单元——withdraw()方法。我们先定义这个方法的契约:
- 如果账户被冻结,取款将失败,并抛出AccountLockedException异常
- 如果取款金额是0或者负数,取款将失败,并抛出InvalidAmountException异常。
- 如果余额不足,取款将失败,并抛出BalanceInsufficientException异常。
- 如果上述情况都没发生,取款将成功,账户余额会相应扣减,并在系统中记录这一笔交易。
下面是关键的业务规则:
- 如果取款由于任何原因失败,账户余额不会发生任何变化。
- 如果取款成功,账户余额将会相应减少,并在系统中记录这笔交易。
2. 实现
2.1 被测类Account
基于上面的契约和规则,我们编写了下面的实现(此处暂不采用TDD,我们先写好产品代码,再编写测试):
package yang.yu.tdd.bank;
//被测对象
public class Account {
//内部状态:账户是否被冻结
private boolean locked = false;
//内部状态:当前余额
private int balance = 0;
//外部依赖(协作者):记录每一笔收支
private Transactions transactions;
//用于注入外部协作者的方法
public void setTransactions(Transactions transactions) {
this.transactions = transactions;
}
public boolean isLocked() {
return locked;
}
public int getBalance() {
return balance;
}
//存款工作单元
public void deposit(int amount) {
//失败路径1:账户被冻结时不允许存款
if (locked) {
throw new AccountLockedException();
}
//失败路径2:存款金额不是正数时不允许存款
if (amount <= 0) {
throw new InvalidAmountException();
}
//成功(快乐)路径
balance += amount; //存款成功后改变内部状态
transactions.add(this, TransactionType.DEBIT, amount); //存款成功后调用外部协作者
}
//取款工作单元
public void withdraw(int amount) {
//失败路径1:账户被冻结时不允许取款
if (locked) {
throw new AccountLockedException();
}
//失败路径2:取款金额不是正数时不允许取款
if (amount <= 0