mockito 外部接口_原创 |使用JUnit、AssertJ和Mockito编写单元测试和实践TDD (五)第一个单元测试...

bd8f2bba590e16e23ab0f52268b4a5ca.png

bb816a34038a3665f90d9bef502d144f.gif

ede9157a263e8e2bd842fe2c99f12859.png

上一章讲到“关于单元测试的常见错误观念和做法”,这一章我们通过实例讲讲“第一个单元测试”到底应该怎么做。

1. 需求

我们要测试一个银行账户类Account的“取款”工作单元——withdraw()方法。我们先定义这个方法的契约:

  1. 如果账户被冻结,取款将失败,并抛出AccountLockedException异常
  2. 如果取款金额是0或者负数,取款将失败,并抛出InvalidAmountException异常。
  3. 如果余额不足,取款将失败,并抛出BalanceInsufficientException异常。
  4. 如果上述情况都没发生,取款将成功,账户余额会相应扣减,并在系统中记录这一笔交易。

下面是关键的业务规则:

  1. 如果取款由于任何原因失败,账户余额不会发生任何变化。
  2. 如果取款成功,账户余额将会相应减少,并在系统中记录这笔交易。

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) {
            throw new InvalidAmountException();
        }
        //失败路径3:取款金额超过余额时不允许取款
        if (amount > balance) {
            throw new BalanceInsufficientException();
        }
        //成功(快乐)路径
        balance -= amount;   //取款成功后改变内部状态
        transactions.add(this, TransactionType.CREDIT, amount); //取款成功后调用外部协作者
    }

    //冻结工作单元
    public void lock() {
        locked = true;
    }

    //解冻工作单元
    public void unlock() {
        locked = false;
    }
}

代码说明如下:

  • Account类有三个字段,其中locked和balance是两个内部状态,分别代表冻结状态和当前余额;transactions是外部依赖(协作者),用来记录存取交易。
  • Account类提供了isLocked()和getBalance()方法,分别将locked和balance内部状态暴露给外界。
  • Account类提供了lock()和unlock()方法来设置locked内部状态,deposit()和withdraw()来更改balance内部状态。
  • Account类提供了setTransactions()方法,用来注入外部依赖。

2.2 外部依赖Transactions接口

Transactions接口提供了记录每一笔存款、取款交易的方法add():

public interface Transactions {
    void add(Account account, TransactionType transactionType, int amount);
}

第一个参数记录交易关联的账户,第二个参数TransactionType是个枚举,表明是存款还是取款。第三个参数表示存取金额。

3. 单元测试

针对withdraw()契约和业务规则,我们编写下面一组单元测试来对它进行全面测试覆盖:

package yang.yu.tdd.bank;


import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*;

public class AccountWithdrawTest {

    private static final int ORIGINAL_BALANCE = 10000;

    private Transactions transactions;

    private Account account;

    @BeforeEach
    void setUp() {
        account = new Account();
        transactions = mock(Transactions.class);
        account.setTransactions(transactions);
        account.deposit(ORIGINAL_BALANCE);
    }

    //账户状态正常,取款金额小于当前余额时取款成功
    @Test
    void shouldSuccess() {
        int amountOfWithdraw = 2000;
        account.withdraw(amountOfWithdraw);
        assertThat(account.getBalance()).isEqualTo(ORIGINAL_BALANCE - amountOfWithdraw);
        verify(transactions).add(account, TransactionType.CREDIT, amountOfWithdraw);
    }

    //将余额全部取完,也可以取款成功
    @Test
    void shouldSuccessWhenWithdrawAll() {
        account.withdraw(ORIGINAL_BALANCE);
        assertThat(account.getBalance()).isEqualTo(0);
        verify(transactions).add(account, TransactionType.CREDIT, ORIGINAL_BALANCE);
    }

    //账户被冻结,取款应当失败
    @Test
    void shouldFailWhenAccountLocked() {
        account.lock();
        assertThrows(AccountLockedException.class, () -> {
            account.withdraw(2000);
        });
        assertThat(account.getBalance()).isEqualTo(ORIGINAL_BALANCE);
        verify(transactions, never()).add(account, TransactionType.CREDIT, 2000);
    }

    //取款金额是负数,取款应当失败
    @Test
    void shouldFailWhenAmountLessThanZero() {
        assertThrows(InvalidAmountException.class, () -> {
            account.withdraw(-1);
        });
        assertThat(account.getBalance()).isEqualTo(ORIGINAL_BALANCE);
        verify(transactions, never()).add(account, TransactionType.CREDIT, -1);
    }

    //取款金额是0,应当失败
    @Test
    void shouldFailWhenAmountEqualToZero() {
        assertThrows(InvalidAmountException.class, () -> {
            account.withdraw(0);
        });
        assertThat(account.getBalance()).isEqualTo(ORIGINAL_BALANCE);
        verify(transactions, never()).add(account, TransactionType.CREDIT, ORIGINAL_BALANCE);
    }

    //余额不足,应当失败
    @Test
    void shouldFailWhenBalanceInsufficient() {
        assertThrows(BalanceInsufficientException.class, () -> {
            account.withdraw(ORIGINAL_BALANCE + 1);
        });
        assertThat(account.getBalance()).isEqualTo(ORIGINAL_BALANCE);
        verify(transactions, never()).add(account, TransactionType.CREDIT, ORIGINAL_BALANCE + 1);
    }
}

上面的测试代码采用JUnit 5,Mockito 3和AssertJ 3编写。需要在JDK 8以上的版本运行。

说明:

  • 标注了@Test的方法是测试方法。方法没有返回值。一般情况下也没有参数。方法名字可以任意取,但最好能够充分表达测试意图。
  • 标注了@BeforeEach的方法,会在每一个测试方法执行之前都执行一次。方法名字可以任意取。

从上面每一个测试方法来看,每个测试通常都包含以下的过程:

  1. 创建被测对象。

java account = new Account();

  1. 设置内测对象的内部状态并注入外部依赖。对于单元测试,外部依赖应该用测试替身代替。

java //用Mockito创建测试替身,它实现了Transactions接口 transactions = mock(Transactions.class); //注入测试替身 account.setTransactions(transactions); //调用存款方法,设置初始余额 account.deposit(ORIGINAL_BALANCE); //调用冻结方法,设置冻结状态 account.lock();

  1. 调用被测试方法,执行测试。

java account.withdraw(amountOfWithdraw);

  1. 断言测试结果

成功时断言修改了内部状态并调用了外部依赖的方法:

java //断言当前余额等于原有余额减去取款金额 assertThat(account.getBalance()).isEqualTo(ORIGINAL_BALANCE - amountOfWithdraw); //断言调用了外部依赖transactions的add()方法,以account, TransactionType.CREDIT, amountOfWithdraw为参数 verify(transactions).add(account, TransactionType.CREDIT, amountOfWithdraw);

失败时断言抛出了期待的异常,余额没有减少并且没有调用外部依赖transactions来创建交易记录:

java //断言调用被测方法后抛出AccountLockedException异常 assertThrows(AccountLockedException.class, () -> { account.withdraw(2000); }); //断言余额没有减少 assertThat(account.getBalance()).isEqualTo(ORIGINAL_BALANCE); //断言没有调用外部依赖的方法 verify(transactions, never()).add(account, TransactionType.CREDIT, 2000);

上面的单元测试用到了本门课程将要介绍的三大框架:

  • JUnit用来编写测试的主体
  • Mockito用来创建外部依赖的测试替身,注入到被测对象。
  • AssertJ用来编写各种断言,断言单元测试的结果。虽然JUnit也包含了本身的断言库,但是内容不够丰富,形式不够优美。用AssertJ来写断言可读性等方面会好得多。

下一章将讲讲“测试哪些内容:Right-BICEP”!

- THE END -

原创作者 | 杨宇Yangyu

编程道与术原创内容

转载请注明“编程道与术”出处

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值